diff options
325 files changed, 8967 insertions, 3667 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml index 6ff8337..a4271ec 100644 --- a/.github/codeql.yml +++ b/.github/codeql.yml @@ -2,8 +2,12 @@ query-filters: - exclude: id: cpp/commented-out-code - exclude: + id: cpp/include-non-header + - exclude: id: cpp/long-switch - exclude: id: cpp/loop-variable-changed - exclude: id: cpp/poorly-documented-function + - exclude: + id: cpp/constant-comparison diff --git a/.github/diag.sh b/.github/diag.sh index 942487a..d89e7a4 100755 --- a/.github/diag.sh +++ b/.github/diag.sh @@ -8,9 +8,13 @@ set -eu +SEDFLAGS="-En" +if sed -u 's/s/s/' </dev/null &>/dev/null; then + SEDFLAGS="${SEDFLAGS}u" +fi + filter() { - sed -E 's/^(([^:]*):([^:]*):([^:]*): (warning|error): (.*))$/::\5 file=\2,line=\3,col=\4,title=Compiler \5::\6\ -\1/' + sed $SEDFLAGS 'p; s/^([^:]*):([^:]*):([^:]*): (warning|error): (.*)$/::\4 file=\1,line=\2,col=\3,title=Compiler \4::\5/p' } -"$@" 2> >(filter >&2) | filter +exec "$@" > >(filter) 2> >(filter >&2) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8011224..4075eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,12 @@ name: CI on: [push, pull_request] jobs: - linux: - name: Linux + linux-x86: + name: Linux (x86) + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + # Don't run on both pushes and pull requests + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -16,7 +18,7 @@ jobs: sudo dpkg --add-architecture i386 sudo apt-get update -y sudo apt-get install -y \ - expect \ + mandoc \ gcc-multilib \ libgcc-s1:i386 \ acl \ @@ -34,8 +36,6 @@ jobs: sudo ln -s libacl.so.1 /lib/i386-linux-gnu/libacl.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: | @@ -43,22 +43,52 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: linux-config.log + name: linux-x86-config.log + path: distcheck-*/gen/config.log + + linux-arm: + name: Linux (Arm64) + runs-on: ubuntu-24.04-arm + + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y \ + mandoc \ + acl \ + libacl1-dev \ + attr \ + libcap2-bin \ + libcap-dev \ + libonig-dev \ + liburing-dev + + - name: Run tests + run: | + .github/diag.sh make -j$(nproc) distcheck + + - uses: actions/upload-artifact@v4 + with: + name: linux-arm-config.log path: distcheck-*/gen/config.log macos: name: macOS + runs-on: macos-15 - runs-on: macos-14 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install \ - bash \ - expect + brew install bash - name: Run tests run: | @@ -67,25 +97,24 @@ jobs: freebsd: name: FreeBSD + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: cross-platform-actions/action@v0.24.0 + uses: cross-platform-actions/action@v0.28.0 with: operating_system: freebsd - version: "14.0" + version: "14.2" run: | sudo pkg install -y \ bash \ - expect \ oniguruma \ - pkgconf \ - tcl-wrapper + pkgconf sudo mount -t fdescfs none /dev/fd .github/diag.sh make -j$(nproc) distcheck @@ -96,22 +125,22 @@ jobs: openbsd: name: OpenBSD + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: cross-platform-actions/action@v0.24.0 + uses: cross-platform-actions/action@v0.28.0 with: operating_system: openbsd - version: "7.5" + version: "7.7" run: | sudo pkg_add \ bash \ - expect \ gmake \ oniguruma jobs=$(sysctl -n hw.ncpu) @@ -125,25 +154,25 @@ jobs: netbsd: name: NetBSD + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 - name: Run tests - uses: cross-platform-actions/action@v0.24.0 + uses: cross-platform-actions/action@v0.28.0 with: operating_system: netbsd - version: "10.0" + version: "10.1" run: | PATH="/sbin:/usr/sbin:$PATH" sudo pkgin -y install \ bash \ oniguruma \ - pkgconf \ - tcl-expect + pkgconf jobs=$(sysctl -n hw.ncpu) ./configure .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped" @@ -155,8 +184,9 @@ jobs: dragonflybsd: name: DragonFly BSD + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -170,11 +200,9 @@ jobs: prepare: | pkg install -y \ bash \ - expect \ oniguruma \ pkgconf \ - sudo \ - tcl-wrapper + sudo pw useradd -n action -m -G wheel -s /usr/local/bin/bash echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers @@ -191,8 +219,9 @@ jobs: omnios: name: OmniOS + runs-on: ubuntu-24.04 - runs-on: ubuntu-22.04 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - uses: actions/checkout@v4 @@ -200,14 +229,13 @@ jobs: - name: Run tests uses: vmactions/omnios-vm@v1 with: - release: "r151048" + release: "r151052" usesh: true prepare: | pkg install \ bash \ build-essential \ - expect \ gnu-make \ onig \ sudo diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 6aaace6..e4e8f71 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -13,7 +13,6 @@ jobs: run: | sudo apt-get update -y sudo apt-get install -y \ - expect \ gcc \ acl \ libacl1-dev \ @@ -29,7 +28,7 @@ jobs: make -j$(nproc) check TEST_FLAGS="--sudo" gcov -abcfpu obj/*/*.o - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a0b8fe3..1f2041c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,7 +13,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: actions: read contents: read @@ -1,4 +1,4 @@ -Copyright © 2015-2023 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors +Copyright © 2015-2025 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. @@ -27,10 +27,11 @@ asan lsan msan tsan ubsan gcov lint release:: # Print an error if `make` is run before `./configure` gen/config.mk:: - @if ! [ -e $@ ]; then \ + if ! [ -e $@ ]; then \ printf 'error: You must run `./configure` before `%s`.\n' "${MAKE}" >&2; \ false; \ fi +.SILENT: gen/config.mk ## Build phase (`make`) @@ -42,46 +43,72 @@ bfs: bin/bfs BINS := \ bin/bfs \ bin/tests/mksock \ + bin/tests/ptyx \ bin/tests/units \ bin/tests/xspawnee \ - bin/tests/xtouch + bin/tests/xtouch \ + bin/bench/ioq all: ${BINS} .PHONY: all +# All object files except the entry point +LIBBFS := \ + obj/src/alloc.o \ + obj/src/bar.o \ + obj/src/bfstd.o \ + obj/src/bftw.o \ + obj/src/color.o \ + obj/src/ctx.o \ + obj/src/diag.o \ + obj/src/dir.o \ + obj/src/dstring.o \ + obj/src/eval.o \ + obj/src/exec.o \ + obj/src/expr.o \ + obj/src/fsade.o \ + obj/src/ioq.o \ + obj/src/mtab.o \ + obj/src/opt.o \ + obj/src/parse.o \ + obj/src/printf.o \ + obj/src/pwcache.o \ + obj/src/sighook.o \ + obj/src/stat.o \ + obj/src/thread.o \ + obj/src/trie.o \ + obj/src/typo.o \ + obj/src/version.o \ + obj/src/xregex.o \ + obj/src/xspawn.o \ + obj/src/xtime.o + +# All object files +OBJS := ${LIBBFS} + # The main binary -bin/bfs: ${LIBBFS} obj/src/main.o +bin/bfs: obj/src/main.o ${LIBBFS} +OBJS += obj/src/main.o ${BINS}: @${MKDIR} ${@D} - +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} ${.ALLSRC} ${_LDLIBS} -o $@ + +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} $^ ${_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.i.new:: + ${MKDIR} ${@D} + build/version.sh | tr -d '\n' | build/embed.sh >$@ +.SILENT: gen/version.i.new -gen/version.c: gen/version.c.new - @test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@ +gen/version.i: gen/version.i.new + test -e $@ && cmp -s $@ $^ && ${RM} $^ || mv $^ $@ +.SILENT: gen/version.i -obj/gen/version.o: gen/version.c +obj/src/version.o: gen/version.i ## Test phase (`make check`) @@ -93,6 +120,7 @@ UTEST_BINS := \ # Integration test binaries ITEST_BINS := \ bin/tests/mksock \ + bin/tests/ptyx \ bin/tests/xtouch # Build (but don't run) test binaries @@ -108,12 +136,24 @@ 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 +# Unit test objects +UNIT_OBJS := \ + obj/tests/alloc.o \ + obj/tests/bfstd.o \ + obj/tests/bit.o \ + obj/tests/ioq.o \ + obj/tests/list.o \ + obj/tests/main.o \ + obj/tests/sighook.o \ + obj/tests/trie.o \ + obj/tests/xspawn.o \ + obj/tests/xtime.o + +bin/tests/units: ${UNIT_OBJS} ${LIBBFS} +OBJS += ${UNIT_OBJS} + +bin/tests/xspawnee: obj/tests/xspawnee.o +OBJS += obj/tests/xspawnee.o # The different flag combinations we check INTEGRATIONS := default dfs ids eds j1 j2 j3 s @@ -138,13 +178,14 @@ check-j1 check-j2 check-j3 check-s: bin/bfs ${ITEST_BINS} integration-tests: ${INTEGRATION_TESTS} .PHONY: integration-tests -bin/tests/mksock: \ - obj/tests/mksock.o \ - ${LIBBFS} +bin/tests/mksock: obj/tests/mksock.o ${LIBBFS} +OBJS += obj/tests/mksock.o + +bin/tests/ptyx: obj/tests/ptyx.o ${LIBBFS} +OBJS += obj/tests/ptyx.o -bin/tests/xtouch: \ - obj/tests/xtouch.o \ - ${LIBBFS} +bin/tests/xtouch: obj/tests/xtouch.o ${LIBBFS} +OBJS += obj/tests/xtouch.o # `make distcheck` configurations DISTCHECKS := \ @@ -161,6 +202,8 @@ distcheck: @+test "$$(uname)" = FreeBSD || ${MAKE} distcheck-tsan @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32 @+${MAKE} distcheck-release + @+${MAKE} -C distcheck-release check-install + @+test "$$(uname)" != Linux || ${MAKE} check-man .PHONY: distcheck # Per-distcheck configuration @@ -172,11 +215,41 @@ DISTCHECK_CONFIG_release := --enable-release ${DISTCHECKS}:: @${MKDIR} $@ + @test "$${GITHUB_ACTIONS-}" != true || printf '::group::%s\n' $@ @+cd $@ \ - && ../configure ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \ + && ../configure MAKE="${MAKE}" ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \ && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped" + @test "$${GITHUB_ACTIONS-}" != true || printf '::endgroup::\n' + +## Benchmarks (`make bench`) + +bench: bin/bench/ioq +.PHONY: bench + +bin/bench/ioq: obj/bench/ioq.o ${LIBBFS} +OBJS += obj/bench/ioq.o -## Packaging (`make install`) +## Automatic dependency tracking + +# Rebuild when the configuration changes +${OBJS}: gen/config.mk + @${MKDIR} ${@D} + ${MSG} "[ CC ] ${CSRC}" ${CC} ${_CPPFLAGS} ${_CFLAGS} -c ${CSRC} -o $@ + +# Include any generated dependency files +-include ${OBJS:.o=.d} + +## Packaging (`make dist`, `make install`) + +TARBALL = bfs-$$(build/version.sh).tar.gz + +dist: + ${MSG} "[DIST] ${TARBALL}" git archive HEAD -o ${TARBALL} + +distsign: dist + ${MSG} "[SIGN] ${TARBALL}" ssh-keygen -Y sign -q -f $$(git config user.signingkey) -n file ${TARBALL} + +.PHONY: dist distsign DEST_PREFIX := ${DESTDIR}${PREFIX} DEST_MANDIR := ${DESTDIR}${MANDIR} @@ -217,6 +290,12 @@ check-install:: bin/bfs pkg -not -type d -print -exit 1 ${RM} -r pkg +# Check man page markup +check-man:: + ${MSG} "[LINT] docs/bfs.1" + ${Q}groff -man -rCHECKSTYLE=3 -ww -b -z docs/bfs.1 + ${Q}mandoc -Tlint -Wwarning docs/bfs.1 + ## Cleanup (`make clean`) # Clean all build products @@ -226,6 +305,6 @@ clean:: # Clean everything, including generated files distclean: clean - ${MSG} "[ RM ] gen" \ + ${MSG} "[ RM ] gen distcheck-*" \ ${RM} -r gen ${DISTCHECKS} .PHONY: distclean @@ -34,7 +34,7 @@ It is otherwise compatible with many versions of `find`, including **[POSIX] • [GNU] • [FreeBSD] • [OpenBSD] • [NetBSD] • [macOS]** -[POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html +[POSIX]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html [GNU]: https://www.gnu.org/software/findutils/ [FreeBSD]: https://www.freebsd.org/cgi/man.cgi?find(1) [OpenBSD]: https://man.openbsd.org/find.1 diff --git a/bench/bench.sh b/bench/bench.sh index cf1ae49..c9ed978 100644 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -22,6 +22,7 @@ PRINT_DEFAULT=(linux) STRATEGIES_DEFAULT=(rust) JOBS_DEFAULT=(rust) EXEC_DEFAULT=(linux) +SORTED_DEFAULT=(chromium) usage() { printf 'Usage: tailfin run %s\n' "${BASH_SOURCE[0]}" @@ -60,6 +61,10 @@ usage() { printf ' Process spawning benchmark.\n' printf ' Default corpus is --exec=%s\n\n' "${EXEC_DEFAULT[*]}" + printf ' --sorted[=CORPUS]\n' + printf ' Sorted traversal benchmark.\n' + printf ' Default corpus is --sorted=%s\n\n' "${SORTED_DEFAULT[*]}" + printf ' --build=COMMIT\n' printf ' Build this bfs commit and benchmark it. Specify multiple times to\n' printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n' @@ -121,6 +126,7 @@ setup() { STRATEGIES=() JOBS=() EXEC=() + SORTED=() for arg; do case "$arg" in @@ -195,6 +201,12 @@ setup() { --exec=*) read -ra EXEC <<<"${arg#*=}" ;; + --sorted) + SORTED=("${SORTED_DEFAULT[@]}") + ;; + --sorted=*) + read -ra SORTED <<<"${arg#*=}" + ;; --default) COMPLETE=("${COMPLETE_DEFAULT[@]}") EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}") @@ -203,6 +215,7 @@ setup() { STRATEGIES=("${STRATEGIES_DEFAULT[@]}") JOBS=("${JOBS_DEFAULT[@]}") EXEC=("${EXEC_DEFAULT[@]}") + SORTED=("${SORTED_DEFAULT[@]}") ;; --help) usage @@ -227,7 +240,7 @@ setup() { as-user mkdir -p bench/corpus declare -A cloned=() - for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}"; do + for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}" "${SORTED[@]}"; do if ((cloned["$corpus"])); then continue fi @@ -269,12 +282,7 @@ setup() { ) done - # $SETUP_DIR contains `:` so it won't work in $PATH - # Work around this with a symlink - tmp=$(as-user mktemp) - as-user ln -sf "$bin" "$tmp" - defer rm "$tmp" - export PATH="$tmp:$PATH" + export PATH="$bin:$PATH" fi export_array BFS @@ -288,6 +296,7 @@ setup() { export_array STRATEGIES export_array JOBS export_array EXEC + export_array SORTED if ((UID == 0)); then turbo-off @@ -365,7 +374,7 @@ bench-complete() { fi } -# Benchmark quiting as soon as a file is seen +# Benchmark quitting as soon as a file is seen bench-early-quit-corpus() { dir="$2" max_depth=$(./bin/bfs "$dir" -printf '%d\n' | sort -rn | head -n1) @@ -655,6 +664,29 @@ bench-exec() { fi } +# Benchmark sorted traversal +bench-sorted-corpus() { + subgroup '%s' "$1" + + cmds=() + for bfs in "${BFS[@]}"; do + cmds+=("$bfs -s $2 -false") + done + + do-hyperfine "${cmds[@]}" +} + +# All sorted traversal benchmarks +bench-sorted() { + if (($#)); then + group "Sorted traversal" + + for corpus; do + bench-sorted-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus" + done + fi +} + # Print benchmarked versions bench-versions() { subgroup "Versions" @@ -703,6 +735,7 @@ bench() { import_array STRATEGIES import_array JOBS import_array EXEC + import_array SORTED bench-complete "${COMPLETE[@]}" bench-early-quit "${EARLY_QUIT[@]}" @@ -711,5 +744,6 @@ bench() { bench-strategies "${STRATEGIES[@]}" bench-jobs "${JOBS[@]}" bench-exec "${EXEC[@]}" + bench-sorted "${SORTED[@]}" bench-details } diff --git a/bench/ioq.c b/bench/ioq.c new file mode 100644 index 0000000..fb9edbc --- /dev/null +++ b/bench/ioq.c @@ -0,0 +1,455 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "atomic.h" +#include "bfs.h" +#include "bfstd.h" +#include "diag.h" +#include "ioq.h" +#include "sighook.h" +#include "xtime.h" + +#include <errno.h> +#include <locale.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> + +/** A latency sample. */ +struct lat { + /** The sampled latency. */ + struct timespec time; + /** A random integer, for reservoir sampling. */ + long key; +}; + +/** Number of latency samples to keep. */ +#define SAMPLES 1000 +/** Latency sampling period. */ +#define PERIOD 128 + +/** Latency measurements. */ +struct lats { + /** Lowest observed latency. */ + struct timespec min; + /** Highest observed latency. */ + struct timespec max; + /** Total latency. */ + struct timespec sum; + /** Number of measured requests. */ + size_t count; + + /** Priority queue for reservoir sampling. */ + struct lat heap[SAMPLES]; + /** Current size of the heap. */ + size_t heap_size; +}; + +/** Initialize a latency reservoir. */ +static void lats_init(struct lats *lats) { + lats->min = (struct timespec) { .tv_sec = 1000 }; + lats->max = (struct timespec) { 0 }; + lats->sum = (struct timespec) { 0 }; + lats->count = 0; + lats->heap_size = 0; +} + +/** Binary heap parent. */ +static size_t heap_parent(size_t i) { + return (i - 1) / 2; +} + +/** Binary heap left child. */ +static size_t heap_child(size_t i) { + return 2 * i + 1; +} + +/** Binary heap smallest child. */ +static size_t heap_min_child(const struct lats *lats, size_t i) { + size_t j = heap_child(i); + size_t k = j + 1; + if (k < lats->heap_size && lats->heap[k].key < lats->heap[j].key) { + return k; + } else { + return j; + } +} + +/** Check if the heap property is met. */ +static bool heap_check(const struct lat *parent, const struct lat *child) { + return parent->key <= child->key; +} + +/** Reservoir sampling. */ +static void heap_push(struct lats *lats, const struct lat *lat) { + size_t i; + + if (lats->heap_size < SAMPLES) { + // Heapify up + i = lats->heap_size++; + while (i > 0) { + size_t j = heap_parent(i); + if (heap_check(&lats->heap[j], lat)) { + break; + } + lats->heap[i] = lats->heap[j]; + i = j; + } + } else if (lat->key > lats->heap[0].key) { + // Heapify down + i = 0; + while (true) { + size_t j = heap_min_child(lats, i); + if (j >= SAMPLES || heap_check(lat, &lats->heap[j])) { + break; + } + lats->heap[i] = lats->heap[j]; + i = j; + } + } else { + // Reject + return; + } + + lats->heap[i] = *lat; +} + +/** Add a latency sample. */ +static void lats_push(struct lats *lats, const struct timespec *ts) { + timespec_min(&lats->min, ts); + timespec_max(&lats->max, ts); + timespec_add(&lats->sum, ts); + ++lats->count; + + struct lat lat = { + .time = *ts, + .key = lrand48(), + }; + heap_push(lats, &lat); +} + +/** Merge two latency reservoirs. */ +static void lats_merge(struct lats *into, const struct lats *from) { + timespec_min(&into->min, &from->min); + timespec_max(&into->max, &from->max); + timespec_add(&into->sum, &from->sum); + into->count += from->count; + + for (size_t i = 0; i < from->heap_size; ++i) { + heap_push(into, &from->heap[i]); + } +} + +/** Latency qsort() comparator. */ +static int lat_cmp(const void *a, const void *b) { + const struct lat *la = a; + const struct lat *lb = b; + return timespec_cmp(&la->time, &lb->time); +} + +/** Sort the latency reservoir. */ +static void lats_sort(struct lats *lats) { + qsort(lats->heap, lats->heap_size, sizeof(lats->heap[0]), lat_cmp); +} + +/** Get the nth percentile. */ +static const struct timespec *lats_percentile(const struct lats *lats, int percent) { + size_t i = lats->heap_size * percent / 100; + return &lats->heap[i].time; +} + +/** Which clock to use for benchmarking. */ +static clockid_t clockid = CLOCK_REALTIME; + +/** Get a current time measurement. */ +static void gettime(struct timespec *tp) { + int ret = clock_gettime(clockid, tp); + bfs_everify(ret == 0, "clock_gettime(%d)", (int)clockid); +} + +/** + * Time measurements. + */ +struct times { + /** The start time. */ + struct timespec start; + + /** Total requests started. */ + size_t pushed; + /** Total requests finished. */ + size_t popped; + + /** The start time for the currently tracked request. */ + struct timespec req_start; + /** Whether a timed request is currently in flight. */ + bool timing; + + /** Latency measurements. */ + struct lats lats; +}; + +/** Initialize a timer. */ +static void times_init(struct times *times) { + gettime(×->start); + times->pushed = 0; + times->popped = 0; + bfs_assert(!times->timing); + lats_init(×->lats); +} + +/** Finish timing a request. */ +static void track_latency(struct times *times) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, ×->req_start); + lats_push(×->lats, &elapsed); + + bfs_assert(times->timing); + times->timing = false; +} + +/** Add times to the totals, and reset the lap times. */ +static void times_lap(struct times *total, struct times *lap) { + total->pushed += lap->pushed; + total->popped += lap->popped; + lats_merge(&total->lats, &lap->lats); + + times_init(lap); +} + +/** Print some times. */ +static void times_print(struct times *times, long seconds) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, ×->start); + + double fsec = timespec_ns(&elapsed) / 1.0e9; + + if (seconds > 0) { + printf("%5ld", seconds); + } else if (elapsed.tv_nsec >= 10 * 1000 * 1000) { + printf("%5.2f", fsec); + } else { + printf("%5.0f", fsec); + } + + double iops = times->popped / fsec; + double mean = timespec_ns(×->lats.sum) / times->lats.count; + double min = timespec_ns(×->lats.min); + double max = timespec_ns(×->lats.max); + + lats_sort(×->lats); + double n50 = timespec_ns(lats_percentile(×->lats, 50)); + double n90 = timespec_ns(lats_percentile(×->lats, 90)); + double n99 = timespec_ns(lats_percentile(×->lats, 99)); + + printf(" │ %'12.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f\n", iops, mean, min, n50, n90, n99, max); + fflush(stdout); +} + +/** Push an ioq request. */ +static bool push(struct ioq *ioq, enum ioq_nop_type type, struct times *lap) { + void *ptr = NULL; + + // Track latency for a small fraction of requests + if (!lap->timing && (lap->pushed + 1) % PERIOD == 0) { + ptr = lap; + gettime(&lap->req_start); + } + + int ret = ioq_nop(ioq, type, ptr); + if (ret != 0) { + bfs_everify(errno == EAGAIN, "ioq_nop(%d)", (int)type); + return false; + } + + ++lap->pushed; + if (ptr) { + lap->timing = true; + } + return true; +} + +/** Pop an ioq request. */ +static bool pop(struct ioq *ioq, struct times *lap, bool block) { + struct ioq_ent *ent = ioq_pop(ioq, block); + if (!ent) { + return false; + } + + if (ent->ptr) { + track_latency(lap); + } + + ioq_free(ioq, ent); + ++lap->popped; + return true; +} + +/** ^C flag. */ +static atomic bool quit = false; + +/** ^C hook. */ +static void ctrlc(int sig, siginfo_t *info, void *arg) { + store(&quit, true, relaxed); +} + +int main(int argc, char *argv[]) { + // Use CLOCK_MONOTONIC if available +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clockid = CLOCK_MONOTONIC; + } +#endif + + // Enable thousands separators + setlocale(LC_ALL, ""); + + // -d: queue depth + unsigned int depth = 4096; + // -j: threads + unsigned int threads = 0; + // -t: timeout + double timeout = 5.0; + // -L|-H: ioq_nop() type + enum ioq_nop_type type = IOQ_NOP_LIGHT; + + const char *cmd = argc > 0 ? argv[0] : "ioq"; + int c; + while (c = getopt(argc, argv, ":d:j:t:LH"), c != -1) { + switch (c) { + case 'd': + if (xstrtoui(optarg, NULL, 10, &depth) != 0) { + fprintf(stderr, "%s: Bad depth '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 'j': + if (xstrtoui(optarg, NULL, 10, &threads) != 0) { + fprintf(stderr, "%s: Bad thread count '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 't': + if (xstrtod(optarg, NULL, &timeout) != 0) { + fprintf(stderr, "%s: Bad timeout '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case 'L': + type = IOQ_NOP_LIGHT; + break; + case 'H': + type = IOQ_NOP_HEAVY; + break; + case ':': + fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt); + return EXIT_FAILURE; + case '?': + fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt); + return EXIT_FAILURE; + } + } + + if (!threads) { + threads = nproc(); + if (threads > 8) { + threads = 8; + } + } + if (threads < 2) { + threads = 2; + } + --threads; + + // Listen for ^C to print the summary + struct sighook *hook = sighook(SIGINT, ctrlc, NULL, SH_CONTINUE | SH_ONESHOT); + + printf("I/O queue benchmark (%s)\n\n", bfs_version); + + printf("[-d] depth: %u\n", depth); + printf("[-j] threads: %u (including main)\n", threads + 1); + if (type == IOQ_NOP_HEAVY) { + printf("[-H] type: heavy (with syscalls)\n"); + } else { + printf("[-L] type: light (no syscalls)\n"); + } + printf("\n"); + + printf(" Time │ Throughput │ Latency │ min │ 50%% │ 90%% │ 99%% │ max\n"); + printf(" (s) │ (IO/s) │ (ns/IO) │ │ │ │ │\n"); + printf("══════╪══════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════\n"); + fflush(stdout); + + struct ioq *ioq = ioq_create(depth, threads); + bfs_everify(ioq, "ioq_create(%u, %u)", depth, threads); + + // Pre-allocate all the requests + while (ioq_capacity(ioq) > 0) { + int ret = ioq_nop(ioq, type, NULL); + bfs_everify(ret == 0, "ioq_nop(%d)", (int)type); + } + while (true) { + struct ioq_ent *ent = ioq_pop(ioq, true); + if (!ent) { + break; + } + ioq_free(ioq, ent); + } + + struct times total, lap; + times_init(&total); + lap = total; + + long seconds = 0; + while (!load(&quit, relaxed)) { + bool was_timing = lap.timing; + + for (int i = 0; i < 16; ++i) { + bool block = ioq_capacity(ioq) == 0; + if (!pop(ioq, &lap, block)) { + break; + } + } + + if (was_timing && !lap.timing) { + struct timespec elapsed; + gettime(&elapsed); + timespec_sub(&elapsed, &total.start); + + if (elapsed.tv_sec > seconds) { + seconds = elapsed.tv_sec; + times_print(&lap, seconds); + times_lap(&total, &lap); + } + + double ns = timespec_ns(&elapsed); + if (timeout > 0 && ns >= timeout * 1.0e9) { + break; + } + } + + for (int i = 0; i < 8; ++i) { + if (!push(ioq, type, &lap)) { + break; + } + } + ioq_submit(ioq); + } + + while (pop(ioq, &lap, true)); + times_lap(&total, &lap); + + if (load(&quit, relaxed)) { + printf("\r──^C──┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n"); + } else { + printf("──────┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n"); + } + times_print(&total, 0); + + ioq_destroy(ioq); + sigunhook(hook); + return 0; +} diff --git a/build/cc.sh b/build/cc.sh index 45d51ca..e1d2b0b 100755 --- a/build/cc.sh +++ b/build/cc.sh @@ -3,14 +3,32 @@ # Copyright © Tavian Barnes <tavianator@tavianator.com> # SPDX-License-Identifier: 0BSD -# Run the compiler and check if it succeeded +# Run the compiler and check if it succeeded. Usage: +# +# $ build/cc.sh [-q] path/to/file.c [-flags -Warnings ...] set -eu -TMP=$(mktemp) -trap 'rm -f "$TMP"' EXIT +QUIET= +if [ "$1" = "-q" ]; then + QUIET=y + shift +fi -( +# Source files can specify their own flags with lines like +# +# /// _CFLAGS += -Wmissing-variable-declarations +# +# which will be added to the makefile on success, or lines like +# +# /// -Werror +# +# which are just used for the current file. +EXTRA_FLAGS=$(sed -n '\|^///|{s|^/// ||; s|[^=]*= ||; p;}' "$1") + +# Without -q, print the executed command for config.log +if [ -z "$QUIET" ]; then set -x - $XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS -o "$TMP" -) +fi + +$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $EXTRA_FLAGS $XLDLIBS diff --git a/build/config.mk b/build/config.mk index 24873ec..663926c 100644 --- a/build/config.mk +++ b/build/config.mk @@ -10,19 +10,12 @@ include build/exports.mk 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} +gen/config.mk: gen/vars.mk gen/flags.mk gen/pkgs.mk ${MSG} "[ GEN] $@" @printf '# %s\n' "$@" >$@ - @printf 'include %s\n' ${.ALLSRC} >>$@ - ${VCAT} gen/config.mk + @printf 'include %s\n' $^ >>$@ + ${VCAT} $@ .PHONY: gen/config.mk # Saves the configurable variables @@ -38,6 +31,7 @@ gen/vars.mk:: @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@ @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@ @printf 'RM := %s\n' "$$XRM" >>$@ + @test -z "$$VERSION" || printf 'export VERSION=%s\n' "$$VERSION" >>$@ ${VCAT} $@ # Sets the build flags. This depends on vars.mk and uses a recursive make so @@ -46,17 +40,12 @@ 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 +gen/config.h: gen/pkgs.mk @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@ .PHONY: gen/config.h diff --git a/build/define-if.sh b/build/define-if.sh index 295ead8..204cfa4 100755 --- a/build/define-if.sh +++ b/build/define-if.sh @@ -7,9 +7,7 @@ set -eu -SLUG="${1#build/}" -SLUG="${SLUG%.c}" -MACRO="BFS_$(printf '%s' "$SLUG" | tr '/a-z-' '_A-Z_')" +MACRO=$(printf 'BFS_%s' "$1" | tr '/a-z-' '_A-Z_') shift if "$@"; then diff --git a/build/deps.mk b/build/deps.mk deleted file mode 100644 index d382f5d..0000000 --- a/build/deps.mk +++ /dev/null @@ -1,18 +0,0 @@ -# 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/embed.sh b/build/embed.sh new file mode 100755 index 0000000..c0744f6 --- /dev/null +++ b/build/embed.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Convert data into a C array like #embed + +set -eu + +{ cat; printf '\0'; } \ + | od -An -tx1 \ + | sed 's/[^ ][^ ]*/0x&,/g' diff --git a/build/flags-if.sh b/build/flags-if.sh new file mode 100755 index 0000000..81eb345 --- /dev/null +++ b/build/flags-if.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Add flags to a makefile if a build succeeds + +set -eu + +build/cc.sh "$@" || exit 1 + +# If the build succeeded, print any lines like +# +# /// _CFLAGS += -foo +# +# (unless they're already set) +OLD_FLAGS="$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS $XLDLIBS" + +while IFS="" read -r line; do + case "$line" in + ///*=*) + flag="${line#*= }" + if [ "${OLD_FLAGS#*"$flag"}" = "$OLD_FLAGS" ]; then + printf '%s\n' "${line#/// }" + fi + ;; + esac +done <"$1" diff --git a/build/flags.mk b/build/flags.mk index c50b077..3748a8a 100644 --- a/build/flags.mk +++ b/build/flags.mk @@ -7,27 +7,17 @@ include build/prelude.mk include gen/vars.mk # Internal flags -_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 - -_CFLAGS := -std=c17 -pthread +_CPPFLAGS := -Isrc -Igen -include src/prelude.h +_CFLAGS := -std=c17 _LDFLAGS := _LDLIBS := # Platform-specific system libraries LDLIBS,DragonFly := -lposix1e +LDLIBS,FreeBSD := -lrt LDLIBS,Linux := -lrt LDLIBS,NetBSD := -lutil +LDLIBS,QNX := -lregex -lsocket LDLIBS,SunOS := -lsec -lsocket -lnsl _LDLIBS += ${LDLIBS,${OS}} @@ -41,9 +31,8 @@ _GCOV := ${TRUTHY,${GCOV}} _LINT := ${TRUTHY,${LINT}} _RELEASE := ${TRUTHY,${RELEASE}} -# https://github.com/google/sanitizers/issues/342 -TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0 -_CPPFLAGS += ${TSAN_CPPFLAGS,${_TSAN}} +LTO ?= ${RELEASE} +_LTO := ${TRUTHY,${LTO}} ASAN_CFLAGS,y := -fsanitize=address LSAN_CFLAGS,y := -fsanitize=leak @@ -77,22 +66,16 @@ _CPPFLAGS += ${LINT_CPPFLAGS,${_LINT}} _CFLAGS += ${LINT_CFLAGS,${_LINT}} RELEASE_CPPFLAGS,y := -DNDEBUG -RELEASE_CFLAGS,y := -O3 -flto=auto +RELEASE_CFLAGS,y := -O3 _CPPFLAGS += ${RELEASE_CPPFLAGS,${_RELEASE}} _CFLAGS += ${RELEASE_CFLAGS,${_RELEASE}} +LTO_CFLAGS,y := -flto=auto +_CFLAGS += ${LTO_CFLAGS,${_LTO}} + # Configurable flags -CFLAGS ?= \ - -g \ - -Wall \ - -Wformat=2 \ - -Werror=implicit \ - -Wimplicit-fallthrough \ - -Wmissing-declarations \ - -Wshadow \ - -Wsign-compare \ - -Wstrict-prototypes +CFLAGS ?= -g -Wall # Add the configurable flags last so they can override ours _CPPFLAGS += ${CPPFLAGS} ${EXTRA_CPPFLAGS} @@ -103,7 +86,22 @@ _LDLIBS := ${LDLIBS} ${EXTRA_LDLIBS} ${_LDLIBS} include build/exports.mk -gen/flags.mk:: +# Conditionally-supported flags +AUTO_FLAGS := \ + gen/flags/Wformat.mk \ + gen/flags/Wimplicit-fallthrough.mk \ + gen/flags/Wimplicit.mk \ + gen/flags/Wmissing-decls.mk \ + gen/flags/Wmissing-var-decls.mk \ + gen/flags/Wshadow.mk \ + gen/flags/Wsign-compare.mk \ + gen/flags/Wstrict-prototypes.mk \ + gen/flags/Wundef-prefix.mk \ + gen/flags/bind-now.mk \ + gen/flags/deps.mk \ + gen/flags/pthread.mk + +gen/flags.mk: ${AUTO_FLAGS} ${MSG} "[ GEN] $@" @printf '# %s\n' "$@" >$@ @printf '_CPPFLAGS := %s\n' "$$XCPPFLAGS" >>$@ @@ -112,4 +110,27 @@ gen/flags.mk:: @printf '_LDLIBS := %s\n' "$$XLDLIBS" >>$@ @printf 'NOLIBS := %s\n' "$$XNOLIBS" >>$@ @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@ + @cat $^ >>$@ + @cat ${^:%=%.log} >gen/flags.log ${VCAT} $@ +.PHONY: gen/flags.mk + +# Check that the C compiler works at all +cc:: + @build/cc.sh -q build/empty.c -o gen/.cc.out; \ + ret=$$?; \ + build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \ + exit $$ret + +# The short name of the config test +SLUG = ${@:gen/%.mk=%} +# The source file to build +CSRC = build/${SLUG}.c +# The hidden output file name +OUT = ${SLUG:flags/%=gen/flags/.%.out} + +${AUTO_FLAGS}: cc + @${MKDIR} ${@D} + @build/flags-if.sh ${CSRC} -o ${OUT} >$@ 2>$@.log; \ + build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0 +.PHONY: ${AUTO_FLAGS} diff --git a/build/has/aligned-alloc.c b/build/flags/Wformat.c index 4460038..287b209 100644 --- a/build/has/aligned-alloc.c +++ b/build/flags/Wformat.c @@ -1,8 +1,9 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include <stdlib.h> +/// _CFLAGS += -Wformat=2 +/// -Werror int main(void) { - return !aligned_alloc(_Alignof(void *), sizeof(void *)); + return 0; } diff --git a/build/flags/Wimplicit-fallthrough.c b/build/flags/Wimplicit-fallthrough.c new file mode 100644 index 0000000..c32058d --- /dev/null +++ b/build/flags/Wimplicit-fallthrough.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wimplicit-fallthrough +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wimplicit.c b/build/flags/Wimplicit.c new file mode 100644 index 0000000..3ea2b90 --- /dev/null +++ b/build/flags/Wimplicit.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Werror=implicit +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wmissing-decls.c b/build/flags/Wmissing-decls.c new file mode 100644 index 0000000..5ef3e96 --- /dev/null +++ b/build/flags/Wmissing-decls.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wmissing-declarations +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wmissing-var-decls.c b/build/flags/Wmissing-var-decls.c new file mode 100644 index 0000000..5c20cc6 --- /dev/null +++ b/build/flags/Wmissing-var-decls.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wmissing-variable-declarations +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wshadow.c b/build/flags/Wshadow.c new file mode 100644 index 0000000..28f6ef3 --- /dev/null +++ b/build/flags/Wshadow.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wshadow +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wsign-compare.c b/build/flags/Wsign-compare.c new file mode 100644 index 0000000..f083083 --- /dev/null +++ b/build/flags/Wsign-compare.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wsign-compare +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wstrict-prototypes.c b/build/flags/Wstrict-prototypes.c new file mode 100644 index 0000000..9614bee --- /dev/null +++ b/build/flags/Wstrict-prototypes.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -Wstrict-prototypes +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/Wundef-prefix.c b/build/flags/Wundef-prefix.c new file mode 100644 index 0000000..3eaf82b --- /dev/null +++ b/build/flags/Wundef-prefix.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CPPFLAGS += -Wundef-prefix=BFS_ +/// -Werror + +int main(void) { + return 0; +} diff --git a/build/flags/bind-now.c b/build/flags/bind-now.c new file mode 100644 index 0000000..08bb4f2 --- /dev/null +++ b/build/flags/bind-now.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _LDFLAGS += -Wl,-z,now + +int main(void) { + return 0; +} diff --git a/build/flags/deps.c b/build/flags/deps.c new file mode 100644 index 0000000..1c8c309 --- /dev/null +++ b/build/flags/deps.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CPPFLAGS += -MD -MP + +int main(void) { + return 0; +} diff --git a/build/flags/pthread.c b/build/flags/pthread.c new file mode 100644 index 0000000..db09aa4 --- /dev/null +++ b/build/flags/pthread.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// _CFLAGS += -pthread + +int main(void) { + return 0; +} diff --git a/build/has/_Fork.c b/build/has/_Fork.c new file mode 100644 index 0000000..4d7fbd3 --- /dev/null +++ b/build/has/_Fork.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <unistd.h> + +int main(void) { + return _Fork(); +} diff --git a/build/has/builtin-riscv-pause.c b/build/has/builtin-riscv-pause.c new file mode 100644 index 0000000..24b0675 --- /dev/null +++ b/build/has/builtin-riscv-pause.c @@ -0,0 +1,7 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +int main(void) { + __builtin_riscv_pause(); + return 0; +} diff --git a/build/has/dprintf.c b/build/has/dprintf.c new file mode 100644 index 0000000..c206fa3 --- /dev/null +++ b/build/has/dprintf.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <stdio.h> + +int main(void) { + return dprintf(1, "%s\n", "Hello world!"); +} diff --git a/build/has/io-uring-max-workers.c b/build/has/io-uring-max-workers.c new file mode 100644 index 0000000..34ab5b7 --- /dev/null +++ b/build/has/io-uring-max-workers.c @@ -0,0 +1,11 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <liburing.h> + +int main(void) { + struct io_uring ring; + io_uring_queue_init(1, &ring, 0); + unsigned int values[] = {0, 0}; + return io_uring_register_iowq_max_workers(&ring, values); +} diff --git a/build/has/pragma-nounroll.c b/build/has/pragma-nounroll.c new file mode 100644 index 0000000..2bdae14 --- /dev/null +++ b/build/has/pragma-nounroll.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/// -Werror + +int main(void) { +#pragma nounroll + for (int i = 0; i < 100; ++i); + return 0; +} diff --git a/build/has/pthread-set-name-np.c b/build/has/pthread-set-name-np.c new file mode 100644 index 0000000..324aab9 --- /dev/null +++ b/build/has/pthread-set-name-np.c @@ -0,0 +1,10 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <pthread.h> +#include <pthread_np.h> + +int main(void) { + pthread_set_name_np(pthread_self(), "name"); + return 0; +} diff --git a/build/has/pthread-setname-np.c b/build/has/pthread-setname-np.c new file mode 100644 index 0000000..a3b94c1 --- /dev/null +++ b/build/has/pthread-setname-np.c @@ -0,0 +1,8 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <pthread.h> + +int main(void) { + return pthread_setname_np(pthread_self(), "name"); +} diff --git a/build/has/sched-getaffinity.c b/build/has/sched-getaffinity.c new file mode 100644 index 0000000..6f8fd98 --- /dev/null +++ b/build/has/sched-getaffinity.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <sched.h> + +int main(void) { + cpu_set_t set; + return sched_getaffinity(0, sizeof(set), &set); +} diff --git a/build/has/tcgetwinsize.c b/build/has/tcgetwinsize.c new file mode 100644 index 0000000..d25d12b --- /dev/null +++ b/build/has/tcgetwinsize.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <termios.h> + +int main(void) { + struct winsize ws; + return tcgetwinsize(0, &ws); +} diff --git a/build/has/tcsetwinsize.c b/build/has/tcsetwinsize.c new file mode 100644 index 0000000..6717415 --- /dev/null +++ b/build/has/tcsetwinsize.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <termios.h> + +int main(void) { + const struct winsize ws = {0}; + return tcsetwinsize(0, &ws); +} diff --git a/build/has/timer-create.c b/build/has/timer-create.c new file mode 100644 index 0000000..d5354c3 --- /dev/null +++ b/build/has/timer-create.c @@ -0,0 +1,9 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include <time.h> + +int main(void) { + timer_t timer; + return timer_create(CLOCK_REALTIME, NULL, &timer); +} diff --git a/build/header.mk b/build/header.mk index 09454c5..f15829a 100644 --- a/build/header.mk +++ b/build/header.mk @@ -4,19 +4,23 @@ # Makefile that generates gen/config.h include build/prelude.mk -include gen/config.mk +include gen/vars.mk +include gen/flags.mk +include gen/pkgs.mk include build/exports.mk # All header fragments we generate HEADERS := \ gen/has/--st-birthtim.h \ + gen/has/_Fork.h \ gen/has/acl-get-entry.h \ gen/has/acl-get-file.h \ gen/has/acl-get-tag-type.h \ gen/has/acl-is-trivial-np.h \ gen/has/acl-trivial.h \ - gen/has/aligned-alloc.h \ + gen/has/builtin-riscv-pause.h \ gen/has/confstr.h \ + gen/has/dprintf.h \ gen/has/extattr-get-file.h \ gen/has/extattr-get-link.h \ gen/has/extattr-list-file.h \ @@ -30,10 +34,15 @@ HEADERS := \ gen/has/getmntinfo.h \ gen/has/getprogname-gnu.h \ gen/has/getprogname.h \ + gen/has/io-uring-max-workers.h \ gen/has/pipe2.h \ + gen/has/pragma-nounroll.h \ gen/has/posix-getdents.h \ gen/has/posix-spawn-addfchdir-np.h \ gen/has/posix-spawn-addfchdir.h \ + gen/has/pthread-set-name-np.h \ + gen/has/pthread-setname-np.h \ + gen/has/sched-getaffinity.h \ gen/has/st-acmtim.h \ gen/has/st-acmtimespec.h \ gen/has/st-birthtim.h \ @@ -46,7 +55,10 @@ HEADERS := \ gen/has/strerror-r-posix.h \ gen/has/string-to-flags.h \ gen/has/strtofflags.h \ + gen/has/tcgetwinsize.h \ + gen/has/tcsetwinsize.h \ gen/has/timegm.h \ + gen/has/timer-create.h \ gen/has/tm-gmtoff.h \ gen/has/uselocale.h @@ -58,16 +70,24 @@ gen/config.h: ${PKG_HEADERS} ${HEADERS} @printf '// %s\n' "$@" >$@ @printf '#ifndef BFS_CONFIG_H\n' >>$@ @printf '#define BFS_CONFIG_H\n' >>$@ - @cat ${.ALLSRC} >>$@ + @cat $^ >>$@ @printf '#endif // BFS_CONFIG_H\n' >>$@ - @cat ${.ALLSRC:%=%.log} >gen/config.log + @cat gen/flags.log ${^:%=%.log} >gen/config.log ${VCAT} $@ + @printf '%s' "$$CONFFLAGS" | build/embed.sh >gen/confflags.i + @printf '%s' "$$XCC" | build/embed.sh >gen/cc.i + @printf '%s' "$$XCPPFLAGS" | build/embed.sh >gen/cppflags.i + @printf '%s' "$$XCFLAGS" | build/embed.sh >gen/cflags.i + @printf '%s' "$$XLDFLAGS" | build/embed.sh >gen/ldflags.i + @printf '%s' "$$XLDLIBS" | build/embed.sh >gen/ldlibs.i .PHONY: gen/config.h # The short name of the config test SLUG = ${@:gen/%.h=%} +# The hidden output file name +OUT = ${SLUG:has/%=gen/has/.%.out} ${HEADERS}:: @${MKDIR} ${@D} - @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c >$@ 2>$@.log; \ + @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0 diff --git a/build/msg.sh b/build/msg.sh index a7da31b..2249125 100755 --- a/build/msg.sh +++ b/build/msg.sh @@ -59,4 +59,4 @@ if is_loud; then printf '%s\n' "$*" fi -"$@" +exec "$@" diff --git a/build/pkgconf.sh b/build/pkgconf.sh index 244c95d..decf706 100755 --- a/build/pkgconf.sh +++ b/build/pkgconf.sh @@ -34,10 +34,10 @@ if [ -z "$MODE" ]; then 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/with/$LIB.c $LDLIBS || exit 1 + XCFLAGS="$XCFLAGS $("$0" --cflags "$LIB")" || exit 1 + XLDFLAGS="$XLDFLAGS $("$0" --ldflags "$LIB")" || exit 1 + XLDLIBS="$("$0" --ldlibs "$LIB") $XLDLIBS" || exit 1 + build/cc.sh "build/with/$LIB.c" -o "gen/with/.$LIB.out" || exit 1 done fi @@ -92,5 +92,5 @@ done case "$MODE" in --ldlibs) printf '%s\n' "$LDLIBS" - ;; + ;; esac diff --git a/build/pkgs.mk b/build/pkgs.mk index 5de9ac2..f692739 100644 --- a/build/pkgs.mk +++ b/build/pkgs.mk @@ -19,7 +19,7 @@ gen/pkgs.mk: ${HEADERS} 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|') >>$@ + gen $$(grep -l ' true$$' $^ | sed 's|.*/\(.*\)\.h|\1|') >>$@ ${VCAT} $@ .PHONY: gen/pkgs.mk diff --git a/build/prelude.mk b/build/prelude.mk index d0968ea..6250d73 100644 --- a/build/prelude.mk +++ b/build/prelude.mk @@ -9,11 +9,9 @@ # 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 ?= $^ +# GNU make has $^ for the full list of targets, while BSD make has $> (and the +# long-form ${.ALLSRC}). We use the GNU version, bringing it to BSD like this: +^ ?= $> # Installation paths DESTDIR ?= @@ -37,7 +35,7 @@ RM ?= rm -f # 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] +# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [empty] # # Inspired by https://github.com/wahern/autoguess TRUTHY,y := y @@ -68,57 +66,3 @@ ALL_PKGS := \ 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/sighook.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/sighook.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/version.sh b/build/version.sh new file mode 100755 index 0000000..ec0663a --- /dev/null +++ b/build/version.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Copyright © Tavian Barnes <tavianator@tavianator.com> +# SPDX-License-Identifier: 0BSD + +# Print the version number + +set -eu + +DIR="$(dirname -- "$0")/.." + +if [ "${VERSION-}" ]; then + printf '%s' "$VERSION" +elif [ -e "$DIR/.git" ] && command -v git >/dev/null 2>&1; then + git -C "$DIR" describe --always --dirty +else + echo "4.0.8" +fi diff --git a/completions/bfs.bash b/completions/bfs.bash index 6fd82c8..0dd39f4 100644 --- a/completions/bfs.bash +++ b/completions/bfs.bash @@ -99,6 +99,7 @@ _bfs() { -ignore_readdir_race -mount -nocolor + -noerror -noignore_readdir_race -noleaf -nowarn diff --git a/completions/bfs.fish b/completions/bfs.fish index 24b0ad9..7182bee 100644 --- a/completions/bfs.fish +++ b/completions/bfs.fish @@ -42,7 +42,8 @@ complete -c bfs -o ignore_readdir_race -d "Don't report an error if the file tre complete -c bfs -o noignore_readdir_race -d "Report an error if the file tree is modified during the search" complete -c bfs -o maxdepth -d "Ignore files deeper than specified number" -x complete -c bfs -o mindepth -d "Ignore files shallower than specified number" -x -complete -c bfs -o mount -d "Don't descend into other mount points" +complete -c bfs -o mount -d "Exclude mount points" +complete -c bfs -o noerror -d "Ignore any errors that occur during traversal" complete -c bfs -o nohidden -d "Exclude hidden files and directories" complete -c bfs -o noleaf -d "Ignored; for compatibility with GNU find" complete -c bfs -o regextype -d "Use specified flavored regex" -a $regex_type_comp -x diff --git a/completions/bfs.zsh b/completions/bfs.zsh index 432ab8c..6b46f83 100644 --- a/completions/bfs.zsh +++ b/completions/bfs.zsh @@ -41,7 +41,8 @@ args=( '*-noignore_readdir_race[do not report an error if bfs detects file tree is modified during search]' '*-maxdepth[ignore files deeper than N]:maximum search depth' '*-mindepth[ignore files shallower than N]:minimum search depth' - "*-mount[don't descend into other mount points]" + "*-mount[exclude mount points]" + '*-noerror[ignore any errors that occur during traversal]' '*-nohidden[exclude hidden files]' '*-noleaf[ignored, for compatibility with GNU find]' '-regextype[type of regex to use, default posix-basic]:regexp syntax:(help posix-basic posix-extended ed emacs grep sed)' @@ -90,11 +91,11 @@ args=( '*-user[find files owned by user NAME]:user:_users' '*-hidden[find hidden files (those beginning with .)]' - '*-ilname[find symbolic links whose target matches GLOB (case insensitve)]:link pattern to search (case insensitive):' + '*-ilname[find symbolic links whose target matches GLOB (case insensitive)]:link pattern to search (case insensitive):' '*-iname[find files whose name matches GLOB (case insensitive)]:name pattern to match (case insensitive):' '*-inum[find files with inode number N]:inode number:' - '*-ipath[find files whose entire path matches GLOB (case insenstive)]:path pattern to search (case insensitive):' - '*-iregex[find files whose entire path matches REGEX (case insenstive)]:regular expression to search (case insensitive):' + '*-ipath[find files whose entire path matches GLOB (case insensitive)]:path pattern to search (case insensitive):' + '*-iregex[find files whose entire path matches REGEX (case insensitive)]:regular expression to search (case insensitive):' '*-iwholename[find files whose entire path matches GLOB (case insensitive)]:full path pattern to search (case insensitive):' '*-links[find files with N hard links]:number of links:' @@ -7,29 +7,110 @@ set -eu -# Default to `make` -MAKE="${MAKE:-make}" - -# Pass -j$(nproc) unless MAKEFLAGS is set -if [ "${MAKEFLAGS+y}" ]; then - j="" -else - j="-j$({ nproc || sysctl -n hw.ncpu || getconf _NPROCESSORS_ONLN || echo 1; } 2>/dev/null)" -fi - -# Convert kebab-case to UPPER_CASE -toupper() { - printf '%s' "$1" | tr 'a-z-' 'A-Z_' +# Get the relative path to the source tree based on how the script was run +DIR=$(dirname -- "$0") + +# Print the help message +help() { + cat <<EOF +Usage: + + \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...] + \$ $MAKE -j$(_nproc) + +Variables set in the environment or on the command line will be picked up: + + MAKE + The make implementation to use + CC + The C compiler to use + + CPPFLAGS="-I... -D..." + CFLAGS="-W... -f..." + LDFLAGS="-L... -Wl,..." + Preprocessor/compiler/linker flags + + LDLIBS="-l... -l..." + Dynamic libraries to link + + EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS} + Adds to the default flags, instead of replacing them + +The default flags result in a plain debug build. Other build profiles include: + + --enable-release + Enable optimizations, disable assertions + --enable-{asan,lsan,msan,tsan,ubsan} + Enable sanitizers + --enable-gcov + Enable code coverage instrumentation + +External dependencies are auto-detected by default, but you can build --with or +--without them explicitly: + + --with-libacl --without-libacl + --with-libcap --without-libcap + --with-libselinux --without-libselinux + --with-liburing --without-liburing + --with-oniguruma --without-oniguruma + +Packaging: + + --prefix=/path + Set the installation prefix (default: /usr) + --mandir=/path + Set the man page directory (default: \$PREFIX/share/man) + --version=X.Y.Z + Set the version string (default: $("$DIR/build/version.sh")) + +This script is a thin wrapper around a makefile-based configuration system. +Any other arguments will be passed directly to the $MAKE invocation, e.g. + + \$ $0 -j$(_nproc) V=1 +EOF +} + +# Report a warning +warn() { + fmt="$1" + shift + printf "%s: warning: $fmt\\n" "$0" "$@" >&2 } # Report an argument parsing error invalid() { - printf 'error: Unrecognized option "%s"\n\n' "$1" >&2 + printf '%s: error: Unrecognized option "%s"\n\n' "$0" "$1" >&2 printf 'Run %s --help for more information.\n' "$0" >&2 exit 1 } +# Get the number of cores to use +_nproc() { + { + nproc \ + || sysctl -n hw.ncpu \ + || getconf _NPROCESSORS_ONLN \ + || echo 1 + } 2>/dev/null +} + +# Save the ./configure command line for bfs --version +export CONFFLAGS="" + +# Default to `make` +MAKE="${MAKE-make}" + +# Parse the command-line arguments for arg; do + shift + + # Only add --options to CONFFLAGS, so we don't print FLAG=values twice in bfs --version + case "$arg" in + -*) + CONFFLAGS="${CONFFLAGS}${CONFFLAGS:+ }${arg}" + ;; + esac + # --[(enable|disable|with|without)-]$name[=$value] value="${arg#*=}" name="${arg%%=*}" @@ -69,75 +150,21 @@ for arg; do --enable-*) arg="--with-${arg#--*-}" ;; --disable-*) arg="--without-${arg#--*-}" ;; esac - printf 'warning: Treating "%s" like "%s"\n' "$old" "$arg" >&2 + warn 'Treating "%s" like "%s"' "$old" "$arg" ;; esac - ;; + ;; esac case "$arg" in -h|--help) - cat <<EOF -Usage: - - \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...] - \$ $MAKE $j - -Variables set in the environment or on the command line will be picked up: - - MAKE - The make implementation to use - CC - The C compiler to use - - CPPFLAGS="-I... -D..." - CFLAGS="-W... -f..." - LDFLAGS="-L... -Wl,..." - Preprocessor/compiler/linker flags - - LDLIBS="-l... -l..." - Dynamic libraries to link - - EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS} - Adds to the default flags, instead of replacing them - -The default flags result in a plain debug build. Other build profiles include: - - --enable-release - Enable optimizations, disable assertions - --enable-{asan,lsan,msan,tsan,ubsan} - Enable sanitizers - --enable-gcov - Enable code coverage instrumentation - -External dependencies are auto-detected by default, but you can build --with or ---without them explicitly: - - --with-libacl --without-libacl - --with-libcap --without-libcap - --with-libselinux --without-libselinux - --with-liburing --without-liburing - --with-oniguruma --without-oniguruma - -Packaging: - - --prefix=/path - Set the installation prefix (default: /usr) - --mandir=/path - Set the man page directory (default: \$PREFIX/share/man) - -This script is a thin wrapper around a makefile-based configuration system. -Any other arguments will be passed directly to the $MAKE invocation, e.g. - - \$ $0 $j V=1 -EOF + help exit 0 ;; --enable-*|--disable-*) case "$name" in - release|asan|lsan|msan|tsan|ubsan|lint|gcov) - shift + release|lto|asan|lsan|msan|tsan|ubsan|lint|gcov) set -- "$@" "$NAME=$yn" ;; *) @@ -149,7 +176,6 @@ EOF --with-*|--without-*) case "$name" in libacl|libcap|libselinux|liburing|oniguruma) - shift set -- "$@" "WITH_$NAME=$yn" ;; *) @@ -158,24 +184,40 @@ EOF esac ;; - --prefix=*|--mandir=*) - shift + --prefix=*|--mandir=*|--version=*) set -- "$@" "$NAME=$value" ;; --infodir=*|--build=*|--host=*|--target=*) - shift - printf 'warning: Ignoring option "%s"\n' "$arg" >&2 + warn 'Ignoring option "%s"' "$arg" ;; MAKE=*) - shift MAKE="$value" ;; + # Warn about MAKE variables that have documented configure flags + RELEASE=*|LTO=*|ASAN=*|LSAN=*|MSAN=*|TSAN=*|UBSAN=*|LINT=*|GCOV=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --enable-%s' "$arg" "$name" + set -- "$@" "$arg" + ;; + + PREFIX=*|MANDIR=*|VERSION=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --%s=%s' "$arg" "$name" "$value" + set -- "$@" "$arg" + ;; + + WITH_*=*) + name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-') + warn '"%s" is deprecated; use --%s' "$arg" "$name" + set -- "$@" "$arg" + ;; + # make flag (-j2) or variable (CC=clang) -*|*=*) - continue + set -- "$@" "$arg" ;; *) @@ -184,15 +226,12 @@ EOF 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 +for f in Makefile bench build completions docs src tests; do test -e "$f" || ln -s "$DIR/$f" "$f" done -# Set MAKEFLAGS to -j$(nproc) if it's unset -export MAKEFLAGS="${MAKEFLAGS-$j}" +# Set MAKEFLAGS to -j$(_nproc) if it's unset +export MAKEFLAGS="${MAKEFLAGS--j$(_nproc)}" $MAKE -rf build/config.mk "$@" diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 025dadf..69a997c 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -93,7 +93,7 @@ External dependencies are auto-detected by default, but you can build `--with` o </pre> [`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=""`). +If this is undesirable, disable it by setting `PKG_CONFIG` to the empty string (`./configure PKG_CONFIG=""`). [`pkg-config`]: https://www.freedesktop.org/wiki/Software/pkg-config/ @@ -178,11 +178,11 @@ It can be handy to generate the snapshot with a different `find` implementation But keep in mind, other `find` implementations may not be correct. To my knowledge, no other implementation passes even the POSIX-compatible subset of the tests: - $ ./tests/tests.sh --bfs=find --posix + $ ./tests/tests.sh --bfs=find --sudo --posix ... - tests passed: 90 - tests skipped: 3 - tests failed: 6 + [PASS] 104 / 119 + [SKIP] 1 / 119 + [FAIL] 14 / 119 Run diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 62b6480..56f53b4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,265 @@ +4.* +=== + +4.0.8 +----- + +**June 20, 2025** + +### Bug fixes + +- Fixed an invalid optimization that transformed + + $ bfs -user you -or -user me + + into just + + $ bfs -user you + + The bug was originally introduced in bfs 2.0 (October 14, 2020). + ([#155](https://github.com/tavianator/bfs/issues/155)) + + +4.0.7 +----- + +**June 15, 2025** + +### Changes + +- `bfs` now takes CPU affinity into account when picking how many threads to use + ([`a36774b`](https://github.com/tavianator/bfs/commit/a36774be636c3429c6e73de33bf65a1bdbdcfb4b)) + +- `-execdir /bin/...` is now allowed even with a relative path in `$PATH` + ([`cb40f51`](https://github.com/tavianator/bfs/commit/cb40f51e4e6375a10265484b6959c6b1b0591378)) + +- *Expect* is no longer a test suite dependency + ([`7102fec`](https://github.com/tavianator/bfs/commit/7102fec257835302cb4978160bba4cbebd0b63e1)) + +### Bug fixes + +- Only the last `-files0-from` argument now has any effect, to match GNU find + ([`a662fda`](https://github.com/tavianator/bfs/commit/a662fda2642e17478bc8e78adb4c6642a8505cdb)) + +- Fixed `-execdir {}`, which was inadvertently broken in bfs 4.0 + ([`def4a83`](https://github.com/tavianator/bfs/commit/def4a832425bfe94b96b8cb1146a83552b754fb4)) + + +4.0.6 +----- + +**February 26, 2025** + +### Bug fixes + +- Fixed `-fstype` with btrfs subvolumes (requires Linux 5.8+) + ([`0dccdae`](https://github.com/tavianator/bfs/commit/0dccdae4510ff5603247be871e64a6119647ea2a)) + +- Fixed `-ls` with timestamps very far in the future + ([`dd5df1f`](https://github.com/tavianator/bfs/commit/dd5df1f8997550c5bf49205578027715b957bd01)) + +- Fixed the `posix/exec_sigmask` test on mips64el Linux + ([`532dec0`](https://github.com/tavianator/bfs/commit/532dec0849dcdc3e15e530ac40a8168f146a41cd)) + +- Fixed time-related tests with `mawk 1.3.4 20250131` + ([#152](https://github.com/tavianator/bfs/issues/152)) + + +4.0.5 +----- + +**January 18, 2025** + +### Bug fixes + +- Fixed a bug that could cause child processes (e.g. from `-exec`) to run with all signals blocked. + The bug was introduced in version 3.3. + ([`af207e7`](https://github.com/tavianator/bfs/commit/af207e702148e5c9ae08047d7a2dce6394653b62)) + +### Changes + +- Fixed the build against old liburing versions + ([#147](https://github.com/tavianator/bfs/issues/147)) + +- Async I/O performance optimizations + + +4.0.4 +----- + +**October 31, 2024** + +## Bug fixes + +- Fixed a man page typo + ([#144](https://github.com/tavianator/bfs/pull/144)) + +- Fixed the build on PowerPC macOS + ([#145](https://github.com/tavianator/bfs/issues/145)) + +- Fixed a bug introduced in bfs 4.0.3 that colorized every file as if it had capabilities on non-Linux systems + ([#146](https://github.com/tavianator/bfs/pull/146)) + + +4.0.3 +----- + +**October 22, 2024** + +### Bug fixes + +- Fixed an assertion failure when `$LS_COLORS` contained escaped NUL bytes like `*\0.gz=` + ([`f5eaadb9`](https://github.com/tavianator/bfs/commit/f5eaadb96fb94b2d3666e53a99495840a3099aec)) + +- Fixed a use-after-free bug introduced in bfs 4.0 when unregistering and re-registering signal hooks. + This could be reproduced with `bfs -nocolor` by repeatedly sending `SIGINFO`/`SIGUSR1` to toggle the status bar. + ([`39ff273`](https://github.com/tavianator/bfs/commit/39ff273df97e51b1285358b9e6808b117ea8adb1)) + +- Fixed a hang present since bfs 3.0 colorizing paths like `notdir/file`, where `notdir` is a symlink pointing to a non-directory file. + ([`b89f22cb`](https://github.com/tavianator/bfs/commit/b89f22cbf250958a802915eb7b6bf0e5f38376ca)) + + +4.0.2 +----- + +**September 17, 2024** + +### New features + +- Implemented `./configure --version=X.Y.Z`, mainly for packagers to override the version number + ([`4a278d3`](https://github.com/tavianator/bfs/commit/4a278d3e39a685379711727eac7bfaa83679e0e4)) + +### Changes + +- Minor refactoring of the build system + +### Bug fixes + +- Fixed `./configure --help`, which was broken since `bfs` 4.0 + ([`07ae989`](https://github.com/tavianator/bfs/commit/07ae98906dbb0caaac2f758d72e88dd0975b2a81)) + +- Fixed compiler flag auto-detection on systems with non-GNU `sed`. + This fixes a potential race condition on FreeBSD since `bfs` 4.0 due to the [switch to `_Fork()`](https://github.com/tavianator/bfs/commit/085bb402c7b2c2f96624fb0523ff3f9686fe26d9) without passing `-z now` to the linker. + ([`34e6081`](https://github.com/tavianator/bfs/commit/34e60816adb0ea8ddb155a454676a99ab225dc8a)) + +- Fixed `$MAKE distcheck` when `$MAKE` is not `make`, e.g. `gmake distcheck` on BSD + ([`2135b00`](https://github.com/tavianator/bfs/commit/2135b00d215efc5c2c38e1abd3254baf31229ad4)) + +- Fixed some roff syntax issues in the `bfs` manpage + ([`812ecd1`](https://github.com/tavianator/bfs/commit/812ecd1feeb002252dd4d732b395d31c4179afaf)) + +- Fixed an assertion failure optimizing expressions like `bfs -not \( -prune , -type f \)` since `bfs` 3.1. + Release builds were not affected, since their assertions are disabled and the behaviour was otherwise correct. + ([`b1a9998`](https://github.com/tavianator/bfs/commit/b1a999892b9e13181ddd9a7d895f3d1c65fbb449)) + + +4.0.1 +----- + +**August 19, 2024** + +### Bug fixes + +- `bfs` no longer prints a "suppressed errors" warning unless `-noerror` is actually suppressing errors + ([`5d03c9d`](https://github.com/tavianator/bfs/commit/5d03c9d460d1c1afcdf062d494537986ce96a690)) + + +4.0 +--- + +**August 16, 2024** + +### New features + +- To match BSD `find` (and the POSIX Utility Syntax Guidelines), multiple flags can now be given in a single argument like `-LEXO2`. + Previously, you would have had to write `-L -E -X -O2`. + ([`c0fd33a`](https://github.com/tavianator/bfs/commit/c0fd33aaef5f345566a41c7c2558f27adf05558b)) + +- Explicit timestamps can now be written as `@SECONDS_SINCE_EPOCH`. + For example, `bfs -newermt @946684800` will print files modified since January 1, 2000 (UTC). + ([`c6bb003`](https://github.com/tavianator/bfs/commit/c6bb003b8882e9a16941f5803d072ec1cb728318)) + +- The new `-noerror` option suppresses all error messages during traversal. + ([#142](https://github.com/tavianator/bfs/issues/142)) + +### Changes + +- `-mount` now excludes mount points entirely, to comply with the recently published POSIX 2024 standard. + Use `-xdev` to include the mount point itself, but not its contents. + `bfs` has been warning about this change since version 1.5.1 (September 2019). + ([`33b85e1`](https://github.com/tavianator/bfs/commit/33b85e1f8769e7f75721887638ae454d109a034f)) + +- `-perm` now takes the current file creation mask into account when parsing a symbolic mode like `+rw`, as clarified by [POSIX defect 1392](https://www.austingroupbugs.net/view.php?id=1392). + This matches the behaviour of BSD `find`, contrary to the behaviour of GNU `find`. + ([`6290ce4`](https://github.com/tavianator/bfs/commit/6290ce41f3ec1f889abb881cf90ca91da869b5b2)) + +### Bug fixes + +- Fixed commands like `./configure CC=clang --enable-release` that set variables before other options + ([`49a5d48`](https://github.com/tavianator/bfs/commit/49a5d48d0a43bac313c8b8d1b167e60da9eaadf6)) + +- Fixed the build on RISC-V with GCC versions older than 14 + ([`e93a1dc`](https://github.com/tavianator/bfs/commit/e93a1dccd82f831a2f0d2cc382d8af5e1fda55ed)) + +- Fixed running `bfs` under Valgrind + ([`a01cfac`](https://github.com/tavianator/bfs/commit/a01cfacd423af28af6b7c13ba51e2395f3a52ee7)) + +- Fixed the exit code when failing to execute a non-existent command with `-exec`/`-ok` on some platforms including OpenBSD and HPPA + ([`8c130ca`](https://github.com/tavianator/bfs/commit/8c130ca0117fd225c24569be2ec16c7dc2150a13)) + +- Fixed `$LS_COLORS` case-sensitivity to match GNU ls more closely when the same extension is specified multiple times + ([`08030ae`](https://github.com/tavianator/bfs/commit/08030aea919039165c02805e8c637a9ec1ad0d70)) + +- Fixed the `-status` bar on Solaris/Illumos + + 3.* === +3.3.1 +----- + +**June 3, 2024** + +### Bug fixes + +- Reduced the scope of the symbolic link loop change in version 3.3. + `-xtype l` remains true for symbolic link loops, matching a change in GNU findutils 4.10.0. + However, `-L` will report an error, just like `bfs` prior to 3.3 and other `find` implementations, as required by POSIX. + + +3.3 +--- + +**May 28, 2024** + +### New features + +- The `-status` bar can now be toggled by `SIGINFO` (<kbd>Ctrl</kbd>+<kbd>T</kbd>) on systems that support it, and `SIGUSR1` on other systems + +- `-regextype` now supports all regex types from GNU find ([#21](https://github.com/tavianator/bfs/issues/21)) + +- File birth times are now supported on OpenBSD + +### Changes + +- Symbolic link loops are now treated like other broken links, rather than an error + +- `./configure` now expects `--with-libacl`, `--without-libcap`, etc. rather than `--enable-`/`--disable-` + +- The ` ` (space) flag is now restricted to numeric `-printf` specifiers + +### Bug fixes + +- `-regextype emacs` now supports [shy](https://www.gnu.org/software/emacs/manual/html_node/elisp/Regexp-Backslash.html#index-shy-groups) (non-capturing) groups + +- Fixed `-status` bar visual corruption when the terminal is resized + +- `bfs` now prints a reset escape sequence when terminated by a signal in the middle of colored output ([#138](https://github.com/tavianator/bfs/issues/138)) + +- `./configure CFLAGS=...` no longer overrides flags from `pkg-config` during configuration + + 3.2 --- @@ -183,7 +442,7 @@ - Breadth-first search could become highly unbalanced, negating many of the benefits of `bfs` - - On non-{Linux,FreeBSD} plaforms, directories could stay open longer than necessary, consuming extra memory + - On non-{Linux,FreeBSD} platforms, directories could stay open longer than necessary, consuming extra memory [#107]: https://github.com/tavianator/bfs/pull/107 diff --git a/docs/RELATED.md b/docs/RELATED.md index cf52b70..6e7bd38 100644 --- a/docs/RELATED.md +++ b/docs/RELATED.md @@ -25,8 +25,9 @@ These are not usually installed as the system `find`, but are designed to be `fi - [`bfs`](https://tavianator.com/projects/bfs.html) ([manual](https://man.archlinux.org/man/bfs.1), [source](https://github.com/tavianator/bfs)) - [schilytools](https://codeberg.org/schilytools/schilytools) `sfind` ([source](https://codeberg.org/schilytools/schilytools/src/branch/master/sfind)) - [BusyBox](https://busybox.net/) `find` ([manual](https://busybox.net/downloads/BusyBox.html#find), [source](https://git.busybox.net/busybox/tree/findutils/find.c)) -- [ToyBox](http://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c)) -- uutils `find` ([source](https://github.com/uutils/findutils)) +- [ToyBox](https://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c)) +- [Heirloom Project](https://heirloom.sourceforge.net/) `find` ([manual](https://heirloom.sourceforge.net/man/find.1.html), [source](https://github.com/eunuchs/heirloom-project/blob/master/heirloom/heirloom/find/find.c)) +- [uutils](https://uutils.github.io/) `find` ([source](https://github.com/uutils/findutils)) ## `find` alternatives diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..dd3277a --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,126 @@ +Security +======== + +Threat model +------------ + +`bfs` is a command line program running on multi-user operating systems. +Those other users may be malicious, but `bfs` should not allow them to do anything they couldn't already do. +That includes situations where one user (especially `root`) is running `bfs` on files owned or controlled by another user. + +On the other hand, `bfs` implicitly trusts the user running it. +Anyone with enough control over the command line of `bfs` or any `find`-compatible tool can wreak havoc with dangerous actions like `-exec`, `-delete`, etc. + +> [!CAUTION] +> The only untrusted input that should *ever* be passed on the `bfs` command line are **file paths**. +> It is *always* unsafe to allow *any* other part of the command line to be affected by untrusted input. +> Use the `-f` flag, or `-files0-from`, to ensure that the input is interpreted as a path. + +This still has security implications, including: + +- **Information disclosure:** an attacker may learn whether particular files exist by observing `bfs`'s output, exit status, or even side channels like execution time. +- **Denial of service:** large directory trees or slow/network storage may cause `bfs` to consume excessive system resources. + +> [!TIP] +> When in doubt, do not pass any untrusted input to `bfs`. + + +Executing commands +------------------ + +The `-exec` family of actions execute commands, passing the matched paths as arguments. +File names that begin with a dash may be misinterpreted as options, so `bfs` adds a leading `./` in some instances: + +```console +user@host$ bfs -execdir echo {} \; +./-rf +``` + +This might save you from accidentally running `rm -rf` (for example) when you didn't mean to. +This mitigation applies to `-execdir`, but not `-exec`, because the full path typically does not begin with a dash. +But it is possible, so be careful: + +```console +user@host$ bfs -f -rf -exec echo {} \; +-rf +``` + + +Race conditions +--------------- + +Like many programs that interface with the file system, `bfs` can be affected by race conditions—in particular, "[time-of-check to time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)" (TOCTTOU) issues. +For example, + +```console +user@host$ bfs / -user user -exec dangerous_command {} \; +``` + +is not guaranteed to only run `dangerous_command` on files you own, because another user may run + +```console +evil@host$ mv /path/to/file /path/to/exile +evil@host$ mv ~/malicious /path/to/file +``` + +in between checking `-user user` and executing the command. + +> [!WARNING] +> Be careful when running `bfs` on directories that other users have write access to, because they can modify the directory tree while `bfs` is running, leading to unpredictable results and possible TOCTTOU issues. + + +Output sanitization +------------------- + +In general, printing arbitrary data to a terminal may have [security](https://hdm.io/writing/termulation.txt) [implications](https://dgl.cx/2023/09/ansi-terminal-security#vulnerabilities-using-known-replies). +On many platforms, file paths may be completely arbitrary data (except for NUL (`\0`) bytes). +Therefore, when `bfs` is writing output to a terminal, it will escape non-printable characters: + +<pre> +user@host$ touch $'\e[1mBOLD\e[0m' +user@host$ bfs +. +./$'\e[1mBOLD\e[0m' +</pre> + +However, this is fragile as it only applies when outputting directly to a terminal: + +<pre> +user@host$ bfs | grep BOLD +<strong>BOLD</strong> +</pre> + + +Code quality +------------ + +Every correctness issue in `bfs` is a potential security issue, because acting on the wrong path may do arbitrarily bad things. +For example: + +```console +root@host# bfs /etc -name passwd -exec cat {} \; +``` + +should print `/etc/passwd` but not `/etc/shadow`. +`bfs` tries to ensure correct behavior through careful programming practice, an extensive testsuite, and static analysis. + +`bfs` is written in C, which is a memory unsafe language. +Bugs that lead to memory corruption are likely to be exploitable due to the nature of C. +We use [sanitizers](https://github.com/google/sanitizers) to try to detect these bugs. +Fuzzing has also been applied in the past, and deploying continuous fuzzing is a work in progress. + + +Supported versions +------------------ + +`bfs` comes with [no warranty](/LICENSE), and is maintained by [me](https://tavianator.com/) and [other volunteers](https://github.com/tavianator/bfs/graphs/contributors) in our spare time. +In that sense, there are no *supported* versions. +However, as long as I maintain `bfs` I will attempt to address any security issues swiftly. +In general, security fixes will be part of the latest release, though for significant issues I may backport fixes to older release series. + + +Reporting a vulnerability +------------------------- + +If you think you have found a sensitive security issue in `bfs`, you can [report it privately](https://github.com/tavianator/bfs/security/advisories/new). +Or you can [report it publicly](https://github.com/tavianator/bfs/issues/new); I won't judge you. diff --git a/docs/USAGE.md b/docs/USAGE.md index 70f8475..16aeaf6 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -105,7 +105,7 @@ For expressions like `-name`, that's all they do. But some expressions, called *actions*, have other side effects. If no actions are included in the expression, `bfs` adds the `-print` action automatically, which is why the above examples actually print any output. -The default `-print` is supressed if any actions are given explicitly. +The default `-print` is suppressed if any actions are given explicitly. Available actions include printing with alternate formats (`-ls`, `-printf`, etc.), executing commands (`-exec`, `-execdir`, etc.), deleting files (`-delete`), and stopping the search (`-quit`, `-exit`). @@ -1,4 +1,6 @@ -.TH BFS 1 +.\" Copyright © Tavian Barnes <tavianator@tavianator.com> +.\" SPDX-License-Identifier: 0BSD +.TH BFS 1 2025-06-15 "bfs 4.0.8" .SH NAME bfs \- breadth-first search for your files .SH SYNOPSIS @@ -41,17 +43,17 @@ For example, .PP .nf .RS -.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-and \-print +.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-and \-print .RE .fi .PP -will print the all the paths that are either .txt files or symbolic links to .txt files. +will print all the paths that are either .txt files or symbolic links to .txt files. .B \-and is implied between two consecutive expressions, so this is equivalent: .PP .nf .RS -.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-print +.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-print .RE .fi .PP @@ -71,7 +73,7 @@ will also accept .I \-N or .IR +N . -.IR \-N +.I \-N means "less than .IR N ," and @@ -90,7 +92,9 @@ Follow all symbolic links. Never follow symbolic links (the default). .TP .B \-E -Use extended regular expressions (same as \fB\-regextype \fIposix-extended\fR). +Use extended regular expressions (same as +.B \-regextype +.IR posix-extended ). .TP .B \-X Filter out files with @@ -109,20 +113,20 @@ The sorting takes place within each directory separately, which makes it differe but still provides a deterministic ordering. .TP .B \-x -Don't descend into other mount points (same as \fB\-xdev\fR). +Don't descend into other mount points (same as +.BR \-xdev ). .TP -\fB\-f \fIPATH\fR +.BI "\-f " PATH Treat .I PATH as a path to search (useful if it begins with a dash). -.PP .TP -\fB\-D \fIFLAG\fR +.BI "\-D " FLAG Turn on a debugging flag (see .B \-D .IR help ). .PP -\fB\-O\fIN\fR +.BI \-O N .RS Enable optimization level .I N @@ -178,35 +182,42 @@ Typically far faster than .IR ids . .RE .TP -\fB\-j\fIN\fR +.BI \-j N Search with .I N threads in parallel (default: number of CPUs, up to .IR 8 ). .SH OPERATORS .TP -\fB( \fIexpression \fB)\fR +.BI "( " expression " )" Parentheses are used for grouping expressions together. You'll probably have to write -.B \\\\( +.B \e( .I expression -.B \\\\) +.B \e) to avoid the parentheses being interpreted by the shell. .PP \fB! \fIexpression\fR .br -\fB\-not \fIexpression\fR +.B \-not +.I expression .RS The "not" operator: returns the negation of the truth value of the .IR expression . -You may have to write \fB\\! \fIexpression\fR to avoid \fB!\fR being interpreted by the shell. +You may have to write \fB\e! \fIexpression\fR to avoid +.B ! +being interpreted by the shell. .RE .PP -\fIexpression\fR \fIexpression\fR +.I expression expression .br -\fIexpression \fB\-a \fIexpression\fR +.I expression +.B \-a +.I expression .br -\fIexpression \fB\-and \fIexpression\fR +.I expression +.B \-and +.I expression .RS Short-circuiting "and" operator: if the left-hand .I expression @@ -218,9 +229,13 @@ otherwise, returns .BR false . .RE .PP -\fIexpression \fB\-o \fIexpression\fR +.I expression +.B \-o +.I expression .br -\fIexpression \fB\-or \fIexpression\fR +.I expression +.B \-or +.I expression .RS Short-circuiting "or" operator: if the left-hand .I expression @@ -232,14 +247,14 @@ otherwise, returns .BR true . .RE .TP -\fIexpression \fB, \fIexpression\fR +.IB "expression " , " expression" The "comma" operator: evaluates the left-hand .I expression but discards the result, returning the right-hand .IR expression . .SH SPECIAL FORMS .TP -\fB\-exclude \fIexpression\fR +.BI "\-exclude " expression Exclude all paths matching the .I expression from the search. @@ -266,7 +281,6 @@ Print usage information, and exit immediately (without parsing the rest of the c Print version information, and exit immediately. .RE .SH OPTIONS -.PP .B \-color .br .B \-nocolor @@ -288,8 +302,8 @@ Search in post-order (descendents first). Follow all symbolic links (same as .BR \-L ). .TP -\fB\-files0\-from \fIFILE\fR -Treat the NUL ('\\0')-separated paths in +.BI "\-files0\-from " FILE +Treat the NUL ('\e0')-separated paths in .I FILE as starting points for the search. Pass @@ -297,9 +311,9 @@ Pass .I \- to read the paths from standard input. .PP -\fB\-ignore_readdir_race\fR +.B \-ignore_readdir_race .br -\fB\-noignore_readdir_race\fR +.B \-noignore_readdir_race .RS Whether to report an error if .B bfs @@ -307,18 +321,21 @@ detects that the file tree is modified during the search (default: .BR \-noignore_readdir_race ). .RE .PP -\fB\-maxdepth \fIN\fR +.B \-maxdepth +.I N .br -\fB\-mindepth \fIN\fR +.B \-mindepth +.I N .RS Ignore files deeper/shallower than .IR N . .RE .TP .B \-mount -Don't descend into other mount points (same as -.B \-xdev -for now, but will skip mount points entirely in the future). +Exclude mount points entirely from the results. +.TP +.B \-noerror +Ignore any errors that occur during traversal. .TP .B \-nohidden Exclude hidden files and directories. @@ -326,7 +343,7 @@ Exclude hidden files and directories. .B \-noleaf Ignored; for compatibility with GNU find. .TP -\fB\-regextype \fITYPE\fR +.BI "\-regextype " TYPE Use .IR TYPE -flavored regular expressions. @@ -337,7 +354,7 @@ The possible types are POSIX basic regular expressions (the default). .TP .I posix-extended -POSIX extended resular expressions. +POSIX extended regular expressions. .TP .I ed Like @@ -381,6 +398,9 @@ Turn on or off warnings about the command line. .TP .B \-xdev Don't descend into other mount points. +Unlike +.BR \-mount , +the mount point itself is still included. .SH TESTS .TP .B \-acl @@ -401,13 +421,17 @@ Find files minutes ago. .RE .PP -\fB\-anewer \fIFILE\fR +.B \-anewer +.I FILE .br -\fB\-Bnewer \fIFILE\fR +.B \-Bnewer +.I FILE .br -\fB\-cnewer \fIFILE\fR +.B \-cnewer +.I FILE .br -\fB\-mnewer \fIFILE\fR +.B \-mnewer +.I FILE .RS Find files .BR a ccessed/ B irthed/ c hanged/ m odified @@ -416,13 +440,17 @@ more recently than was modified. .RE .PP -\fB\-asince \fITIME\fR +.B \-asince +.I TIME .br -\fB\-Bsince \fITIME\fR +.B \-Bsince +.I TIME .br -\fB\-csince \fITIME\fR +.B \-csince +.I TIME .br -\fB\-msince \fITIME\fR +.B \-msince +.I TIME .RS Find files .BR a ccessed/ B irthed/ c hanged/ m odified @@ -452,7 +480,7 @@ Find files with POSIX.1e .BR capabilities (7) set. .TP -\fB\-context \fIGLOB\fR +.BI "\-context " GLOB Find files whose SELinux context matches the .IR GLOB . .TP @@ -479,7 +507,11 @@ Find files the current user can execute/read/write. Always false/true. .RE .TP -\fB\-fstype \fITYPE\fR +\fB\-flags\fR [\fI\-+\fR]\fIFLAGS\fR +Find files with matching inode +.BR FLAGS . +.TP +.BI "\-fstype " TYPE Find files on file systems with the given .IR TYPE . .PP @@ -491,9 +523,11 @@ Find files owned by group/user ID .IR N . .RE .PP -\fB\-group \fINAME\fR +.B \-group +.I NAME .br -\fB\-user \fINAME\fR +.B \-user +.I NAME .RS Find files owned by the group/user .IR NAME . @@ -503,15 +537,20 @@ Find files owned by the group/user Find hidden files (those beginning with .IR . ). .PP -\fB\-ilname \fIGLOB\fR +.B \-ilname +.I GLOB .br -\fB\-iname \fIGLOB\fR +.B \-iname +.I GLOB .br -\fB\-ipath \fIGLOB\fR +.B \-ipath +.I GLOB .br -\fB\-iregex \fIREGEX\fR +.B \-iregex +.I REGEX .br -\fB\-iwholename \fIGLOB\fR +.B \-iwholename +.I GLOB .RS Case-insensitive versions of .BR \-lname / \-name / \-path / \-regex / \-wholename . @@ -526,19 +565,19 @@ Find files with .I N hard links. .TP -\fB\-lname \fIGLOB\fR +.BI "\-lname " GLOB Find symbolic links whose target matches the .IR GLOB . .TP -\fB\-name \fIGLOB\fR +.BI "\-name " GLOB Find files whose name matches the .IR GLOB . .TP -\fB\-newer \fIFILE\fR +.BI "\-newer " FILE Find files newer than .IR FILE . .TP -\fB\-newer\fIXY \fIREFERENCE\fR +.BI \-newer "XY REFERENCE" Find files whose .I X time is newer than the @@ -574,26 +613,28 @@ as an ISO 8601-style timestamp. For example: Find files owned by nonexistent groups/users. .RE .PP -\fB\-path \fIGLOB\fR +.B \-path +.I GLOB .br -\fB\-wholename \fIGLOB\fR +.B \-wholename +.I GLOB .RS Find files whose entire path matches the .IR GLOB . .RE .TP -\fB\-perm\fR [\fI\-\fR]\fIMODE\fR +\fB\-perm\fR [\fI\-+/\fR]\fIMODE\fR Find files with a matching mode. .TP -\fB\-regex \fIREGEX\fR +.BI "\-regex " REGEX Find files whose entire path matches the regular expression .IR REGEX . .TP -\fB\-samefile \fIFILE\fR +.BI "\-samefile " FILE Find hard links to .IR FILE . .TP -\fB\-since \fITIME\fR +.BI "\-since " TIME Find files modified since the ISO 8601-style timestamp .IR TIME . See @@ -672,7 +713,7 @@ days after they were changed. Find files with extended attributes .RB ( xattr (7)). .TP -\fB\-xattrname\fR \fINAME\fR +.BI "\-xattrname " NAME Find files with the extended attribute .IR NAME . .TP @@ -681,28 +722,31 @@ Find files of the given type, following links when .B \-type would not, and vice versa. .SH ACTIONS -.PP .B \-delete .br .B \-rm .RS -Delete any found files (implies \fB-depth\fR). +Delete any found files (implies +.BR \-depth ). .RE .TP -\fB\-exec \fIcommand ... {} ;\fR +.BI "\-exec " "command ... {} ;" Execute a command. .TP -\fB\-exec \fIcommand ... {} +\fR +.BI "\-exec " "command ... {} +" Execute a command with multiple files at once. .TP -\fB\-ok \fIcommand ... {} ;\fR +.BI "\-ok " "command ... {} ;" Prompt the user whether to execute a command. .PP -\fB\-execdir \fIcommand ... {} ;\fR +.B \-execdir +.I command ... {} ; .br -\fB\-execdir \fIcommand ... {} +\fR +.B \-execdir +.I command ... {} + .br -\fB\-okdir \fIcommand ... {} ;\fR +.B \-okdir +.I command ... {} ; .RS Like .BR \-exec / \-ok , @@ -714,13 +758,17 @@ Exit immediately with the given status .RI ( 0 if unspecified). .PP -\fB\-fls \fIFILE\fR +.B \-fls +.I FILE .br -\fB\-fprint \fIFILE\fR +.B \-fprint +.I FILE .br -\fB\-fprint0 \fIFILE\fR +.B \-fprint0 +.I FILE .br -\fB\-fprintf \fIFILE FORMAT\fR +.B \-fprintf +.I FILE FORMAT .RS Like .BR \-ls / \-print / \-print0 / \-printf , @@ -729,7 +777,7 @@ but write to instead of standard output. .RE .TP -\fB\-limit \fIN\fR +.BI "\-limit " N Quit once this action is evaluated .I N times. @@ -745,12 +793,12 @@ Print the path to the found file. .B \-print0 Like .BR \-print , -but use the null character ('\\0') as a separator rather than newlines. +but use the null character ('\e0') as a separator rather than newlines. Useful in conjunction with .B xargs .IR \-0 . .TP -\fB\-printf \fIFORMAT\fR +.BI "\-printf " FORMAT Print according to a format string (see .BR find (1)). These additional format directives are supported: @@ -896,7 +944,7 @@ is quoted to ensure the glob is processed by .B bfs rather than the shell. .TP -\fBbfs \-name access_log \-L \fI/var\fR +.BI "bfs \-name access_log \-L " /var Finds all files named .B access_log under @@ -905,7 +953,7 @@ following symbolic links. .B bfs allows flags and paths to appear anywhere on the command line. .TP -\fBbfs \fI~ \fB\-not \-user $USER\fR +.BI "bfs " ~ " \-not \-user $USER" Prints all files in your home directory not owned by you. .TP .B bfs \-xtype l @@ -913,7 +961,7 @@ Finds broken symbolic links. .TP .B bfs \-name config \-exclude \-name .git Finds all files named -.BR config, +.BR config , skipping every .B .git directory. diff --git a/src/alloc.c b/src/alloc.c index ebaff38..f505eda 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -1,11 +1,13 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "alloc.h" + +#include "bfs.h" #include "bit.h" #include "diag.h" #include "sanity.h" + #include <errno.h> #include <stdlib.h> #include <stdint.h> @@ -18,24 +20,22 @@ # define ALLOC_MAX (SIZE_MAX / 2) #endif -/** Portable aligned_alloc()/posix_memalign(). */ +/** posix_memalign() wrapper. */ static void *xmemalign(size_t align, size_t size) { bfs_assert(has_single_bit(align)); bfs_assert(align >= sizeof(void *)); - bfs_assert(is_aligned(align, size)); -#if BFS_HAS_ALIGNED_ALLOC - return aligned_alloc(align, size); -#else + // Since https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2072.htm, + // aligned_alloc() doesn't require the size to be a multiple of align. + // But the sanitizers don't know about that yet, so always use + // posix_memalign(). void *ptr = NULL; errno = posix_memalign(&ptr, align, size); return ptr; -#endif } void *alloc(size_t align, size_t size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, size)); if (size > ALLOC_MAX) { errno = EOVERFLOW; @@ -51,7 +51,6 @@ void *alloc(size_t align, size_t size) { void *zalloc(size_t align, size_t size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, size)); if (size > ALLOC_MAX) { errno = EOVERFLOW; @@ -71,8 +70,6 @@ void *zalloc(size_t align, size_t size) { void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size) { bfs_assert(has_single_bit(align)); - bfs_assert(is_aligned(align, old_size)); - bfs_assert(is_aligned(align, new_size)); if (new_size == 0) { free(ptr); @@ -106,10 +103,10 @@ void *reserve(void *ptr, size_t align, size_t size, size_t count) { size_t old_size = size * count; // Capacity is doubled every power of two, from 0→1, 1→2, 2→4, etc. - // If we stayed within the same size class, re-use ptr. + // If we stayed within the same size class, reuse ptr. if (count & (count - 1)) { // Tell sanitizers about the new array element - sanitize_alloc((char *)ptr + old_size, size); + sanitize_resize(ptr, old_size, old_size + size, bit_ceil(count) * size); errno = 0; return ptr; } @@ -124,7 +121,7 @@ void *reserve(void *ptr, size_t align, size_t size, size_t count) { } // Pretend we only allocated one more element - sanitize_free((char *)ret + old_size + size, new_size - old_size - size); + sanitize_resize(ret, new_size, old_size + size, new_size); errno = 0; return ret; } @@ -176,7 +173,7 @@ void arena_init(struct arena *arena, size_t align, size_t size) { } /** Allocate a new slab. */ -attr(cold) +_cold static int slab_alloc(struct arena *arena) { // Make the initial allocation size ~4K size_t size = 4096; @@ -231,6 +228,7 @@ void arena_free(struct arena *arena, void *ptr) { union chunk *chunk = ptr; chunk_set_next(arena, chunk, arena->chunks); arena->chunks = chunk; + sanitize_uninit(chunk, arena->size); sanitize_free(chunk, arena->size); } @@ -250,7 +248,7 @@ void arena_destroy(struct arena *arena) { sanitize_uninit(arena); } -void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size) { +void varena_init(struct varena *varena, size_t align, size_t offset, size_t size) { varena->align = align; varena->offset = offset; varena->size = size; @@ -259,7 +257,7 @@ void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, // The smallest size class is at least as many as fit in the smallest // aligned allocation size - size_t min_count = (flex_size(align, min, offset, size, 1) - offset + size - 1) / size; + size_t min_count = (flex_size(align, offset, size, 1) - offset + size - 1) / size; varena->shift = bit_width(min_count - 1); } @@ -272,7 +270,7 @@ static size_t varena_size_class(struct varena *varena, size_t count) { /** Get the exact size of a flexible struct. */ static size_t varena_exact_size(const struct varena *varena, size_t count) { - return flex_size(varena->align, 0, varena->offset, varena->size, count); + return flex_size(varena->align, varena->offset, varena->size, count); } /** Get the arena for the given array length. */ @@ -306,8 +304,7 @@ void *varena_alloc(struct varena *varena, size_t count) { } // Tell the sanitizers the exact size of the allocated struct - sanitize_free(ret, arena->size); - sanitize_alloc(ret, varena_exact_size(varena, count)); + sanitize_resize(ret, arena->size, varena_exact_size(varena, count), arena->size); return ret; } @@ -319,15 +316,14 @@ void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t return NULL; } - size_t new_exact_size = varena_exact_size(varena, new_count); - size_t old_exact_size = varena_exact_size(varena, old_count); + size_t old_size = old_arena->size; + size_t new_size = new_arena->size; if (new_arena == old_arena) { - if (new_count < old_count) { - sanitize_free((char *)ptr + new_exact_size, old_exact_size - new_exact_size); - } else if (new_count > old_count) { - sanitize_alloc((char *)ptr + old_exact_size, new_exact_size - old_exact_size); - } + sanitize_resize(ptr, + varena_exact_size(varena, old_count), + varena_exact_size(varena, new_count), + new_size); return ptr; } @@ -336,16 +332,18 @@ void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t return NULL; } - size_t old_size = old_arena->size; - sanitize_alloc((char *)ptr + old_exact_size, old_size - old_exact_size); + // Non-sanitized builds don't bother computing exact sizes, and just use + // the potentially-larger arena size for each size class instead. To + // allow the below memcpy() to work with the less-precise sizes, expand + // the old allocation to its full capacity. + sanitize_resize(ptr, varena_exact_size(varena, old_count), old_size, old_size); - size_t new_size = new_arena->size; size_t min_size = new_size < old_size ? new_size : old_size; memcpy(ret, ptr, min_size); arena_free(old_arena, ptr); - sanitize_free((char *)ret + new_exact_size, new_size - new_exact_size); + sanitize_resize(ret, new_size, varena_exact_size(varena, new_count), new_size); return ret; } diff --git a/src/alloc.h b/src/alloc.h index 095134a..1fafbab 100644 --- a/src/alloc.h +++ b/src/alloc.h @@ -8,127 +8,145 @@ #ifndef BFS_ALLOC_H #define BFS_ALLOC_H -#include "prelude.h" +#include "bfs.h" + #include <errno.h> #include <stddef.h> #include <stdlib.h> +#define IS_ALIGNED(align, size) \ + (((size) & ((align) - 1)) == 0) + /** Check if a size is properly aligned. */ static inline bool is_aligned(size_t align, size_t size) { - return (size & (align - 1)) == 0; + return IS_ALIGNED(align, size); } +#define ALIGN_FLOOR(align, size) \ + ((size) & ~((align) - 1)) + /** Round down to a multiple of an alignment. */ static inline size_t align_floor(size_t align, size_t size) { - return size & ~(align - 1); + return ALIGN_FLOOR(align, size); } +#define ALIGN_CEIL(align, size) \ + ((((size) - 1) | ((align) - 1)) + 1) + /** Round up to a multiple of an alignment. */ static inline size_t align_ceil(size_t align, size_t size) { - return align_floor(align, size + align - 1); + return ALIGN_CEIL(align, size); } /** - * Saturating array size. - * - * @param align - * Array element alignment. - * @param size - * Array element size. - * @param count - * Array element count. - * @return - * size * count, saturating to the maximum aligned value on overflow. + * Saturating size addition. + */ +static inline size_t size_add(size_t lhs, size_t rhs) { + size_t ret = lhs + rhs; + return ret >= lhs ? ret : (size_t)-1; +} + +/** + * Saturating size multiplication. */ -static inline size_t array_size(size_t align, size_t size, size_t count) { +static inline size_t size_mul(size_t size, size_t count) { size_t ret = size * count; - return ret / size == count ? ret : ~(align - 1); + return ret / size == count ? ret : (size_t)-1; } /** Saturating array sizeof. */ #define sizeof_array(type, count) \ - array_size(alignof(type), sizeof(type), count) + size_mul(sizeof(type), count) /** Size of a struct/union field. */ #define sizeof_member(type, member) \ sizeof(((type *)NULL)->member) /** + * @internal + * Our flexible struct size calculations assume that structs have the minimum + * trailing padding to align the type properly. A pathological ABI that adds + * extra padding would result in us under-allocating space for those structs, + * so we static_assert() that no such padding exists. + */ +#define ASSERT_FLEX_ABI(type, member) \ + ASSERT_FLEX_ABI_( \ + ALIGN_CEIL(alignof(type), offsetof(type, member)) >= sizeof(type), \ + "Unexpected tail padding in " #type) + +/** + * @internal + * The contortions here allow static_assert() to be used in expressions, rather + * than just declarations. + */ +#define ASSERT_FLEX_ABI_(...) \ + ((void)sizeof(struct { char _; static_assert(__VA_ARGS__); })) + +/** * Saturating flexible struct size. * - * @param align + * @align * Struct alignment. - * @param min - * Minimum struct size. - * @param offset + * @offset * Flexible array member offset. - * @param size + * @size * Flexible array element size. - * @param count + * @count * Flexible array element count. * @return * The size of the struct with count flexible array elements. Saturates * to the maximum aligned value on overflow. */ -static inline size_t flex_size(size_t align, size_t min, size_t offset, size_t size, size_t count) { - size_t ret = size * count; - size_t overflow = ret / size != count; - - size_t extra = offset + align - 1; - ret += extra; - overflow |= ret < extra; - ret |= -overflow; +static inline size_t flex_size(size_t align, size_t offset, size_t size, size_t count) { + size_t ret = size_mul(size, count); + ret = size_add(ret, offset + align - 1); ret = align_floor(align, ret); - - // Make sure flex_sizeof(type, member, 0) >= sizeof(type), even if the - // type has more padding than necessary for alignment - if (min > align_ceil(align, offset)) { - ret = ret < min ? min : ret; - } - return ret; } /** * Computes the size of a flexible struct. * - * @param type + * @type * The type of the struct containing the flexible array. - * @param member + * @member * The name of the flexible array member. - * @param count + * @count * The length of the flexible array. * @return * The size of the struct with count flexible array elements. Saturates * to the maximum aligned value on overflow. */ #define sizeof_flex(type, member, count) \ - flex_size(alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0]), count) + (ASSERT_FLEX_ABI(type, member), flex_size( \ + alignof(type), offsetof(type, member), sizeof_member(type, member[0]), count)) /** * General memory allocator. * - * @param align + * @align * The required alignment. - * @param size + * @size * The size of the allocation. * @return * The allocated memory, or NULL on failure. */ -attr(malloc(free, 1), aligned_alloc(1, 2)) +_malloc(free, 1) +_aligned_alloc(1, 2) void *alloc(size_t align, size_t size); /** * Zero-initialized memory allocator. * - * @param align + * @align * The required alignment. - * @param size + * @size * The size of the allocation. * @return * The allocated memory, or NULL on failure. */ -attr(malloc(free, 1), aligned_alloc(1, 2)) +_malloc(free, 1) +_aligned_alloc(1, 2) void *zalloc(size_t align, size_t size); /** Allocate memory for the given type. */ @@ -158,18 +176,19 @@ void *zalloc(size_t align, size_t size); /** * Alignment-aware realloc(). * - * @param ptr + * @ptr * The pointer to reallocate. - * @param align + * @align * The required alignment. - * @param old_size + * @old_size * The previous allocation size. - * @param new_size + * @new_size * The new allocation size. * @return * The reallocated memory, or NULL on failure. */ -attr(nodiscard, aligned_alloc(2, 4)) +_aligned_alloc(2, 4) +_nodiscard void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); /** Reallocate memory for an array. */ @@ -183,11 +202,11 @@ void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); /** * Reserve space for one more element in a dynamic array. * - * @param ptr + * @ptr * The pointer to reallocate. - * @param align + * @align * The required alignment. - * @param count + * @count * The current size of the array. * @return * The reallocated memory, on both success *and* failure. On success, @@ -195,17 +214,17 @@ void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size); * for (count + 1) elements. On failure, errno will be non-zero, and * ptr will returned unchanged. */ -attr(nodiscard) +_nodiscard void *reserve(void *ptr, size_t align, size_t size, size_t count); /** * Convenience macro to grow a dynamic array. * - * @param type + * @type * The array element type. - * @param type **ptr + * @type **ptr * A pointer to the array. - * @param size_t *count + * @size_t *count * A pointer to the array's size. * @return * On success, a pointer to the newly reserved array element, i.e. @@ -253,7 +272,7 @@ void arena_free(struct arena *arena, void *ptr); /** * Allocate an object out of the arena. */ -attr(malloc(arena_free, 2)) +_malloc(arena_free, 2) void *arena_alloc(struct arena *arena); /** @@ -287,40 +306,39 @@ struct varena { /** * Initialize a varena for a struct with the given layout. * - * @param varena + * @varena * The varena to initialize. - * @param align + * @align * alignof(type) - * @param min - * sizeof(type) - * @param offset + * @offset * offsetof(type, flexible_array) - * @param size + * @size * sizeof(flexible_array[i]) */ -void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size); +void varena_init(struct varena *varena, size_t align, size_t offset, size_t size); /** * Initialize a varena for the given type and flexible array. * - * @param varena + * @varena * The varena to initialize. - * @param type + * @type * A struct type containing a flexible array. - * @param member + * @member * The name of the flexible array member. */ #define VARENA_INIT(varena, type, member) \ - varena_init(varena, alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0])) + (ASSERT_FLEX_ABI(type, member), varena_init( \ + varena, alignof(type), offsetof(type, member), sizeof_member(type, member[0]))) /** * Free an arena-allocated flexible struct. * - * @param varena + * @varena * The that allocated the object. - * @param ptr + * @ptr * The object to free. - * @param count + * @count * The length of the flexible array. */ void varena_free(struct varena *varena, void *ptr, size_t count); @@ -328,46 +346,46 @@ void varena_free(struct varena *varena, void *ptr, size_t count); /** * Arena-allocate a flexible struct. * - * @param varena + * @varena * The varena to allocate from. - * @param count + * @count * The length of the flexible array. * @return * The allocated struct, or NULL on failure. */ -attr(malloc(varena_free, 2)) +_malloc(varena_free, 2) void *varena_alloc(struct varena *varena, size_t count); /** * Resize a flexible struct. * - * @param varena + * @varena * The varena to allocate from. - * @param ptr + * @ptr * The object to resize. - * @param old_count - * The old array lenth. - * @param new_count + * @old_count + * The old array length. + * @new_count * The new array length. * @return * The resized struct, or NULL on failure. */ -attr(nodiscard) +_nodiscard void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count); /** * Grow a flexible struct by an arbitrary amount. * - * @param varena + * @varena * The varena to allocate from. - * @param ptr + * @ptr * The object to resize. - * @param count + * @count * Pointer to the flexible array length. * @return * The resized struct, or NULL on failure. */ -attr(nodiscard) +_nodiscard void *varena_grow(struct varena *varena, void *ptr, size_t *count); /** diff --git a/src/atomic.h b/src/atomic.h index 360de20..5c2826f 100644 --- a/src/atomic.h +++ b/src/atomic.h @@ -8,8 +8,8 @@ #ifndef BFS_ATOMIC_H #define BFS_ATOMIC_H -#include "prelude.h" -#include "sanity.h" +#include "bfs.h" + #include <stdatomic.h> /** @@ -20,9 +20,9 @@ /** * Shorthand for atomic_load_explicit(). * - * @param obj + * @obj * A pointer to the atomic object. - * @param order + * @order * The memory ordering to use, without the memory_order_ prefix. * @return * The loaded value. @@ -87,7 +87,7 @@ /** * Shorthand for atomic_thread_fence(). */ -#if SANITIZE_THREAD +#if __SANITIZE_THREAD__ // TSan doesn't support fences: https://github.com/google/sanitizers/issues/1415 # define thread_fence(obj, order) \ fetch_add(obj, 0, order) @@ -109,7 +109,7 @@ # define spin_loop() __builtin_ia32_pause() #elif __has_builtin(__builtin_arm_yield) # define spin_loop() __builtin_arm_yield() -#elif __has_builtin(__builtin_riscv_pause) +#elif BFS_HAS_BUILTIN_RISCV_PAUSE # define spin_loop() __builtin_riscv_pause() #else # define spin_loop() ((void)0) @@ -1,21 +1,25 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "bar.h" + #include "alloc.h" #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "bit.h" #include "dstring.h" #include "sighook.h" + #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdarg.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> -#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> struct bfs_bar { int fd; @@ -28,19 +32,14 @@ struct bfs_bar { /** Get the terminal size, if possible. */ static int bfs_bar_getsize(struct bfs_bar *bar) { -#ifdef TIOCGWINSZ struct winsize ws; - if (ioctl(bar->fd, TIOCGWINSZ, &ws) != 0) { + if (xtcgetwinsize(bar->fd, &ws) != 0) { return -1; } store(&bar->width, ws.ws_col, relaxed); store(&bar->height, ws.ws_row, relaxed); return 0; -#else - errno = ENOTSUP; - return -1; -#endif } /** Write a string to the status bar (async-signal-safe). */ @@ -72,8 +71,26 @@ static char *ass_itoa(char *str, unsigned int n) { return str + len; } +/** Reset the scrollable region and hide the bar. */ +static int bfs_bar_reset(struct bfs_bar *bar) { + return bfs_bar_puts(bar, + "\0337" // DECSC: Save cursor + "\033[r" // DECSTBM: Reset scrollable region + "\0338" // DECRC: Restore cursor + "\033[J" // ED: Erase display from cursor to end + ); +} + +/** Hide the bar if the terminal is shorter than this. */ +#define BFS_BAR_MIN_HEIGHT 3 + /** Update the size of the scrollable region. */ static int bfs_bar_resize(struct bfs_bar *bar) { + unsigned int height = load(&bar->height, relaxed); + if (height < BFS_BAR_MIN_HEIGHT) { + return bfs_bar_reset(bar); + } + static const char PREFIX[] = "\033D" // IND: Line feed, possibly scrolling "\033[1A" // CUU: Move cursor up 1 row @@ -87,10 +104,8 @@ static int bfs_bar_resize(struct bfs_bar *bar) { char esc_seq[sizeof(PREFIX) + ITOA_DIGITS + sizeof(SUFFIX)]; // DECSTBM takes the height as the second argument - unsigned int height = load(&bar->height, relaxed) - 1; - char *cur = stpcpy(esc_seq, PREFIX); - cur = ass_itoa(cur, height); + cur = ass_itoa(cur, height - 1); cur = stpcpy(cur, SUFFIX); return bfs_bar_write(bar, esc_seq, cur - esc_seq); @@ -105,16 +120,6 @@ static void bfs_bar_sigwinch(int sig, siginfo_t *info, void *arg) { } #endif -/** Reset the scrollable region and hide the bar. */ -static int bfs_bar_reset(struct bfs_bar *bar) { - return bfs_bar_puts(bar, - "\0337" // DECSC: Save cursor - "\033[r" // DECSTBM: Reset scrollable region - "\0338" // DECRC: Restore cursor - "\033[J" // ED: Erase display from cursor to end - ); -} - /** Signal handler for process-terminating signals. */ static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) { struct bfs_bar *bar = arg; @@ -122,7 +127,7 @@ static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) { } /** printf() to the status bar with a single write(). */ -attr(printf(2, 3)) +_printf(2, 3) static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) { va_list args; va_start(args, format); @@ -183,6 +188,10 @@ unsigned int bfs_bar_width(const struct bfs_bar *bar) { int bfs_bar_update(struct bfs_bar *bar, const char *str) { unsigned int height = load(&bar->height, relaxed); + if (height < BFS_BAR_MIN_HEIGHT) { + return 0; + } + return bfs_bar_printf(bar, "\0337" // DECSC: Save cursor "\033[%u;0f" // HVP: Move cursor to row, column @@ -27,9 +27,9 @@ unsigned int bfs_bar_width(const struct bfs_bar *bar); /** * Update the status bar message. * - * @param bar + * @bar * The status bar to update. - * @param str + * @str * The string to display. * @return * 0 on success, -1 on failure. diff --git a/src/bfs.h b/src/bfs.h new file mode 100644 index 0000000..3cee727 --- /dev/null +++ b/src/bfs.h @@ -0,0 +1,241 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Configuration and fundamental utilities. + */ + +#ifndef BFS_H +#define BFS_H + +// Standard versions + +/** Possible __STDC_VERSION__ values. */ +#define C95 199409L +#define C99 199901L +#define C11 201112L +#define C17 201710L +#define C23 202311L + +/** Possible _POSIX_C_SOURCE and _POSIX_<OPTION> values. */ +#define POSIX_1990 1 +#define POSIX_1992 2 +#define POSIX_1993 199309L +#define POSIX_1995 199506L +#define POSIX_2001 200112L +#define POSIX_2008 200809L +#define POSIX_2024 202405L + +// Build configuration + +#include "config.h" + +#ifndef BFS_COMMAND +# define BFS_COMMAND "bfs" +#endif + +#ifndef BFS_HOMEPAGE +# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html" +#endif + +#ifndef BFS_LINT +# define BFS_LINT false +#endif + +// This is a symbol instead of a literal so we don't have to rebuild everything +// when the version number changes +extern const char bfs_version[]; + +extern const char bfs_confflags[]; +extern const char bfs_cc[]; +extern const char bfs_cppflags[]; +extern const char bfs_cflags[]; +extern const char bfs_ldflags[]; +extern const char bfs_ldlibs[]; + +// Get __GLIBC__ +#include <assert.h> + +// Fundamental utilities + +/** + * Get the length of an array. + */ +#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__])) + +/** + * False sharing/destructive interference/largest cache line size. + */ +#ifdef __GCC_DESTRUCTIVE_SIZE +# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE +#else +# define FALSE_SHARING_SIZE 64 +#endif + +/** + * True sharing/constructive interference/smallest cache line size. + */ +#ifdef __GCC_CONSTRUCTIVE_SIZE +# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE +#else +# define TRUE_SHARING_SIZE 64 +#endif + +/** + * Alignment specifier that avoids false sharing. + */ +#define cache_align alignas(FALSE_SHARING_SIZE) + +// Wrappers for attributes + +/** + * Silence warnings about switch/case fall-throughs. + */ +#if __has_attribute(fallthrough) +# define _fallthrough __attribute__((fallthrough)) +#else +# define _fallthrough ((void)0) +#endif + +/** + * Silence warnings about unused declarations. + */ +#if __has_attribute(unused) +# define _maybe_unused __attribute__((unused)) +#else +# define _maybe_unused +#endif + +/** + * Warn if a value is unused. + */ +#if __has_attribute(warn_unused_result) +# define _nodiscard __attribute__((warn_unused_result)) +#else +# define _nodiscard +#endif + +/** + * Hint to avoid inlining a function. + */ +#if __has_attribute(noinline) +# define _noinline __attribute__((noinline)) +#else +# define _noinline +#endif + +/** + * Marks a non-returning function. + */ +#if __STDC_VERSION__ >= C23 +# define _noreturn [[noreturn]] +#else +# define _noreturn _Noreturn +#endif + +/** + * Hint that a function is unlikely to be called. + */ +#if __has_attribute(cold) +# define _cold _noinline __attribute__((cold)) +#else +# define _cold _noinline +#endif + +/** + * Adds compiler warnings for bad printf()-style function calls, if supported. + */ +#if __has_attribute(format) +# define _printf(fmt, args) __attribute__((format(printf, fmt, args))) +#else +# define _printf(fmt, args) +#endif + +/** + * Annotates functions that potentially modify and return format strings. + */ +#if __has_attribute(format_arg) +# define _format_arg(arg) __attribute__((format_arg(arg))) +#else +# define _format_arg(arg) +#endif + +/** + * Annotates allocator-like functions. + */ +#if __has_attribute(malloc) +# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC +# define _malloc(...) _nodiscard __attribute__((malloc(__VA_ARGS__))) +# else +# define _malloc(...) _nodiscard __attribute__((malloc)) +# endif +#else +# define _malloc(...) _nodiscard +#endif + +/** + * Specifies that a function returns allocations with a given alignment. + */ +#if __has_attribute(alloc_align) +# define _alloc_align(param) __attribute__((alloc_align(param))) +#else +# define _alloc_align(param) +#endif + +/** + * Specifies that a function returns allocations with a given size. + */ +#if __has_attribute(alloc_size) +# define _alloc_size(...) __attribute__((alloc_size(__VA_ARGS__))) +#else +# define _alloc_size(...) +#endif + +/** + * Shorthand for _alloc_align() and _alloc_size(). + */ +#define _aligned_alloc(align, ...) _alloc_align(align) _alloc_size(__VA_ARGS__) + +/** + * Check if function multiversioning via GNU indirect functions (ifunc) is supported. + * + * Disabled on TSan due to https://github.com/google/sanitizers/issues/342. + */ +#ifndef BFS_USE_TARGET_CLONES +# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) && !__SANITIZE_THREAD__ +# define BFS_USE_TARGET_CLONES true +# else +# define BFS_USE_TARGET_CLONES false +# endif +#endif + +/** + * Apply the target_clones attribute, if available. + */ +#if BFS_USE_TARGET_CLONES +# define _target_clones(...) __attribute__((target_clones(__VA_ARGS__))) +#else +# define _target_clones(...) +#endif + +/** + * Mark the size of a flexible array member. + */ +#if __has_attribute(counted_by) +# define _counted_by(...) __attribute__((counted_by(__VA_ARGS__))) +#else +# define _counted_by(...) +#endif + +/** + * Optimization hint to not unroll a loop. + */ +#if BFS_HAS_PRAGMA_NOUNROLL +# define _nounroll _Pragma("nounroll") +#elif __GNUC__ && !__clang__ +# define _nounroll _Pragma("GCC unroll 0") +#else +# define _nounroll +#endif + +#endif // BFS_H diff --git a/src/bfstd.c b/src/bfstd.c index 44eda7c..b78af7a 100644 --- a/src/bfstd.c +++ b/src/bfstd.c @@ -1,13 +1,15 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "bfstd.h" + +#include "bfs.h" #include "bit.h" #include "diag.h" #include "sanity.h" #include "thread.h" #include "xregex.h" + #include <errno.h> #include <fcntl.h> #include <langinfo.h> @@ -15,25 +17,28 @@ #include <locale.h> #include <nl_types.h> #include <pthread.h> +#include <sched.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/ioctl.h> #include <sys/resource.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> +#include <termios.h> #include <unistd.h> #include <wchar.h> -#if BFS_USE_SYS_SYSMACROS_H +#if __has_include(<sys/sysmacros.h>) # include <sys/sysmacros.h> -#elif BFS_USE_SYS_MKDEV_H +#elif __has_include(<sys/mkdev.h>) # include <sys/mkdev.h> #endif -#if BFS_USE_UTIL_H +#if __has_include(<util.h>) # include <util.h> #endif @@ -184,16 +189,6 @@ char *xgetdelim(FILE *file, char delim) { } } -int open_cterm(int flags) { - char path[L_ctermid]; - if (ctermid(path) == NULL || strlen(path) == 0) { - errno = ENOTTY; - return -1; - } - - return open(path, flags); -} - const char *xgetprogname(void) { const char *cmd = NULL; #if BFS_HAS_GETPROGNAME @@ -209,6 +204,171 @@ const char *xgetprogname(void) { return cmd; } +/** Common prologue for xstrto*() wrappers. */ +static int xstrtox_prologue(const char *str) { + // strto*() skips leading spaces, but we want to reject them + if (xisspace(str[0])) { + errno = EINVAL; + return -1; + } + + errno = 0; + return 0; +} + +/** Common epilogue for xstrto*() wrappers. */ +static int xstrtox_epilogue(const char *str, char **end, char *endp) { + if (errno != 0) { + return -1; + } + + if (end) { + *end = endp; + } + + // If end is NULL, make sure the entire string is valid + if (endp == str || (!end && *endp != '\0')) { + errno = EINVAL; + return -1; + } + + return 0; +} + +int xstrtos(const char *str, char **end, int base, short *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < SHRT_MIN || n > SHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoi(const char *str, char **end, int base, int *value) { + long n; + if (xstrtol(str, end, base, &n) != 0) { + return -1; + } + + if (n < INT_MIN || n > INT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtol(const char *str, char **end, int base, long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtol(str, &endp, base); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtoll(const char *str, char **end, int base, long long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoll(str, &endp, base); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtof(const char *str, char **end, float *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtof(str, &endp); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtod(const char *str, char **end, double *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtod(str, &endp); + return xstrtox_epilogue(str, end, endp); +} + +int xstrtous(const char *str, char **end, int base, unsigned short *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > USHRT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +int xstrtoui(const char *str, char **end, int base, unsigned int *value) { + unsigned long n; + if (xstrtoul(str, end, base, &n) != 0) { + return -1; + } + + if (n > UINT_MAX) { + errno = ERANGE; + return -1; + } + + *value = n; + return 0; +} + +/** Common epilogue for xstrtou*() wrappers. */ +static int xstrtoux_epilogue(const char *str, char **end, char *endp) { + if (xstrtox_epilogue(str, end, endp) != 0) { + return -1; + } + + if (str[0] == '-') { + errno = ERANGE; + return -1; + } + + return 0; +} + +int xstrtoul(const char *str, char **end, int base, unsigned long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoul(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + +int xstrtoull(const char *str, char **end, int base, unsigned long long *value) { + if (xstrtox_prologue(str) != 0) { + return -1; + } + + char *endp; + *value = strtoull(str, &endp, base); + return xstrtoux_epilogue(str, end, endp); +} + /** Compile and execute a regular expression for xrpmatch(). */ static int xrpregex(nl_item item, const char *response) { const char *pattern = nl_langinfo(item); @@ -295,7 +455,7 @@ const char *xstrerror(int errnum) { // 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_STRERROR_L && !(__FreeBSD__ && __SANITIZE_MEMORY__) # if BFS_HAS_USELOCALE locale_t loc = uselocale((locale_t)0); # else @@ -451,7 +611,9 @@ int rlim_cmp(rlim_t a, rlim_t b) { } dev_t xmakedev(int ma, int mi) { -#ifdef makedev +#if __QNX__ + return makedev(0, ma, mi); +#elif defined(makedev) return makedev(ma, mi); #else return (ma << 8) | mi; @@ -482,6 +644,32 @@ pid_t xwaitpid(pid_t pid, int *status, int flags) { return ret; } +int open_cterm(int flags) { + char path[L_ctermid]; + if (ctermid(path) == NULL || strlen(path) == 0) { + errno = ENOTTY; + return -1; + } + + return open(path, flags); +} + +int xtcgetwinsize(int fd, struct winsize *ws) { +#if BFS_HAS_TCGETWINSIZE + return tcgetwinsize(fd, ws); +#else + return ioctl(fd, TIOCGWINSZ, ws); +#endif +} + +int xtcsetwinsize(int fd, const struct winsize *ws) { +#if BFS_HAS_TCSETWINSIZE + return tcsetwinsize(fd, ws); +#else + return ioctl(fd, TIOCSWINSZ, ws); +#endif +} + int dup_cloexec(int fd) { #ifdef F_DUPFD_CLOEXEC return fcntl(fd, F_DUPFD_CLOEXEC, 0); @@ -686,55 +874,117 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long * } long xsysconf(int name) { -#if __FreeBSD__ && SANITIZE_MEMORY +#if __FreeBSD__ && __SANITIZE_MEMORY__ // Work around https://github.com/llvm/llvm-project/issues/88163 __msan_scoped_disable_interceptor_checks(); #endif long ret = sysconf(name); -#if __FreeBSD__ && SANITIZE_MEMORY +#if __FreeBSD__ && __SANITIZE_MEMORY__ __msan_scoped_enable_interceptor_checks(); #endif return ret; } -size_t asciilen(const char *str) { - return asciinlen(str, strlen(str)); -} +#if BFS_HAS_SCHED_GETAFFINITY +/** Get the CPU count in an affinity mask of the given size. */ +static long bfs_sched_getaffinity(size_t size) { + cpu_set_t set, *pset = &set; -size_t asciinlen(const char *str, size_t n) { - size_t i = 0; + if (size > sizeof(set)) { + pset = malloc(size); + if (!pset) { + return -1; + } + } -#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)); + long ret = -1; + if (sched_getaffinity(0, size, pset) == 0) { +# ifdef CPU_COUNT_S + ret = CPU_COUNT_S(size, pset); +# else + bfs_assert(size <= sizeof(set)); + ret = CPU_COUNT(pset); +# endif + } - const size_t mask = (SIZE_MAX / 0xFF) << 7; // 0x808080... - word &= mask; - if (!word) { - continue; - } + if (pset != &set) { + free(pset); + } + return ret; +} +#endif -#if ENDIAN_NATIVE == ENDIAN_BIG - word = bswap(word); -#elif ENDIAN_NATIVE != ENDIAN_LITTLE +long nproc(void) { + long ret = 0; + +#if BFS_HAS_SCHED_GETAFFINITY + size_t size = sizeof(cpu_set_t); + do { + ret = bfs_sched_getaffinity(size); + +# ifdef CPU_COUNT_S + // On Linux, sched_getaffinity(2) says: + // + // When working on systems with large kernel CPU affinity masks, one must + // dynamically allocate the mask argument (see CPU_ALLOC(3)). Currently, + // the only way to do this is by probing for the size of the required mask + // using sched_getaffinity() calls with increasing mask sizes (until the + // call does not fail with the error EINVAL). + size *= 2; +# else + // No support for dynamically-sized CPU masks break; +# endif + } while (ret < 0 && errno == EINVAL); #endif - size_t first = trailing_zeros(word) / 8; - return i + first; + if (ret < 1) { + ret = xsysconf(_SC_NPROCESSORS_ONLN); } -#endif - for (; i < n; ++i) { - if (!xisascii(str[i])) { - break; - } + if (ret < 1) { + ret = 1; } + return ret; +} + +size_t asciilen(const char *str) { + return asciinlen(str, strlen(str)); +} + +size_t asciinlen(const char *str, size_t n) { + const unsigned char *ustr = (const unsigned char *)str; + size_t i = 0; + + // Word-at-a-time isascii() +#define CHUNK(n) CHUNK_(uint##n##_t, load8_leu##n) +#define CHUNK_(type, load8) \ + (n - i >= sizeof(type)) { \ + type word = load8(ustr + i); \ + type mask = (((type)-1) / 0xFF) << 7; /* 0x808080.. */ \ + word &= mask; \ + i += trailing_zeros(word) / 8; \ + if (word) { \ + return i; \ + } \ + } + +#if SIZE_WIDTH >= 64 + while CHUNK(64); + if CHUNK(32); +#else + while CHUNK(32); +#endif + if CHUNK(16); + if CHUNK(8); + +#undef CHUNK_ +#undef CHUNK + return i; } @@ -936,14 +1186,14 @@ static char *dollar_quote(char *dest, char *end, const char *str, size_t len, en /** How much of this string is safe as a bare word? */ static size_t bare_len(const char *str, size_t len) { - // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02 + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02 size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}"); return ret < len ? ret : len; } /** How much of this string is safe to double-quote? */ static size_t quotable_len(const char *str, size_t len) { - // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03 + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_03 size_t ret = strcspn(str, "`$\\\"!"); return ret < len ? ret : len; } diff --git a/src/bfstd.h b/src/bfstd.h index f5d8622..15dd949 100644 --- a/src/bfstd.h +++ b/src/bfstd.h @@ -8,8 +8,8 @@ #ifndef BFS_BFSTD_H #define BFS_BFSTD_H -#include "prelude.h" -#include "sanity.h" +#include "bfs.h" + #include <stddef.h> #include <ctype.h> @@ -18,7 +18,7 @@ * Work around https://github.com/llvm/llvm-project/issues/65532 by forcing a * function, not a macro, to be called. */ -#if __FreeBSD__ && SANITIZE_MEMORY +#if __FreeBSD__ && __SANITIZE_MEMORY__ # define BFS_INTERCEPT(fn) (fn) #else # define BFS_INTERCEPT(fn) fn @@ -48,9 +48,9 @@ * Check if an error code is "like" another one. For example, ENOTDIR is * like ENOENT because they can both be triggered by non-existent paths. * - * @param error + * @error * The error code to check. - * @param category + * @category * The category to test for. Known categories include ENOENT and * ENAMETOOLONG. * @return @@ -66,7 +66,7 @@ bool errno_is_like(int category); /** * Apply the "negative errno" convention. * - * @param ret + * @ret * The return value of the attempted operation. * @return * ret, if non-negative, otherwise -errno. @@ -106,7 +106,7 @@ int try(int ret); /** * Re-entrant dirname() variant that always allocates a copy. * - * @param path + * @path * The path in question. * @return * The parent directory of the path. @@ -116,7 +116,7 @@ char *xdirname(const char *path); /** * Re-entrant basename() variant that always allocates a copy. * - * @param path + * @path * The path in question. * @return * The final component of the path. @@ -126,7 +126,7 @@ char *xbasename(const char *path); /** * Find the offset of the final component of a path. * - * @param path + * @path * The path in question. * @return * The offset of the basename. @@ -138,9 +138,9 @@ size_t xbaseoff(const char *path); /** * fopen() variant that takes open() style flags. * - * @param path + * @path * The path to open. - * @param flags + * @flags * Flags to pass to open(). */ FILE *xfopen(const char *path, int flags); @@ -148,9 +148,9 @@ FILE *xfopen(const char *path, int flags); /** * Convenience wrapper for getdelim(). * - * @param file + * @file * The file to read. - * @param delim + * @delim * The delimiter character to split on. * @return * The read chunk (without the delimiter), allocated with malloc(). @@ -158,16 +158,6 @@ FILE *xfopen(const char *path, int flags); */ char *xgetdelim(FILE *file, char delim); -/** - * Open the controlling terminal. - * - * @param flags - * The open() flags. - * @return - * An open file descriptor, or -1 on failure. - */ -int open_cterm(int flags); - // #include <stdlib.h> /** @@ -179,6 +169,56 @@ int open_cterm(int flags); const char *xgetprogname(void); /** + * Like xstrtol(), but for short. + */ +int xstrtos(const char *str, char **end, int base, short *value); + +/** + * Like xstrtol(), but for int. + */ +int xstrtoi(const char *str, char **end, int base, int *value); + +/** + * Wrapper for strtol() that forbids leading spaces. + */ +int xstrtol(const char *str, char **end, int base, long *value); + +/** + * Wrapper for strtoll() that forbids leading spaces. + */ +int xstrtoll(const char *str, char **end, int base, long long *value); + +/** + * Like xstrtoul(), but for unsigned short. + */ +int xstrtous(const char *str, char **end, int base, unsigned short *value); + +/** + * Like xstrtoul(), but for unsigned int. + */ +int xstrtoui(const char *str, char **end, int base, unsigned int *value); + +/** + * Wrapper for strtoul() that forbids leading spaces, negatives. + */ +int xstrtoul(const char *str, char **end, int base, unsigned long *value); + +/** + * Wrapper for strtoull() that forbids leading spaces, negatives. + */ +int xstrtoull(const char *str, char **end, int base, unsigned long long *value); + +/** + * Wrapper for strtof() that forbids leading spaces. + */ +int xstrtof(const char *str, char **end, float *value); + +/** + * Wrapper for strtod() that forbids leading spaces. + */ +int xstrtod(const char *str, char **end, double *value); + +/** * Process a yes/no prompt. * * @return 1 for yes, 0 for no, and -1 for unknown. @@ -195,9 +235,9 @@ size_t asciilen(const char *str); /** * Get the length of the pure-ASCII prefix of a string. * - * @param str + * @str * The string to check. - * @param n + * @n * The maximum prefix length. */ size_t asciinlen(const char *str, size_t n); @@ -205,9 +245,9 @@ size_t asciinlen(const char *str, size_t n); /** * Allocate a copy of a region of memory. * - * @param src + * @src * The memory region to copy. - * @param size + * @size * The size of the memory region. * @return * A copy of the region, allocated with malloc(), or NULL on failure. @@ -217,12 +257,12 @@ void *xmemdup(const void *src, size_t size); /** * A nice string copying function. * - * @param dest + * @dest * The NUL terminator of the destination string, or `end` if it is * already truncated. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to copy from. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -232,14 +272,14 @@ char *xstpecpy(char *dest, char *end, const char *src); /** * A nice string copying function. * - * @param dest + * @dest * The NUL terminator of the destination string, or `end` if it is * already truncated. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to copy from. - * @param n + * @n * The maximum number of characters to copy. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -249,7 +289,7 @@ char *xstpencpy(char *dest, char *end, const char *src, size_t n); /** * Thread-safe strerror(). * - * @param errnum + * @errnum * An error number. * @return * A string describing that error, which remains valid until the next @@ -265,9 +305,9 @@ const char *errstr(void); /** * Format a mode like ls -l (e.g. -rw-r--r--). * - * @param mode + * @mode * The mode to format. - * @param str + * @str * The string to hold the formatted mode. */ void xstrmode(mode_t mode, char str[11]); @@ -322,12 +362,35 @@ int xminor(dev_t dev); */ pid_t xwaitpid(pid_t pid, int *status, int flags); +#include <sys/ioctl.h> // May be necessary for struct winsize +#include <termios.h> + +/** + * Open the controlling terminal. + * + * @flags + * The open() flags. + * @return + * An open file descriptor, or -1 on failure. + */ +int open_cterm(int flags); + +/** + * tcgetwinsize()/ioctl(TIOCGWINSZ) wrapper. + */ +int xtcgetwinsize(int fd, struct winsize *ws); + +/** + * tcsetwinsize()/ioctl(TIOCSWINSZ) wrapper. + */ +int xtcsetwinsize(int fd, const struct winsize *ws); + // #include <unistd.h> /** * Like dup(), but set the FD_CLOEXEC flag. * - * @param fd + * @fd * The file descriptor to duplicate. * @return * A duplicated file descriptor, or -1 on failure. @@ -337,7 +400,7 @@ int dup_cloexec(int fd); /** * Like pipe(), but set the FD_CLOEXEC flag. * - * @param pipefd + * @pipefd * The array to hold the two file descriptors. * @return * 0 on success, -1 on failure. @@ -359,14 +422,14 @@ size_t xread(int fd, void *buf, size_t nbytes); * writes. * * @return - The number of bytes written. A value != nbytes indicates an error. + * The number of bytes written. A value != nbytes indicates an error. */ size_t xwrite(int fd, const void *buf, size_t nbytes); /** * close() variant that preserves errno. * - * @param fd + * @fd * The file descriptor to close. */ void close_quietly(int fd); @@ -374,7 +437,7 @@ void close_quietly(int fd); /** * close() wrapper that asserts the file descriptor is valid. * - * @param fd + * @fd * The file descriptor to close. * @return * 0 on success, or -1 on error. @@ -389,11 +452,11 @@ int xfaccessat(int fd, const char *path, int amode); /** * readlinkat() wrapper that dynamically allocates the result. * - * @param fd + * @fd * The base directory descriptor. - * @param path + * @path * The path to the link, relative to fd. - * @param size + * @size * An estimate for the size of the link name (pass 0 if unknown). * @return * The target of the link, allocated with malloc(), or NULL on failure. @@ -403,7 +466,7 @@ char *xreadlinkat(int fd, const char *path, size_t size); /** * Wrapper for confstr() that allocates with malloc(). * - * @param name + * @name * The ID of the confstr to look up. * @return * The value of the confstr, or NULL on failure. @@ -413,12 +476,12 @@ char *xconfstr(int name); /** * Portability wrapper for strtofflags(). * - * @param str + * @str * The string to parse. The pointee will be advanced to the first * invalid position on error. - * @param set + * @set * The flags that are set in the string. - * @param clear + * @clear * The flags that are cleared in the string. * @return * 0 on success, -1 on failure. @@ -430,18 +493,36 @@ int xstrtofflags(const char **str, unsigned long long *set, unsigned long long * */ long xsysconf(int name); +/** + * Check for a POSIX option[1] at runtime. + * + * [1]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap02.html#tag_02_01_06 + * + * @name + * The symbolic name of the POSIX option (e.g. SPAWN). + * @return + * The value of the option, either -1 or a date like 202405. + */ +#define sysoption(name) \ + (_POSIX_##name == 0 ? xsysconf(_SC_##name) : _POSIX_##name) + +/** + * Get the number of CPU threads available to the current process. + */ +long nproc(void); + #include <wchar.h> /** * Error-recovering mbrtowc() wrapper. * - * @param str + * @str * The string to convert. - * @param i + * @i * The current index. - * @param len + * @len * The length of the string. - * @param mb + * @mb * The multi-byte decoding state. * @return * The wide character at index *i, or WEOF if decoding fails. In either @@ -452,7 +533,7 @@ wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb); /** * wcswidth() variant that works on narrow strings. * - * @param str + * @str * The string to measure. * @return * The likely width of that string in a terminal. @@ -504,13 +585,13 @@ enum wesc_flags { /** * Escape a string as a single shell word. * - * @param dest + * @dest * The destination string to fill. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to escape. - * @param flags + * @flags * Controls which characters to escape. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -520,15 +601,15 @@ char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags); /** * Escape a string as a single shell word. * - * @param dest + * @dest * The destination string to fill. - * @param end + * @end * The end of the destination buffer. - * @param src + * @src * The string to escape. - * @param n + * @n * The maximum length of the string. - * @param flags + * @flags * Controls which characters to escape. * @return * The new NUL terminator of the destination, or `end` on truncation. @@ -18,9 +18,10 @@ * various helper functions to take fewer parameters. */ -#include "prelude.h" #include "bftw.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "dir.h" @@ -30,6 +31,7 @@ #include "mtab.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdlib.h> @@ -251,7 +253,7 @@ struct bftw_file { /** The length of the file's name. */ size_t namelen; /** The file's name. */ - char name[]; + char name[]; // _counted_by(namelen + 1) }; /** @@ -446,7 +448,7 @@ static void bftw_queue_rebalance(struct bftw_queue *queue, bool async) { } } -/** Detatch the next waiting file. */ +/** Detach the next waiting file. */ static void bftw_queue_detach(struct bftw_queue *queue, struct bftw_file *file, bool async) { bfs_assert(!file->ioqueued); @@ -1006,6 +1008,7 @@ static int bftw_ioq_pop(struct bftw_state *state, bool block) { return -1; } + ioq_submit(ioq); struct ioq_ent *ent = ioq_pop(ioq, block); if (!ent) { return -1; @@ -1049,6 +1052,10 @@ static int bftw_ioq_pop(struct bftw_state *state, bool block) { bftw_queue_attach(&state->fileq, file, true); break; + + default: + bfs_bug("Unexpected ioq op %d", (int)op); + break; } ioq_free(ioq, ent); @@ -1160,12 +1167,13 @@ static int bftw_file_open(struct bftw_state *state, struct bftw_file *file, cons struct bftw_list parents; SLIST_INIT(&parents); - struct bftw_file *cur; - for (cur = file; cur != base; cur = cur->parent) { + // Reverse the chain of parents + for (struct bftw_file *cur = file; cur != base; cur = cur->parent) { SLIST_PREPEND(&parents, cur); } - while ((cur = SLIST_POP(&parents))) { + // Open each component relative to its parent + drain_slist (struct bftw_file, cur, &parents) { if (!cur->parent || cur->parent->fd >= 0) { bftw_file_openat(state, cur, cur->parent, cur->name); } @@ -1281,8 +1289,8 @@ static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) { int fd = parent->fd; if (fd < 0) { - bfs_static_assert((int)AT_FDCWD != -1); - return -1; + // Don't confuse failures with AT_FDCWD + return (int)AT_FDCWD == -1 ? -2 : -1; } bftw_cache_pin(&state->cache, parent); @@ -1431,7 +1439,7 @@ static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bf if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) { return true; } - fallthru; + _fallthrough; default: #if __linux__ @@ -1477,7 +1485,8 @@ fail: /** Check if we should stat() a file asynchronously. */ static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) { - // To avoid surprising users too much, process the roots in order + // POSIX wants the root paths to be processed in order + // See https://www.austingroupbugs.net/view.php?id=1859 if (file->depth == 0) { return false; } @@ -1529,11 +1538,28 @@ static bool bftw_pop_file(struct bftw_state *state) { return bftw_pop(state, &state->fileq); } +/** Add a path component to the path. */ +static void bftw_prepend_path(char *path, size_t nameoff, size_t namelen, const char *name) { + if (nameoff > 0) { + path[nameoff - 1] = '/'; + } + memcpy(path + nameoff, name, namelen); +} + /** Build the path to the current file. */ static int bftw_build_path(struct bftw_state *state, const char *name) { const struct bftw_file *file = state->file; - size_t pathlen = file ? file->nameoff + file->namelen : 0; + size_t nameoff, namelen; + if (name) { + nameoff = file ? bftw_child_nameoff(file) : 0; + namelen = strlen(name); + } else { + nameoff = file->nameoff; + namelen = file->namelen; + } + + size_t pathlen = nameoff + namelen; if (dstresize(&state->path, pathlen) != 0) { state->error = errno; return -1; @@ -1546,11 +1572,11 @@ static int bftw_build_path(struct bftw_state *state, const char *name) { } // Build the path backwards + if (name) { + bftw_prepend_path(state->path, nameoff, namelen, name); + } while (file && file != ancestor) { - if (file->nameoff > 0) { - state->path[file->nameoff - 1] = '/'; - } - memcpy(state->path + file->nameoff, file->name, file->namelen); + bftw_prepend_path(state->path, file->nameoff, file->namelen, file->name); if (ancestor && ancestor->depth == file->depth) { ancestor = ancestor->parent; @@ -1559,20 +1585,6 @@ static int bftw_build_path(struct bftw_state *state, const char *name) { } state->previous = state->file; - - if (name) { - if (pathlen > 0 && state->path[pathlen - 1] != '/') { - if (dstrapp(&state->path, '/') != 0) { - state->error = errno; - return -1; - } - } - if (dstrcat(&state->path, name) != 0) { - state->error = errno; - return -1; - } - } - return 0; } @@ -1676,6 +1688,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { ftwbuf->visit = visit; ftwbuf->type = BFS_UNKNOWN; ftwbuf->error = state->direrror; + ftwbuf->loopoff = 0; ftwbuf->at_fd = AT_FDCWD; ftwbuf->at_path = ftwbuf->path; bftw_stat_init(&ftwbuf->stat_bufs, &state->stat_buf, &state->lstat_buf); @@ -1733,6 +1746,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) { if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) { ftwbuf->type = BFS_ERROR; ftwbuf->error = ELOOP; + ftwbuf->loopoff = ancestor->nameoff + ancestor->namelen; return; } } @@ -1863,8 +1877,8 @@ static int bftw_gc(struct bftw_state *state, enum bftw_gc_flags flags) { } state->direrror = 0; - while ((file = SLIST_POP(&state->to_close, ready))) { - bftw_unwrapdir(state, file); + drain_slist (struct bftw_file, dead, &state->to_close, ready) { + bftw_unwrapdir(state, dead); } enum bftw_gc_flags visit = BFTW_VISIT_FILE; @@ -1945,6 +1959,10 @@ static void bftw_flush(struct bftw_state *state) { bftw_queue_flush(&state->dirq); bftw_ioq_opendirs(state); + + if (state->ioq) { + ioq_submit(state->ioq); + } } /** Close the current directory. */ @@ -10,6 +10,7 @@ #include "dir.h" #include "stat.h" + #include <stddef.h> /** @@ -56,6 +57,8 @@ struct BFTW { enum bfs_type type; /** The errno that occurred, if type == BFS_ERROR. */ int error; + /** For filesystem loops, the length of the loop prefix. */ + size_t loopoff; /** A parent file descriptor for the *at() family of calls. */ int at_fd; @@ -72,9 +75,9 @@ struct BFTW { * Get bfs_stat() info for a file encountered during bftw(), caching the result * whenever possible. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to stat. - * @param flags + * @flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return * A pointer to a bfs_stat() buffer, or NULL if the call failed. @@ -85,9 +88,9 @@ const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags * Get bfs_stat() info for a file encountered during bftw(), if it has already * been cached. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to stat. - * @param flags + * @flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return * A pointer to a bfs_stat() buffer, or NULL if no stat info is cached. @@ -99,9 +102,9 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat * whether to follow links. This function will avoid calling bfs_stat() if * possible. * - * @param ftwbuf + * @ftwbuf * bftw() data for the file to check. - * @param flags + * @flags * flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags. * @return * The type of the file, or BFS_ERROR if an error occurred. @@ -123,9 +126,9 @@ enum bftw_action { /** * Callback function type for bftw(). * - * @param ftwbuf + * @ftwbuf * Data about the current file. - * @param ptr + * @ptr * The pointer passed to bftw(). * @return * An action value. @@ -208,7 +211,7 @@ struct bftw_args { * Like ftw(3) and nftw(3), this function walks a directory tree recursively, * and invokes a callback for each path it encounters. * - * @param args + * @args * The arguments that control the walk. * @return * 0 on success, or -1 on failure. @@ -8,11 +8,12 @@ #ifndef BFS_BIT_H #define BFS_BIT_H -#include "prelude.h" +#include "bfs.h" + #include <limits.h> #include <stdint.h> -#if __STDC_VERSION__ >= C23 +#if __has_include(<stdbit.h>) # include <stdbit.h> #endif @@ -147,7 +148,7 @@ # define INTMAX_WIDTH UINTMAX_WIDTH #endif -// C23 polyfill: byte order +// N3022 polyfill: byte order #ifdef __STDC_ENDIAN_LITTLE__ # define ENDIAN_LITTLE __STDC_ENDIAN_LITTLE__ @@ -173,11 +174,7 @@ # define ENDIAN_NATIVE 0 #endif -#if __STDC_VERSION__ >= C23 -# define bswap_u16 stdc_memreverse8u16 -# define bswap_u32 stdc_memreverse8u32 -# define bswap_u64 stdc_memreverse8u64 -#elif __GNUC__ +#if __GNUC__ # define bswap_u16 __builtin_bswap16 # define bswap_u32 __builtin_bswap32 # define bswap_u64 __builtin_bswap64 @@ -201,15 +198,35 @@ static inline uint8_t bswap_u8(uint8_t n) { return n; } -/** - * Reverse the byte order of an integer. - */ -#define bswap(n) \ - _Generic((n), \ - uint8_t: bswap_u8, \ - uint16_t: bswap_u16, \ - uint32_t: bswap_u32, \ - uint64_t: bswap_u64)(n) +#if UCHAR_WIDTH == 8 +# define bswap_uc bswap_u8 +#endif + +#if USHRT_WIDTH == 16 +# define bswap_us bswap_u16 +#elif USHRT_WIDTH == 32 +# define bswap_us bswap_u32 +#elif USHRT_WIDTH == 64 +# define bswap_us bswap_u64 +#endif + +#if UINT_WIDTH == 16 +# define bswap_ui bswap_u16 +#elif UINT_WIDTH == 32 +# define bswap_ui bswap_u32 +#elif UINT_WIDTH == 64 +# define bswap_ui bswap_u64 +#endif + +#if ULONG_WIDTH == 32 +# define bswap_ul bswap_u32 +#elif ULONG_WIDTH == 64 +# define bswap_ul bswap_u64 +#endif + +#if ULLONG_WIDTH == 64 +# define bswap_ull bswap_u64 +#endif // Define an overload for each unsigned type #define UINT_OVERLOADS(macro) \ @@ -222,25 +239,74 @@ static inline uint8_t bswap_u8(uint8_t n) { // Select an overload based on an unsigned integer type #define UINT_SELECT(n, name) \ _Generic((n), \ - char: name##_uc, \ - signed char: name##_uc, \ unsigned char: name##_uc, \ - signed short: name##_us, \ unsigned short: name##_us, \ - signed int: name##_ui, \ unsigned int: name##_ui, \ - signed long: name##_ul, \ unsigned long: name##_ul, \ - signed long long: name##_ull, \ unsigned long long: name##_ull) +/** + * Reverse the byte order of an integer. + */ +#define bswap(n) UINT_SELECT(n, bswap)(n) + +#define LOAD8_LEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (i)) +#define LOAD8_BEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (n - (i) - 8)) + +/** Load a little-endian 8-bit word. */ +static inline uint8_t load8_leu8(const void *ptr) { + return LOAD8_LEU8(ptr, 0, 8); +} + +/** Load a big-endian 8-bit word. */ +static inline uint8_t load8_beu8(const void *ptr) { + return LOAD8_BEU8(ptr, 0, 8); +} + +#define LOAD8_LEU16(ptr, i, n) (LOAD8_LEU8(ptr, i, n) | LOAD8_LEU8(ptr, i + 8, n)) +#define LOAD8_BEU16(ptr, i, n) (LOAD8_BEU8(ptr, i, n) | LOAD8_BEU8(ptr, i + 8, n)) + +/** Load a little-endian 16-bit word. */ +static inline uint16_t load8_leu16(const void *ptr) { + return LOAD8_LEU16(ptr, 0, 16); +} + +/** Load a big-endian 16-bit word. */ +static inline uint16_t load8_beu16(const void *ptr) { + return LOAD8_BEU16(ptr, 0, 16); +} + +#define LOAD8_LEU32(ptr, i, n) (LOAD8_LEU16(ptr, i, n) | LOAD8_LEU16(ptr, i + 16, n)) +#define LOAD8_BEU32(ptr, i, n) (LOAD8_BEU16(ptr, i, n) | LOAD8_BEU16(ptr, i + 16, n)) + +/** Load a little-endian 32-bit word. */ +static inline uint32_t load8_leu32(const void *ptr) { + return LOAD8_LEU32(ptr, 0, 32); +} + +/** Load a big-endian 32-bit word. */ +static inline uint32_t load8_beu32(const void *ptr) { + return LOAD8_BEU32(ptr, 0, 32); +} + +#define LOAD8_LEU64(ptr, i, n) (LOAD8_LEU32(ptr, i, n) | LOAD8_LEU32(ptr, i + 32, n)) +#define LOAD8_BEU64(ptr, i, n) (LOAD8_BEU32(ptr, i, n) | LOAD8_BEU32(ptr, i + 32, n)) + +/** Load a little-endian 64-bit word. */ +static inline uint64_t load8_leu64(const void *ptr) { + return LOAD8_LEU64(ptr, 0, 64); +} + +/** Load a big-endian 64-bit word. */ +static inline uint64_t load8_beu64(const void *ptr) { + return LOAD8_BEU64(ptr, 0, 64); +} + // C23 polyfill: bit utilities -#if __STDC_VERSION__ >= C23 +#if __STDC_VERSION_STDBIT_H__ >= C23 # define count_ones stdc_count_ones # define count_zeros stdc_count_zeros -# define rotate_left stdc_rotate_left -# define rotate_right stdc_rotate_right # define leading_zeros stdc_leading_zeros # define leading_ones stdc_leading_ones # define trailing_zeros stdc_trailing_zeros @@ -273,31 +339,31 @@ static inline uint8_t bswap_u8(uint8_t n) { #define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix #define COUNT_ONES(type, suffix, width) \ - static inline int count_ones##suffix(type n) { \ + static inline unsigned int count_ones##suffix(type n) { \ return UINT_BUILTIN(popcount, suffix)(n); \ } #define LEADING_ZEROS(type, suffix, width) \ - static inline int leading_zeros##suffix(type n) { \ + static inline unsigned int leading_zeros##suffix(type n) { \ return n \ ? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \ : width; \ } #define TRAILING_ZEROS(type, suffix, width) \ - static inline int trailing_zeros##suffix(type n) { \ + static inline unsigned int trailing_zeros##suffix(type n) { \ return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \ } #define FIRST_TRAILING_ONE(type, suffix, width) \ - static inline int first_trailing_one##suffix(type n) { \ + static inline unsigned int first_trailing_one##suffix(type n) { \ return UINT_BUILTIN(ffs, suffix)(n); \ } #else // !__GNUC__ #define COUNT_ONES(type, suffix, width) \ - static inline int count_ones##suffix(type n) { \ + static inline unsigned int count_ones##suffix(type n) { \ int ret; \ for (ret = 0; n; ++ret) { \ n &= n - 1; \ @@ -306,7 +372,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define LEADING_ZEROS(type, suffix, width) \ - static inline int leading_zeros##suffix(type n) { \ + static inline unsigned int leading_zeros##suffix(type n) { \ type bit = (type)1 << (width - 1); \ int ret; \ for (ret = 0; bit && !(n & bit); ++ret, bit >>= 1); \ @@ -314,7 +380,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define TRAILING_ZEROS(type, suffix, width) \ - static inline int trailing_zeros##suffix(type n) { \ + static inline unsigned int trailing_zeros##suffix(type n) { \ type bit = 1; \ int ret; \ for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \ @@ -322,7 +388,7 @@ static inline uint8_t bswap_u8(uint8_t n) { } #define FIRST_TRAILING_ONE(type, suffix, width) \ - static inline int first_trailing_one##suffix(type n) { \ + static inline unsigned int first_trailing_one##suffix(type n) { \ return n ? trailing_zeros##suffix(n) + 1 : 0; \ } @@ -333,19 +399,9 @@ UINT_OVERLOADS(LEADING_ZEROS) UINT_OVERLOADS(TRAILING_ZEROS) UINT_OVERLOADS(FIRST_TRAILING_ONE) -#define ROTATE_LEFT(type, suffix, width) \ - static inline type rotate_left##suffix(type n, int c) { \ - return (n << c) | (n >> ((width - c) % width)); \ - } - -#define ROTATE_RIGHT(type, suffix, width) \ - static inline type rotate_right##suffix(type n, int c) { \ - return (n >> c) | (n << ((width - c) % width)); \ - } - #define FIRST_LEADING_ONE(type, suffix, width) \ - static inline int first_leading_one##suffix(type n) { \ - return width - leading_zeros##suffix(n); \ + static inline unsigned int first_leading_one##suffix(type n) { \ + return n ? leading_zeros##suffix(n) + 1 : 0; \ } #define HAS_SINGLE_BIT(type, suffix, width) \ @@ -354,17 +410,30 @@ UINT_OVERLOADS(FIRST_TRAILING_ONE) return n - 1 < (n ^ (n - 1)); \ } -UINT_OVERLOADS(ROTATE_LEFT) -UINT_OVERLOADS(ROTATE_RIGHT) +#define BIT_WIDTH(type, suffix, width) \ + static inline unsigned int bit_width##suffix(type n) { \ + return width - leading_zeros##suffix(n); \ + } + +#define BIT_FLOOR(type, suffix, width) \ + static inline type bit_floor##suffix(type n) { \ + return n ? (type)1 << (bit_width##suffix(n) - 1) : 0; \ + } + +#define BIT_CEIL(type, suffix, width) \ + static inline type bit_ceil##suffix(type n) { \ + return (type)1 << bit_width##suffix(n - !!n); \ + } + UINT_OVERLOADS(FIRST_LEADING_ONE) UINT_OVERLOADS(HAS_SINGLE_BIT) +UINT_OVERLOADS(BIT_WIDTH) +UINT_OVERLOADS(BIT_FLOOR) +UINT_OVERLOADS(BIT_CEIL) #define count_ones(n) UINT_SELECT(n, count_ones)(n) #define count_zeros(n) UINT_SELECT(n, count_ones)(~(n)) -#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c) -#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c) - #define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n) #define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n)) @@ -379,23 +448,26 @@ UINT_OVERLOADS(HAS_SINGLE_BIT) #define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n) -#define BIT_FLOOR(type, suffix, width) \ - static inline type bit_floor##suffix(type n) { \ - return n ? (type)1 << (first_leading_one##suffix(n) - 1) : 0; \ - } +#define bit_width(n) UINT_SELECT(n, bit_width)(n) +#define bit_floor(n) UINT_SELECT(n, bit_floor)(n) +#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n) -#define BIT_CEIL(type, suffix, width) \ - static inline type bit_ceil##suffix(type n) { \ - return (type)1 << first_leading_one##suffix(n - !!n); \ +#endif // __STDC_VERSION_STDBIT_H__ < C23 + +#define ROTATE_LEFT(type, suffix, width) \ + static inline type rotate_left##suffix(type n, int c) { \ + return (n << c) | (n >> ((width - c) % width)); \ } -UINT_OVERLOADS(BIT_FLOOR) -UINT_OVERLOADS(BIT_CEIL) +#define ROTATE_RIGHT(type, suffix, width) \ + static inline type rotate_right##suffix(type n, int c) { \ + return (n >> c) | (n << ((width - c) % width)); \ + } -#define bit_width(n) first_leading_one(n) -#define bit_floor(n) UINT_SELECT(n, bit_floor)(n) -#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n) +UINT_OVERLOADS(ROTATE_LEFT) +UINT_OVERLOADS(ROTATE_RIGHT) -#endif // __STDC_VERSION__ < C23 +#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c) +#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c) #endif // BFS_BIT_H diff --git a/src/color.c b/src/color.c index 81f28bb..a026831 100644 --- a/src/color.c +++ b/src/color.c @@ -1,9 +1,10 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "color.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "diag.h" @@ -13,6 +14,7 @@ #include "fsade.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdarg.h> @@ -29,8 +31,8 @@ struct esc_seq { /** The length of the escape sequence. */ size_t len; - /** The escape sequence iteself, without a terminating NUL. */ - char seq[]; + /** The escape sequence itself, without a terminating NUL. */ + char seq[] _counted_by(len); }; /** @@ -46,7 +48,7 @@ struct ext_color { /** Whether the comparison should be case-sensitive. */ bool case_sensitive; /** The extension to match (NUL-terminated). */ - char ext[]; + char ext[]; // _counted_by(len + 1); }; struct colors { @@ -141,13 +143,7 @@ static int init_esc(struct colors *colors, const char *name, const char *value, *field = esc; - struct trie_leaf *leaf = trie_insert_str(&colors->names, name); - if (!leaf) { - return -1; - } - - leaf->value = field; - return 0; + return trie_set_str(&colors->names, name, field); } /** Check if an escape sequence is equal to a string. */ @@ -157,8 +153,7 @@ static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) { /** Get an escape sequence from the table. */ static struct esc_seq **get_esc(const struct colors *colors, const char *name) { - const struct trie_leaf *leaf = trie_find_str(&colors->names, name); - return leaf ? leaf->value : NULL; + return trie_get_str(&colors->names, name); } /** Append an escape sequence to a string. */ @@ -214,58 +209,31 @@ static void ext_tolower(char *ext, size_t len) { } } -/** - * The "smart case" algorithm. - * - * @param ext - * The current extension being added. - * @param prev - * The previous case-sensitive match, if any, for the same extension. - * @param iprev - * The previous case-insensitive match, if any, for the same extension. - * @return - * Whether this extension should become case-sensitive. - */ -static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *prev, struct ext_color *iprev) { - // This is the first case-insensitive occurrence of this extension, e.g. - // - // *.gz=01;31:*.tar.gz=01;33 - if (!iprev) { - bfs_assert(!prev); - return false; - } - - // If the last version of this extension is already case-sensitive, - // this one should be too, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 - if (iprev->case_sensitive) { - return true; - } - - // The case matches the last occurrence exactly, e.g. - // - // *.tar.gz=01;31:*.tar.gz=01;33 - if (iprev == prev) { - return false; - } - - // Different case, but same value, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;31 - if (esc_eq(iprev->esc, ext->esc->seq, ext->esc->len)) { - return false; +/** Insert an extension into a trie. */ +static int insert_ext(struct trie *trie, struct ext_color *ext) { + // A later *.x should override any earlier *.x, *.y.x, etc. + struct trie_leaf *leaf; + while ((leaf = trie_find_postfix(trie, ext->ext))) { + trie_remove(trie, leaf); } - // Different case, different value, e.g. - // - // *.tar.gz=01;31:*.TAR.GZ=01;33 - return true; + size_t len = ext->len + 1; + return trie_set_mem(trie, ext->ext, len, ext); } /** Set the color for an extension. */ -static int set_ext(struct colors *colors, char *key, char *value) { +static int set_ext(struct colors *colors, dchar *key, dchar *value) { size_t len = dstrlen(key); + + // Embedded NUL bytes in extensions can lead to a non-prefix-free + // set of strings, e.g. {".gz", "\0.gz"} would be transformed to + // {"zg.\0", "zg.\0\0"} (showing the implicit terminating NUL). + // Our trie implementation only supports prefix-free key sets, but + // luckily '\0' cannot appear in filenames so we can ignore them. + if (memchr(key, '\0', len)) { + return 0; + } + struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1); if (!ext) { return -1; @@ -279,45 +247,19 @@ static int set_ext(struct colors *colors, char *key, char *value) { goto fail; } - key = memcpy(ext->ext, key, len + 1); + memcpy(ext->ext, key, len + 1); // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix() - ext_reverse(key, len); - - // Find any pre-existing exact match - struct ext_color *prev = NULL; - struct trie_leaf *leaf = trie_find_str(&colors->ext_trie, key); - if (leaf) { - prev = leaf->value; - trie_remove(&colors->ext_trie, leaf); - } - - // A later *.x should override any earlier *.x, *.y.x, etc. - while ((leaf = trie_find_postfix(&colors->ext_trie, key))) { - trie_remove(&colors->ext_trie, leaf); - } + ext_reverse(ext->ext, len); // Insert the extension into the case-sensitive trie - leaf = trie_insert_str(&colors->ext_trie, key); - if (!leaf) { - 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) { + if (insert_ext(&colors->ext_trie, ext) != 0) { goto fail; } - struct ext_color *iprev = leaf->value; - if (ext_case_sensitive(ext, prev, iprev)) { - iprev->case_sensitive = true; - ext->case_sensitive = true; + if (colors->ext_len < len) { + colors->ext_len = len; } - leaf->value = ext; return 0; @@ -329,32 +271,83 @@ fail: return -1; } -/** Rebuild the case-insensitive trie after all extensions have been parsed. */ -static int build_iext_trie(struct colors *colors) { - trie_clear(&colors->iext_trie); +/** + * The "smart case" algorithm. + * + * @ext + * The current extension being added. + * @iext + * The previous case-insensitive match, if any, for the same extension. + * @return + * Whether this extension should become case-sensitive. + */ +static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *iext) { + // This is the first case-insensitive occurrence of this extension, e.g. + // + // *.gz=01;31:*.tar.gz=01;33 + if (!iext) { + return false; + } + + // If the last version of this extension is already case-sensitive, + // this one should be too, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33 + if (iext->case_sensitive) { + return true; + } + + // Different case, but same value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;31 + if (esc_eq(iext->esc, ext->esc->seq, ext->esc->len)) { + return false; + } + // Different case, different value, e.g. + // + // *.tar.gz=01;31:*.TAR.GZ=01;33 + return true; +} + +/** Build the case-insensitive trie, after all extensions have been parsed. */ +static int build_iext_trie(struct colors *colors) { + // Find which extensions should be case-sensitive for_trie (leaf, &colors->ext_trie) { - size_t len = leaf->length - 1; - if (colors->ext_len < len) { - colors->ext_len = len; + struct ext_color *ext = leaf->value; + + // "Smart case": if the same extension is given with two different + // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive + ext_tolower(ext->ext, ext->len); + + size_t len = ext->len + 1; + struct trie_leaf *ileaf = trie_insert_mem(&colors->iext_trie, ext->ext, len); + if (!ileaf) { + return -1; } + struct ext_color *iext = ileaf->value; + if (ext_case_sensitive(ext, iext)) { + ext->case_sensitive = true; + iext->case_sensitive = true; + } + + ileaf->value = ext; + } + + // Rebuild the trie with only the case-insensitive ones + trie_clear(&colors->iext_trie); + + for_trie (leaf, &colors->ext_trie) { struct ext_color *ext = leaf->value; if (ext->case_sensitive) { continue; } - // set_ext() already reversed and lowercased the extension - struct trie_leaf *ileaf; - while ((ileaf = trie_find_postfix(&colors->iext_trie, ext->ext))) { - trie_remove(&colors->iext_trie, ileaf); - } - - ileaf = trie_insert_str(&colors->iext_trie, ext->ext); - if (!ileaf) { + // We already lowercased the extension above + if (insert_ext(&colors->iext_trie, ext) != 0) { return -1; } - ileaf->value = ext; } return 0; @@ -363,9 +356,8 @@ static int build_iext_trie(struct colors *colors) { /** * Find a color by an extension. */ -static const struct esc_seq *get_ext(const struct colors *colors, const char *filename) { +static const struct esc_seq *get_ext(const struct colors *colors, const char *filename, size_t name_len) { size_t ext_len = colors->ext_len; - size_t name_len = strlen(filename); if (name_len < ext_len) { ext_len = name_len; } @@ -374,7 +366,8 @@ static const struct esc_seq *get_ext(const struct colors *colors, const char *fi char buf[256]; char *copy; if (ext_len < sizeof(buf)) { - copy = memcpy(buf, suffix, ext_len + 1); + copy = memcpy(buf, suffix, ext_len); + copy[ext_len] = '\0'; } else { copy = strndup(suffix, ext_len); if (!copy) { @@ -422,13 +415,13 @@ static const struct esc_seq *get_ext(const struct colors *colors, const char *fi * * See man dir_colors. * - * @param str + * @str * A dstring to fill with the unescaped chunk. - * @param value + * @value * The value to parse. - * @param end + * @end * The character that marks the end of the chunk. - * @param[out] next + * @next[out] * Will be set to the next chunk. * @return * 0 on success, -1 on failure. @@ -583,7 +576,7 @@ static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) { break; } - if (dstrncpy(&key, chunk, equals - chunk) != 0) { + if (dstrxcpy(&key, chunk, equals - chunk) != 0) { goto fail; } if (unescape(&value, equals + 1, ':', &next) != 0) { @@ -775,23 +768,196 @@ int cfclose(CFILE *cfile) { return ret; } +bool colors_need_stat(const struct colors *colors) { + return colors->setuid || colors->setgid || colors->executable || colors->multi_hard + || colors->sticky_other_writable || colors->other_writable || colors->sticky; +} + +/** A colorable file path. */ +struct cpath { + /** The full path to color. */ + const char *path; + /** The basename offset of the last valid component. */ + size_t nameoff; + /** The end offset of the last valid component. */ + size_t valid; + /** The total length of the path. */ + size_t len; + + /** The bftw() buffer. */ + const struct BFTW *ftwbuf; + /** bfs_stat() flags for the final component. */ + enum bfs_stat_flags flags; + /** A bfs_stat() buffer, filled in when 0 < valid < len. */ + struct bfs_stat statbuf; +}; + +/** Move the valid range of a path backwards. */ +static void cpath_retreat(struct cpath *cpath) { + const char *path = cpath->path; + size_t nameoff = cpath->nameoff; + size_t valid = cpath->valid; + + if (valid > 0 && path[valid - 1] == '/') { + // Try without trailing slashes, to distinguish "notdir/" from "notdir" + do { + --valid; + } while (valid > 0 && path[valid - 1] == '/'); + + nameoff = valid; + while (nameoff > 0 && path[nameoff - 1] != '/') { + --nameoff; + } + } else { + // Remove the last component and try again + valid = nameoff; + } + + cpath->nameoff = nameoff; + cpath->valid = valid; +} + +/** Initialize a struct cpath. */ +static int cpath_init(struct cpath *cpath, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { + // Normally there are only two components to color: + // + // nameoff valid + // v v + // path/to/filename + // --------+------- + // ${di} ${fi} + // + // Error cases also usually have two components: + // + // valid, + // nameoff + // v + // path/to/nowhere + // --------+------ + // ${di} ${mi} + // + // But with ENOTDIR, there may be three: + // + // nameoff valid + // v v + // path/to/filename/nowhere + // --------+-------+------- + // ${di} ${fi} ${mi} + + cpath->path = path; + cpath->len = strlen(path); + cpath->ftwbuf = ftwbuf; + cpath->flags = flags; + + cpath->valid = cpath->len; + if (path == ftwbuf->path) { + cpath->nameoff = ftwbuf->nameoff; + } else { + cpath->nameoff = xbaseoff(path); + } + + if (bftw_type(ftwbuf, flags) != BFS_ERROR) { + return 0; + } + + cpath_retreat(cpath); + + // Find the base path. For symlinks like + // + // path/to/symlink -> nested/file + // + // this will be something like + // + // path/to/nested/file + int at_fd = AT_FDCWD; + dchar *at_path = NULL; + if (path == ftwbuf->path) { + if (ftwbuf->depth > 0) { + // The parent must have existed to get here + return 0; + } + } else { + // We're in print_link_target(), so resolve relative to the link's parent directory + at_fd = ftwbuf->at_fd; + if (at_fd == (int)AT_FDCWD && path[0] != '/') { + at_path = dstrxdup(ftwbuf->path, ftwbuf->nameoff); + if (!at_path) { + return -1; + } + } + } + + if (!at_path) { + at_path = dstralloc(cpath->valid); + if (!at_path) { + return -1; + } + } + if (dstrxcat(&at_path, path, cpath->valid) != 0) { + dstrfree(at_path); + return -1; + } + + size_t at_off = dstrlen(at_path) - cpath->valid; + + // Find the longest valid path prefix + while (cpath->valid > 0) { + if (bfs_stat(at_fd, at_path, BFS_STAT_FOLLOW, &cpath->statbuf) == 0) { + break; + } + + cpath_retreat(cpath); + dstrshrink(at_path, at_off + cpath->valid); + } + + dstrfree(at_path); + return 0; +} + +/** Get the bfs_stat() buffer for the last valid component. */ +static const struct bfs_stat *cpath_stat(const struct cpath *cpath) { + if (cpath->valid == cpath->len) { + return bftw_stat(cpath->ftwbuf, cpath->flags); + } else { + return &cpath->statbuf; + } +} + +/** Check if a path has non-trivial capabilities. */ +static bool cpath_has_capabilities(const struct cpath *cpath) { + if (cpath->valid == cpath->len) { + return bfs_check_capabilities(cpath->ftwbuf) > 0; + } else { + // TODO: implement capability checks for arbitrary paths + return false; + } +} + /** Check if a symlink is broken. */ -static bool is_link_broken(const struct BFTW *ftwbuf) { +static bool cpath_is_broken(const struct cpath *cpath) { + if (cpath->valid < cpath->len) { + // A valid parent can't be a broken link + return false; + } + + const struct BFTW *ftwbuf = cpath->ftwbuf; if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) { return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0; } else { + // A link encountered with BFS_STAT_TRYFOLLOW must be broken return true; } } -bool colors_need_stat(const struct colors *colors) { - return colors->setuid || colors->setgid || colors->executable || colors->multi_hard - || colors->sticky_other_writable || colors->other_writable || colors->sticky; -} - /** Get the color for a file. */ -static const struct esc_seq *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - enum bfs_type type = bftw_type(ftwbuf, flags); +static const struct esc_seq *file_color(const struct colors *colors, const struct cpath *cpath) { + enum bfs_type type; + if (cpath->valid == cpath->len) { + type = bftw_type(cpath->ftwbuf, cpath->flags); + } else { + type = bfs_mode_to_type(cpath->statbuf.mode); + } + if (type == BFS_ERROR) { goto error; } @@ -802,7 +968,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char switch (type) { case BFS_REG: if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) { - statbuf = bftw_stat(ftwbuf, flags); + statbuf = cpath_stat(cpath); if (!statbuf) { goto error; } @@ -812,7 +978,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char color = colors->setuid; } else if (colors->setgid && (statbuf->mode & 02000)) { color = colors->setgid; - } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) { + } else if (colors->capable && cpath_has_capabilities(cpath)) { color = colors->capable; } else if (colors->executable && (statbuf->mode & 00111)) { color = colors->executable; @@ -821,7 +987,9 @@ static const struct esc_seq *file_color(const struct colors *colors, const char } if (!color) { - color = get_ext(colors, filename); + const char *name = cpath->path + cpath->nameoff; + size_t namelen = cpath->valid - cpath->nameoff; + color = get_ext(colors, name, namelen); } if (!color) { @@ -832,7 +1000,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char case BFS_DIR: if (colors->sticky_other_writable || colors->other_writable || colors->sticky) { - statbuf = bftw_stat(ftwbuf, flags); + statbuf = cpath_stat(cpath); if (!statbuf) { goto error; } @@ -851,7 +1019,7 @@ static const struct esc_seq *file_color(const struct colors *colors, const char break; case BFS_LNK: - if (colors->orphan && is_link_broken(ftwbuf)) { + if (colors->orphan && cpath_is_broken(cpath)) { color = colors->orphan; } else { color = colors->link; @@ -938,6 +1106,10 @@ static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flag /** Print a string with an optional color. */ static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) { + if (len == 0) { + return 0; + } + if (print_esc(cfile, esc) != 0) { return -1; } @@ -954,112 +1126,42 @@ static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *st return 0; } -/** Find the offset of the first broken path component. */ -static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) { - ssize_t ret = max; - bfs_assert(ret >= 0); - - if (bftw_type(ftwbuf, flags) != BFS_ERROR) { - goto out; - } - - dchar *at_path; - int at_fd; - if (path == ftwbuf->path) { - if (ftwbuf->depth == 0) { - at_fd = AT_FDCWD; - at_path = dstrndup(path, max); - } else { - // The parent must have existed to get here - goto out; - } - } else { - // We're in print_link_target(), so resolve relative to the link's parent directory - at_fd = ftwbuf->at_fd; - if (at_fd == (int)AT_FDCWD && path[0] != '/') { - at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff); - if (at_path && dstrncat(&at_path, path, max) != 0) { - ret = -1; - goto out_path; - } - } else { - at_path = dstrndup(path, max); - } - } - - if (!at_path) { - ret = -1; - goto out; - } - - while (ret > 0) { - if (xfaccessat(at_fd, at_path, F_OK) == 0) { - break; - } - - size_t len = dstrlen(at_path); - while (ret && at_path[len - 1] == '/') { - --len, --ret; - } - if (errno != ENOTDIR) { - while (ret && at_path[len - 1] != '/') { - --len, --ret; - } - } - - dstresize(&at_path, len); - } - -out_path: - dstrfree(at_path); -out: - return ret; -} - /** Print a path with colors. */ static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - size_t nameoff; - if (path == ftwbuf->path) { - nameoff = ftwbuf->nameoff; - } else { - nameoff = xbaseoff(path); - } - - const char *name = path + nameoff; - size_t pathlen = nameoff + strlen(name); - - ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff); - if (broken < 0) { + struct cpath cpath; + if (cpath_init(&cpath, path, ftwbuf, flags) != 0) { return -1; } - size_t split = broken; const struct colors *colors = cfile->colors; const struct esc_seq *dirs_color = colors->directory; - const struct esc_seq *name_color; + const struct esc_seq *name_color = NULL; + const struct esc_seq *err_color = colors->missing; + if (!err_color) { + err_color = colors->orphan; + } - if (split < nameoff) { - name_color = colors->missing; - if (!name_color) { - name_color = colors->orphan; - } - } else { - name_color = file_color(cfile->colors, path + nameoff, ftwbuf, flags); + if (cpath.nameoff < cpath.valid) { + name_color = file_color(colors, &cpath); if (name_color == dirs_color) { - split = pathlen; + cpath.nameoff = cpath.valid; } } - if (split > 0) { - if (print_colored(cfile, dirs_color, path, split) != 0) { - return -1; - } + if (print_colored(cfile, dirs_color, path, cpath.nameoff) != 0) { + return -1; } - if (split < pathlen) { - if (print_colored(cfile, name_color, path + split, pathlen - split) != 0) { - return -1; - } + const char *name = path + cpath.nameoff; + size_t name_len = cpath.valid - cpath.nameoff; + if (print_colored(cfile, name_color, name, name_len) != 0) { + return -1; + } + + const char *tail = path + cpath.valid; + size_t tail_len = cpath.len - cpath.valid; + if (print_colored(cfile, err_color, tail, tail_len) != 0) { + return -1; } return 0; @@ -1067,8 +1169,18 @@ static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW /** Print a file name with colors. */ static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) { - const struct esc_seq *esc = file_color(cfile->colors, name, ftwbuf, flags); - return print_colored(cfile, esc, name, strlen(name)); + size_t len = strlen(name); + const struct cpath cpath = { + .path = name, + .nameoff = 0, + .valid = len, + .len = len, + .ftwbuf = ftwbuf, + .flags = flags, + }; + + const struct esc_seq *esc = file_color(cfile->colors, &cpath); + return print_colored(cfile, esc, name, cpath.len); } /** Print the name of a file with the appropriate colors. */ @@ -1125,9 +1237,36 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) { } /** Format some colored output to the buffer. */ -attr(printf(2, 3)) +_printf(2, 3) static int cbuff(CFILE *cfile, const char *format, ...); +/** Print an expression's name, for diagnostics. */ +static int print_expr_name(CFILE *cfile, const struct bfs_expr *expr) { + switch (expr->kind) { + case BFS_FLAG: + return cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]); + case BFS_OPERATOR: + return cbuff(cfile, "${red}%pq${rs}", expr->argv[0]); + default: + return cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]); + } +} + +/** Print an expression's args, for diagnostics. */ +static int print_expr_args(CFILE *cfile, const struct bfs_expr *expr) { + if (print_expr_name(cfile, expr) != 0) { + return -1; + } + + for (size_t i = 1; i < expr->argc; ++i) { + if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { + return -1; + } + } + + return 0; +} + /** Dump a parsed expression tree, for debugging. */ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) { if (depth >= 2) { @@ -1142,20 +1281,8 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i return -1; } - if (bfs_expr_is_parent(expr)) { - if (cbuff(cfile, "${red}%pq${rs}", expr->argv[0]) < 0) { - return -1; - } - } else { - if (cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]) < 0) { - return -1; - } - } - - for (size_t i = 1; i < expr->argc; ++i) { - if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) { - return -1; - } + if (print_expr_args(cfile, expr) != 0) { + return -1; } if (verbose) { @@ -1194,7 +1321,7 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i return 0; } -attr(printf(2, 0)) +_printf(2, 0) static int cvbuff(CFILE *cfile, const char *format, va_list args) { const struct colors *colors = cfile->colors; @@ -1206,7 +1333,7 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { for (const char *i = format; *i; ++i) { size_t verbatim = strcspn(i, "%$"); - if (dstrncat(&cfile->buffer, i, verbatim) != 0) { + if (dstrxcat(&cfile->buffer, i, verbatim) != 0) { return -1; } i += verbatim; @@ -1295,6 +1422,16 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) { return -1; } break; + case 'x': + if (print_expr_args(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; + case 'X': + if (print_expr_name(cfile, va_arg(args, const struct bfs_expr *)) != 0) { + return -1; + } + break; default: goto invalid; @@ -1387,7 +1524,7 @@ int cvfprintf(CFILE *cfile, const char *format, va_list args) { } } - dstresize(&cfile->buffer, 0); + dstrshrink(cfile->buffer, 0); return ret; } diff --git a/src/color.h b/src/color.h index 8582eb3..aac8b33 100644 --- a/src/color.h +++ b/src/color.h @@ -8,8 +8,9 @@ #ifndef BFS_COLOR_H #define BFS_COLOR_H -#include "prelude.h" +#include "bfs.h" #include "dstring.h" + #include <stdio.h> /** @@ -53,11 +54,11 @@ typedef struct CFILE { /** * Wrap an existing file into a colored stream. * - * @param file + * @file * The underlying file. - * @param colors + * @colors * The color table to use if file is a TTY. - * @param close + * @close * Whether to close the underlying stream when this stream is closed. * @return * A colored wrapper around file. @@ -67,7 +68,7 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close); /** * Close a colored file. * - * @param cfile + * @cfile * The colored file to close. * @return * 0 on success, -1 on failure. @@ -77,9 +78,9 @@ int cfclose(CFILE *cfile); /** * Colored, formatted output. * - * @param cfile + * @cfile * The colored stream to print to. - * @param format + * @format * A printf()-style format string, supporting these format specifiers: * * %c: A single character @@ -94,19 +95,21 @@ int cfclose(CFILE *cfile); * %pL: A colored link target, from a const struct BFTW * argument * %pe: Dump a const struct bfs_expr *, for debugging. * %pE: Dump a const struct bfs_expr * in verbose form, for debugging. + * %px: Print a const struct bfs_expr * with syntax highlighting. + * %pX: Print the name of a const struct bfs_expr *, without arguments. * %%: A literal '%' * ${cc}: Change the color to 'cc' * $$: A literal '$' * @return * 0 on success, -1 on failure. */ -attr(printf(2, 3)) +_printf(2, 3) int cfprintf(CFILE *cfile, const char *format, ...); /** * cfprintf() variant that takes a va_list. */ -attr(printf(2, 0)) +_printf(2, 0) int cvfprintf(CFILE *cfile, const char *format, va_list args); /** @@ -2,6 +2,7 @@ // SPDX-License-Identifier: 0BSD #include "ctx.h" + #include "alloc.h" #include "bfstd.h" #include "color.h" @@ -13,28 +14,16 @@ #include "sighook.h" #include "stat.h" #include "trie.h" -#include "xtime.h" + #include <errno.h> #include <limits.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> +#include <sys/stat.h> +#include <time.h> #include <unistd.h> -/** Get the initial value for ctx->threads (-j). */ -static int bfs_nproc(void) { - long nproc = xsysconf(_SC_NPROCESSORS_ONLN); - - if (nproc < 1) { - nproc = 1; - } else if (nproc > 8) { - // Not much speedup after 8 threads - nproc = 8; - } - - return nproc; -} - struct bfs_ctx *bfs_ctx_new(void) { struct bfs_ctx *ctx = ZALLOC(struct bfs_ctx); if (!ctx) { @@ -47,11 +36,19 @@ struct bfs_ctx *bfs_ctx_new(void) { ctx->maxdepth = INT_MAX; ctx->flags = BFTW_RECOVER; ctx->strategy = BFTW_BFS; - ctx->threads = bfs_nproc(); ctx->optlevel = 3; + ctx->threads = nproc(); + if (ctx->threads > 8) { + // Not much speedup after 8 threads + ctx->threads = 8; + } + trie_init(&ctx->files); + ctx->umask = umask(0); + umask(ctx->umask); + if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) { goto fail; } @@ -68,7 +65,7 @@ struct bfs_ctx *bfs_ctx_new(void) { goto fail; } - if (xgettime(&ctx->now) != 0) { + if (clock_gettime(CLOCK_REALTIME, &ctx->now) != 0) { goto fail; } @@ -289,6 +286,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) { } free(ctx->paths); + free(ctx->kinds); free(ctx->argv); free(ctx); } @@ -8,14 +8,15 @@ #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 <stddef.h> #include <sys/resource.h> +#include <sys/types.h> #include <time.h> struct CFILE; @@ -28,6 +29,8 @@ struct bfs_ctx { size_t argc; /** The unparsed command line arguments. */ char **argv; + /** The argument token kinds. */ + enum bfs_kind *kinds; /** The root paths. */ const char **paths; @@ -67,11 +70,18 @@ struct bfs_ctx { bool status; /** Whether to only return unique files (-unique). */ bool unique; - /** Whether to print warnings (-warn/-nowarn). */ - bool warn; /** Whether to only handle paths with xargs-safe characters (-X). */ bool xargs_safe; + /** Whether bfs was run interactively. */ + bool interactive; + /** Whether to print warnings (-warn/-nowarn). */ + bool warn; + /** Whether to report errors (-noerror). */ + bool ignore_errors; + /** Whether any dangerous actions (-delete/-exec) are present. */ + bool dangerous; + /** Color data. */ struct colors *colors; /** The error that occurred parsing the color table, if any. */ @@ -98,6 +108,9 @@ struct bfs_ctx { /** The number of files owned by the context. */ int nfiles; + /** The current file creation mask. */ + mode_t umask; + /** The initial RLIMIT_NOFILE limits. */ struct rlimit orig_nofile; /** The current RLIMIT_NOFILE limits. */ @@ -118,7 +131,7 @@ struct bfs_ctx *bfs_ctx_new(void); /** * Get the mount table. * - * @param ctx + * @ctx * The bfs context. * @return * The cached mount table, or NULL on failure. @@ -128,11 +141,11 @@ const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx); /** * Deduplicate an opened file. * - * @param ctx + * @ctx * The bfs context. - * @param cfile + * @cfile * The opened file. - * @param path + * @path * The path to the opened file (or NULL for standard streams). * @return * If the same file was opened previously, that file is returned. If cfile is a new file, @@ -143,7 +156,7 @@ struct CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, struct CFILE *cfile, const char /** * Flush any caches for consistency with external processes. * - * @param ctx + * @ctx * The bfs context. */ void bfs_ctx_flush(const struct bfs_ctx *ctx); @@ -151,9 +164,9 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx); /** * Dump the parsed command line. * - * @param ctx + * @ctx * The bfs context. - * @param flag + * @flag * The -D flag that triggered the dump. */ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag); @@ -161,7 +174,7 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag); /** * Free a bfs context. * - * @param ctx + * @ctx * The context to free. * @return * 0 on success, -1 if any errors occurred. @@ -1,38 +1,43 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "diag.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "color.h" #include "ctx.h" #include "dstring.h" #include "expr.h" -#include <errno.h> + #include <stdarg.h> #include <stdio.h> #include <stdlib.h> - -/** bfs_diagf() implementation. */ -attr(printf(2, 0)) -static void bfs_vdiagf(const struct bfs_loc *loc, const char *format, va_list args) { - fprintf(stderr, "%s: %s@%s:%d: ", xgetprogname(), loc->func, loc->file, loc->line); - vfprintf(stderr, format, args); - fprintf(stderr, "\n"); -} - -void bfs_diagf(const struct bfs_loc *loc, const char *format, ...) { +#include <unistd.h> + +/** + * Print an error using dprintf() if possible, because it's more likely to be + * async-signal-safe in practice. + */ +#if BFS_HAS_DPRINTF +# define veprintf(...) vdprintf(STDERR_FILENO, __VA_ARGS__) +#else +# define veprintf(...) vfprintf(stderr, __VA_ARGS__) +#endif + +void bfs_diagf(const char *format, ...) { va_list args; va_start(args, format); - bfs_vdiagf(loc, format, args); + veprintf(format, args); va_end(args); } -noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...) { +_noreturn +void bfs_abortf(const char *format, ...) { va_list args; va_start(args, format); - bfs_vdiagf(loc, format, args); + veprintf(format, args); va_end(args); abort(); @@ -8,82 +8,74 @@ #ifndef BFS_DIAG_H #define BFS_DIAG_H -#include "prelude.h" +#include "bfs.h" #include "bfstd.h" -#include <stdarg.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 +#include <stdarg.h> /** - * A source code location. + * Wrap a diagnostic format string so it looks like + * + * bfs: func@src/file.c:0: Message */ -struct bfs_loc { - const char *file; - int line; - const char *func; -}; - -#define BFS_LOC_INIT { .file = __FILE__, .line = __LINE__, .func = __func__ } +#define BFS_DIAG_FORMAT_(format) \ + ((format) ? "%s: %s@%s:%d: " format "%s" : "") /** - * Get the current source code location. + * Add arguments to match a BFS_DIAG_FORMAT string. */ -#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 +#define BFS_DIAG_ARGS_(...) \ + xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ "\n" /** - * Print a low-level diagnostic message to standard error, formatted like - * - * bfs: func@src/file.c:0: Message + * Print a low-level diagnostic message to standard error. */ -attr(printf(2, 3)) -void bfs_diagf(const struct bfs_loc *loc, const char *format, ...); +_printf(1, 2) +void bfs_diagf(const char *format, ...); /** * Unconditional diagnostic message. */ -#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__) +#define bfs_diag(...) \ + bfs_diag_(__VA_ARGS__, ) + +#define bfs_diag_(format, ...) \ + bfs_diagf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__)) /** * Print a diagnostic message including the last error. */ #define bfs_ediag(...) \ - bfs_ediag_("" __VA_ARGS__, errstr()) + bfs_ediag_(__VA_ARGS__, ) #define bfs_ediag_(format, ...) \ - bfs_diag(sizeof(format) > 1 ? format ": %s" : "%s", __VA_ARGS__) + bfs_diag_(format "%s%s", __VA_ARGS__ (sizeof("" format) > 1 ? ": " : ""), errstr(), ) /** * Print a message to standard error and abort. */ -attr(cold, printf(2, 3)) -noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...); +_cold +_printf(1, 2) +_noreturn +void bfs_abortf(const char *format, ...); /** * Unconditional abort with a message. */ #define bfs_abort(...) \ - bfs_abortf(bfs_location(), __VA_ARGS__) + bfs_abort_(__VA_ARGS__, ) + +#define bfs_abort_(format, ...) \ + bfs_abortf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__)) /** * Abort with a message including the last error. */ #define bfs_eabort(...) \ - bfs_eabort_("" __VA_ARGS__, errstr()) + bfs_eabort_(__VA_ARGS__, ) #define bfs_eabort_(format, ...) \ - bfs_abort(sizeof(format) > 1 ? format ": %s" : "%s", __VA_ARGS__) + ((format) ? bfs_abort_(format ": %s", __VA_ARGS__ errstr(), ) : (void)0) /** * Abort in debug builds; no-op in release builds. @@ -97,30 +89,43 @@ noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...); #endif /** + * Get the default assertion message, if no format string was specified. + */ +#define BFS_DIAG_MSG_(format, str) \ + (sizeof(format) > 1 ? "" : str) + +/** * Unconditional assert. */ #define bfs_verify(...) \ - bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", "") + bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", ) #define bfs_verify_(str, cond, format, ...) \ - ((cond) ? (void)0 : bfs_abort( \ + ((cond) ? (void)0 : bfs_verify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__)) + +#define bfs_verify__(format, ...) \ + bfs_abortf( \ sizeof(format) > 1 \ - ? "%.0s" format "%s%s" \ - : "Assertion failed: `%s`%s", \ - str, __VA_ARGS__)) + ? BFS_DIAG_FORMAT_("%s" format "%s") \ + : BFS_DIAG_FORMAT_("Assertion failed: `%s`"), \ + BFS_DIAG_ARGS_(__VA_ARGS__)) /** * Unconditional assert, including the last error. */ #define bfs_everify(...) \ - bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", errstr()) + bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", ) + #define bfs_everify_(str, cond, format, ...) \ - ((cond) ? (void)0 : bfs_abort( \ + ((cond) ? (void)0 : bfs_everify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__)) + +#define bfs_everify__(format, ...) \ + bfs_abortf( \ sizeof(format) > 1 \ - ? "%.0s" format "%s: %s" \ - : "Assertion failed: `%s`: %s", \ - str, __VA_ARGS__)) + ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \ + : BFS_DIAG_FORMAT_("Assertion failed: `%s`: %s"), \ + BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), )) /** * Assert in debug builds; no-op in release builds. @@ -166,13 +171,14 @@ const char *debug_flag_name(enum debug_flags flag); /** * Like perror(), but decorated like bfs_error(). */ -attr(cold) +_cold void bfs_perror(const struct bfs_ctx *ctx, const char *str); /** * Shorthand for printing error messages. */ -attr(cold, printf(2, 3)) +_cold +_printf(2, 3) void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -180,7 +186,8 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a warning was printed. */ -attr(cold, printf(2, 3)) +_cold +_printf(2, 3) bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); /** @@ -188,67 +195,71 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...); * * @return Whether a debug message was printed. */ -attr(cold, printf(3, 4)) +_cold +_printf(3, 4) bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...); /** * bfs_error() variant that takes a va_list. */ -attr(cold, printf(2, 0)) +_cold +_printf(2, 0) void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_warning() variant that takes a va_list. */ -attr(cold, printf(2, 0)) +_cold +_printf(2, 0) bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args); /** * bfs_debug() variant that takes a va_list. */ -attr(cold, printf(3, 0)) +_cold +_printf(3, 0) bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args); /** * Print the error message prefix. */ -attr(cold) +_cold void bfs_error_prefix(const struct bfs_ctx *ctx); /** * Print the warning message prefix. */ -attr(cold) +_cold bool bfs_warning_prefix(const struct bfs_ctx *ctx); /** * Print the debug message prefix. */ -attr(cold) +_cold bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag); /** * Highlight parts of the command line in an error message. */ -attr(cold) +_cold void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in an error message. */ -attr(cold) +_cold void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr); /** * Highlight parts of the command line in a warning message. */ -attr(cold) +_cold bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]); /** * Highlight parts of an expression in a warning message. */ -attr(cold) +_cold bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr); #endif // BFS_DIAG_H @@ -1,13 +1,15 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "dir.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "sanity.h" #include "trie.h" + #include <dirent.h> #include <errno.h> #include <fcntl.h> @@ -8,7 +8,8 @@ #ifndef BFS_DIR_H #define BFS_DIR_H -#include "prelude.h" +#include "bfs.h" + #include <sys/types.h> /** @@ -20,6 +21,8 @@ # define BFS_USE_GETDENTS true # elif __linux__ || __FreeBSD__ # define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL) +# else +# define BFS_USE_GETDENTS false # endif #endif @@ -86,7 +89,7 @@ struct arena; /** * Initialize an arena for directories. * - * @param arena + * @arena * The arena to initialize. */ void bfs_dir_arena(struct arena *arena); @@ -104,14 +107,14 @@ enum bfs_dir_flags { /** * Open a directory. * - * @param dir + * @dir * The allocated directory. - * @param at_fd + * @at_fd * The base directory for path resolution. - * @param at_path + * @at_path * The path of the directory to open, relative to at_fd. Pass NULL to * open at_fd itself. - * @param flags + * @flags * Flags that control which directory entries are listed. * @return * 0 on success, or -1 on failure. @@ -126,7 +129,7 @@ int bfs_dirfd(const struct bfs_dir *dir); /** * Performs any I/O necessary for the next bfs_readdir() call. * - * @param dir + * @dir * The directory to poll. * @return * 1 on success, 0 on EOF, or -1 on failure. @@ -136,9 +139,9 @@ int bfs_polldir(struct bfs_dir *dir); /** * Read a directory entry. * - * @param dir + * @dir * The directory to read. - * @param[out] dirent + * @dirent[out] * The directory entry to populate. * @return * 1 on success, 0 on EOF, or -1 on failure. @@ -164,7 +167,7 @@ int bfs_closedir(struct bfs_dir *dir); /** * Detach the file descriptor from an open directory. * - * @param dir + * @dir * The directory to detach. * @return * The file descriptor of the directory. diff --git a/src/dstring.c b/src/dstring.c index 86ab646..678d685 100644 --- a/src/dstring.c +++ b/src/dstring.c @@ -1,11 +1,12 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "dstring.h" + #include "alloc.h" #include "bit.h" #include "diag.h" + #include <stdarg.h> #include <stddef.h> #include <stdint.h> @@ -22,7 +23,7 @@ struct dstring { /** Length of the string, *excluding* the terminating NUL. */ size_t len; /** The string itself. */ - alignas(dchar) char str[]; + alignas(dchar) char str[] _counted_by(cap); }; #define DSTR_OFFSET offsetof(struct dstring, str) @@ -45,6 +46,13 @@ static dchar *dstrdata(struct dstring *header) { return (char *)header + DSTR_OFFSET; } +/** Set the length of a dynamic string. */ +static void dstrsetlen(struct dstring *header, size_t len) { + bfs_assert(len < header->cap); + header->len = len; + header->str[len] = '\0'; +} + /** Allocate a dstring with the given contents. */ static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) { // Avoid reallocations for small strings @@ -58,11 +66,10 @@ static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) { } header->cap = cap; - header->len = len; + dstrsetlen(header, len); - char *ret = dstrdata(header); + dchar *ret = dstrdata(header); memcpy(ret, str, len); - ret[len] = '\0'; return ret; } @@ -120,11 +127,16 @@ int dstresize(dchar **dstr, size_t len) { } struct dstring *header = dstrheader(*dstr); - header->len = len; - header->str[len] = '\0'; + dstrsetlen(header, len); return 0; } +void dstrshrink(dchar *dstr, size_t len) { + struct dstring *header = dstrheader(dstr); + bfs_assert(len <= header->len); + dstrsetlen(header, len); +} + int dstrcat(dchar **dest, const char *src) { return dstrxcat(dest, src, strlen(src)); } @@ -277,3 +289,20 @@ void dstrfree(dchar *dstr) { free(dstrheader(dstr)); } } + +dchar *dstrepeat(const char *str, size_t n) { + size_t len = strlen(str); + dchar *ret = dstralloc(n * len); + if (!ret) { + return NULL; + } + + for (size_t i = 0; i < n; ++i) { + if (dstrxcat(&ret, str, len) < 0) { + dstrfree(ret); + return NULL; + } + } + + return ret; +} diff --git a/src/dstring.h b/src/dstring.h index 14e1d3e..ce7ef86 100644 --- a/src/dstring.h +++ b/src/dstring.h @@ -8,8 +8,9 @@ #ifndef BFS_DSTRING_H #define BFS_DSTRING_H -#include "prelude.h" +#include "bfs.h" #include "bfstd.h" + #include <stdarg.h> #include <stddef.h> @@ -30,7 +31,7 @@ typedef char dchar; /** * Free a dynamic string. * - * @param dstr + * @dstr * The string to free. */ void dstrfree(dchar *dstr); @@ -38,56 +39,56 @@ void dstrfree(dchar *dstr); /** * Allocate a dynamic string. * - * @param cap + * @cap * The initial capacity of the string. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstralloc(size_t cap); /** * Create a dynamic copy of a string. * - * @param str + * @str * The NUL-terminated string to copy. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrdup(const char *str); /** * Create a length-limited dynamic copy of a string. * - * @param str + * @str * The string to copy. - * @param n + * @n * The maximum number of characters to copy from str. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrndup(const char *str, size_t n); /** * Create a dynamic copy of a dynamic string. * - * @param dstr + * @dstr * The dynamic string to copy. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrddup(const dchar *dstr); /** * Create an exact-sized dynamic copy of a string. * - * @param str + * @str * The string to copy. - * @param len + * @len * The length of the string, which may include internal NUL bytes. */ -attr(malloc(dstrfree, 1)) +_malloc(dstrfree, 1) dchar *dstrxdup(const char *str, size_t len); /** * Get a dynamic string's length. * - * @param dstr + * @dstr * The string to measure. * @return * The length of dstr. @@ -97,9 +98,9 @@ size_t dstrlen(const dchar *dstr); /** * Reserve some capacity in a dynamic string. * - * @param dstr + * @dstr * The dynamic string to preallocate. - * @param cap + * @cap * The new capacity for the string. * @return * 0 on success, -1 on failure. @@ -109,214 +110,246 @@ int dstreserve(dchar **dstr, size_t cap); /** * Resize a dynamic string. * - * @param dstr + * @dstr * The dynamic string to resize. - * @param len + * @len * The new length for the dynamic string. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstresize(dchar **dstr, size_t len); /** + * Shrink a dynamic string. + * + * @dstr + * The dynamic string to shrink. + * @len + * The new length. Must not be greater than the current length. + */ +void dstrshrink(dchar *dstr, size_t len); + +/** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. * @return 0 on success, -1 on failure. */ +_nodiscard int dstrcat(dchar **dest, const char *src); /** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. - * @param n + * @n * The maximum number of characters to take from src. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrncat(dchar **dest, const char *src, size_t n); /** * Append a dynamic string to another dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to append. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrdcat(dchar **dest, const dchar *src); /** * Append to a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to append. - * @param len + * @len * The exact number of characters to take from src. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrxcat(dchar **dest, const char *src, size_t len); /** * Append a single character to a dynamic string. * - * @param str + * @str * The string to append to. - * @param c + * @c * The character to append. * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrapp(dchar **str, char c); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The string to copy. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrcpy(dchar **dest, const char *str); /** * Copy a dynamic string into another one. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrdcpy(dchar **dest, const dchar *str); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. - * @param n + * @n * The maximum number of characters to take from src. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrncpy(dchar **dest, const char *str, size_t n); /** * Copy a string into a dynamic string. * - * @param dest + * @dest * The destination dynamic string. - * @param src + * @src * The dynamic string to copy. - * @param len + * @len * The exact number of characters to take from src. * @returns * 0 on success, -1 on failure. */ +_nodiscard int dstrxcpy(dchar **dest, const char *str, size_t len); /** * Create a dynamic string from a format string. * - * @param format + * @format * The format string to fill in. - * @param ... + * @... * Any arguments for the format string. * @return * The created string, or NULL on failure. */ -attr(printf(1, 2)) +_nodiscard +_printf(1, 2) dchar *dstrprintf(const char *format, ...); /** * Create a dynamic string from a format string and a va_list. * - * @param format + * @format * The format string to fill in. - * @param args + * @args * The arguments for the format string. * @return * The created string, or NULL on failure. */ -attr(printf(1, 0)) +_nodiscard +_printf(1, 0) dchar *dstrvprintf(const char *format, va_list args); /** * Format some text onto the end of a dynamic string. * - * @param str + * @str * The destination dynamic string. - * @param format + * @format * The format string to fill in. - * @param ... + * @... * Any arguments for the format string. * @return * 0 on success, -1 on failure. */ -attr(printf(2, 3)) +_nodiscard +_printf(2, 3) int dstrcatf(dchar **str, const char *format, ...); /** * Format some text from a va_list onto the end of a dynamic string. * - * @param str + * @str * The destination dynamic string. - * @param format + * @format * The format string to fill in. - * @param args + * @args * The arguments for the format string. * @return * 0 on success, -1 on failure. */ -attr(printf(2, 0)) +_nodiscard +_printf(2, 0) int dstrvcatf(dchar **str, const char *format, va_list args); /** * Concatenate while shell-escaping. * - * @param dest + * @dest * The destination dynamic string. - * @param str + * @str * The string to escape. - * @param flags + * @flags * Flags for wordesc(). * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrescat(dchar **dest, const char *str, enum wesc_flags flags); /** * Concatenate while shell-escaping. * - * @param dest + * @dest * The destination dynamic string. - * @param str + * @str * The string to escape. - * @param n + * @n * The maximum length of the string. - * @param flags + * @flags * Flags for wordesc(). * @return * 0 on success, -1 on failure. */ +_nodiscard int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags); +/** + * Repeat a string n times. + */ +_nodiscard +dchar *dstrepeat(const char *str, size_t n); + #endif // BFS_DSTRING_H @@ -5,9 +5,11 @@ * Implementation of all the primary expressions. */ -#include "prelude.h" #include "eval.h" + +#include "atomic.h" #include "bar.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" @@ -22,14 +24,18 @@ #include "printf.h" #include "pwcache.h" #include "sanity.h" +#include "sighook.h" #include "stat.h" #include "trie.h" #include "xregex.h" +#include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <fnmatch.h> #include <grp.h> #include <pwd.h> +#include <signal.h> #include <stdarg.h> #include <stdint.h> #include <stdio.h> @@ -37,6 +43,7 @@ #include <string.h> #include <strings.h> #include <sys/resource.h> +#include <sys/time.h> #include <sys/types.h> #include <time.h> #include <unistd.h> @@ -51,6 +58,8 @@ struct bfs_eval { enum bftw_action action; /** The bfs_eval() return value. */ int *ret; + /** The number of errors that have occurred. */ + size_t *nerrors; /** Whether to quit immediately. */ bool quit; }; @@ -58,12 +67,18 @@ struct bfs_eval { /** * Print an error message. */ -attr(printf(2, 3)) +_printf(2, 3) static void eval_error(struct bfs_eval *state, const char *format, ...) { + const struct bfs_ctx *ctx = state->ctx; + + ++*state->nerrors; + if (ctx->ignore_errors) { + return; + } + // By POSIX, any errors should be accompanied by a non-zero exit status *state->ret = EXIT_FAILURE; - const struct bfs_ctx *ctx = state->ctx; CFILE *cerr = ctx->cerr; bfs_error(ctx, "%pP: ", state->ftwbuf); @@ -122,11 +137,9 @@ static const struct bfs_stat *eval_stat(struct bfs_eval *state) { * Get the difference (in seconds) between two struct timespecs. */ static time_t timespec_diff(const struct timespec *lhs, const struct timespec *rhs) { - time_t ret = lhs->tv_sec - rhs->tv_sec; - if (lhs->tv_nsec < rhs->tv_nsec) { - --ret; - } - return ret; + struct timespec diff = *lhs; + timespec_sub(&diff, rhs); + return diff.tv_sec; } bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) { @@ -245,8 +258,7 @@ bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state) { return false; } - return time->tv_sec > expr->reftime.tv_sec - || (time->tv_sec == expr->reftime.tv_sec && time->tv_nsec > expr->reftime.tv_nsec); + return timespec_cmp(time, &expr->reftime) > 0; } /** @@ -267,10 +279,10 @@ bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) { switch (expr->time_unit) { case BFS_DAYS: diff /= 60 * 24; - fallthru; + _fallthrough; case BFS_MINUTES: diff /= 60; - fallthru; + _fallthrough; case BFS_SECONDS: break; } @@ -396,7 +408,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c if (expr->eval_fn == eval_exec) { if (bfs_exec_finish(expr->exec) != 0) { if (errno != 0) { - bfs_error(ctx, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr()); + bfs_error(ctx, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } ret = -1; } @@ -417,7 +429,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) { bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0; if (errno != 0) { - eval_error(state, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr()); + eval_error(state, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr()); } return ret; } @@ -688,6 +700,34 @@ static int print_owner(FILE *file, const char *name, uintmax_t id, int *width) { } } +/** Print a file's modification time. */ +static int print_time(FILE *file, time_t time, time_t now) { + struct tm tm; + if (!localtime_r(&time, &tm)) { + goto error; + } + + char time_str[256]; + size_t time_ret; + + time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60; + time_t tomorrow = now + 24 * 60 * 60; + if (time <= six_months_ago || time >= tomorrow) { + time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm); + } else { + time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm); + } + + if (time_ret == 0) { + goto error; + } + + return fprintf(file, " %s", time_str); + +error: + return fprintf(file, " %jd", (intmax_t)time); +} + /** * -f?ls action. */ @@ -744,28 +784,11 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) { time_t time = statbuf->mtime.tv_sec; time_t now = ctx->now.tv_sec; - time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60; - time_t tomorrow = now + 24 * 60 * 60; - struct tm tm; - if (!localtime_r(&time, &tm)) { - goto error; - } - char time_str[256]; - size_t time_ret; - if (time <= six_months_ago || time >= tomorrow) { - time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm); - } else { - time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm); - } - if (time_ret == 0) { - errno = EOVERFLOW; - goto error; - } - if (cfprintf(cfile, " %s${rs}", time_str) < 0) { + if (print_time(file, time, now) < 0) { goto error; } - if (cfprintf(cfile, " %pP", ftwbuf) < 0) { + if (cfprintf(cfile, "${rs} %pP", ftwbuf) < 0) { goto error; } @@ -997,6 +1020,13 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { const struct BFTW *ftwbuf = state->ftwbuf; enum bfs_stat_flags flags = ftwbuf->stat_flags ^ (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW); enum bfs_type type = bftw_type(ftwbuf, flags); + + // GNU find treats ELOOP as a broken symbolic link for -xtype l + // (but not -L -type l) + if ((flags & BFS_STAT_TRYFOLLOW) && type == BFS_ERROR && errno == ELOOP) { + type = BFS_LNK; + } + if (type == BFS_ERROR) { eval_report_error(state); return false; @@ -1005,40 +1035,23 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) { } } -#if _POSIX_MONOTONIC_CLOCK > 0 -# define BFS_CLOCK CLOCK_MONOTONIC -#elif _POSIX_TIMERS > 0 -# define BFS_CLOCK CLOCK_REALTIME -#endif - /** - * Call clock_gettime(), if available. + * clock_gettime() wrapper. */ static int eval_gettime(struct bfs_eval *state, struct timespec *ts) { -#ifdef BFS_CLOCK - int ret = clock_gettime(BFS_CLOCK, ts); - if (ret != 0) { - bfs_warning(state->ctx, "%pP: clock_gettime(): %s.\n", state->ftwbuf, errstr()); + clockid_t clock = CLOCK_REALTIME; + +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clock = CLOCK_MONOTONIC; } - return ret; -#else - return -1; #endif -} -/** - * Record an elapsed time. - */ -static void timespec_elapsed(struct timespec *elapsed, const struct timespec *start, const struct timespec *end) { - elapsed->tv_sec += end->tv_sec - start->tv_sec; - elapsed->tv_nsec += end->tv_nsec - start->tv_nsec; - if (elapsed->tv_nsec < 0) { - elapsed->tv_nsec += 1000000000L; - --elapsed->tv_sec; - } else if (elapsed->tv_nsec >= 1000000000L) { - elapsed->tv_nsec -= 1000000000L; - ++elapsed->tv_sec; + int ret = clock_gettime(clock, ts); + if (ret != 0) { + bfs_warning(state->ctx, "%pP: clock_gettime(): %s.\n", state->ftwbuf, errstr()); } + return ret; } /** @@ -1059,7 +1072,8 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) { if (time) { if (eval_gettime(state, &end) == 0) { - timespec_elapsed(&expr->elapsed, &start, &end); + timespec_sub(&end, &start); + timespec_add(&expr->elapsed, &end); } } @@ -1128,20 +1142,7 @@ bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) { } /** Update the status bar. */ -static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct timespec *last_status, size_t count) { - struct timespec now; - if (eval_gettime(state, &now) == 0) { - struct timespec elapsed = {0}; - timespec_elapsed(&elapsed, last_status, &now); - - // Update every 0.1s - if (elapsed.tv_sec > 0 || elapsed.tv_nsec >= 100000000L) { - *last_status = now; - } else { - return; - } - } - +static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, size_t count) { size_t width = bfs_bar_width(bar); if (width < 3) { return; @@ -1157,7 +1158,7 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time size_t rhslen = xstrwidth(rhs); if (3 + rhslen > width) { - dstresize(&rhs, 0); + dstrshrink(rhs, 0); rhslen = 0; } @@ -1201,7 +1202,7 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time } pathwidth += cwidth; } - dstresize(&status, lhslen); + dstrshrink(status, lhslen); if (dstrcat(&status, "...") != 0) { goto out; @@ -1284,7 +1285,7 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enu DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW); DEBUG_FLAG(flags, BFS_STAT_NOSYNC); - fprintf(stderr, ") == %d", err ? 0 : -1); + fprintf(stderr, ") == %d", err == 0 ? 0 : -1); if (err) { fprintf(stderr, " [%d]", err); @@ -1371,18 +1372,85 @@ struct callback_args { /** The status bar. */ struct bfs_bar *bar; - /** The time of the last status update. */ - struct timespec last_status; + /** The SIGALRM hook. */ + struct sighook *alrm_hook; + /** The interval timer. */ + struct timer *timer; + /** Flag set by SIGALRM. */ + atomic bool alrm_flag; + /** Flag set by SIGINFO. */ + atomic bool info_flag; + /** The number of files visited so far. */ size_t count; /** The set of seen files. */ struct trie *seen; + /** The number of errors that have occurred. */ + size_t nerrors; /** Eventual return value from bfs_eval(). */ int ret; }; +/** Update the status bar in response to SIGALRM. */ +static void eval_sigalrm(int sig, siginfo_t *info, void *ptr) { + struct callback_args *args = ptr; + store(&args->alrm_flag, true, relaxed); +} + +/** Show/hide the bar in response to SIGINFO. */ +static void eval_siginfo(int sig, siginfo_t *info, void *ptr) { + struct callback_args *args = ptr; + store(&args->info_flag, true, relaxed); +} + +/** Show the status bar. */ +static void eval_show_bar(struct callback_args *args) { + args->alrm_hook = sighook(SIGALRM, eval_sigalrm, args, SH_CONTINUE); + if (!args->alrm_hook) { + goto fail; + } + + args->bar = bfs_bar_show(); + if (!args->bar) { + goto fail; + } + + // Update the bar every 0.1s + struct timespec ival = { .tv_nsec = 100 * 1000 * 1000 }; + args->timer = xtimer_start(&ival); + if (!args->timer) { + goto fail; + } + + // Update the bar immediately + store(&args->alrm_flag, true, relaxed); + + return; + +fail: + bfs_warning(args->ctx, "Couldn't show status bar: %s.\n\n", errstr()); + + bfs_bar_hide(args->bar); + args->bar = NULL; + + sigunhook(args->alrm_hook); + args->alrm_hook = NULL; +} + +/** Hide the status bar. */ +static void eval_hide_bar(struct callback_args *args) { + xtimer_stop(args->timer); + args->timer = NULL; + + sigunhook(args->alrm_hook); + args->alrm_hook = NULL; + + bfs_bar_hide(args->bar); + args->bar = NULL; +} + /** * bftw() callback. */ @@ -1397,17 +1465,37 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) { state.ctx = ctx; state.action = BFTW_CONTINUE; state.ret = &args->ret; + state.nerrors = &args->nerrors; state.quit = false; - if (args->bar) { - eval_status(&state, args->bar, &args->last_status, args->count); + // Check whether SIGINFO was delivered and show/hide the bar + if (exchange(&args->info_flag, false, relaxed)) { + if (args->bar) { + eval_hide_bar(args); + } else { + eval_show_bar(args); + } + } + + if (exchange(&args->alrm_flag, false, relaxed)) { + eval_status(&state, args->bar, args->count); } if (ftwbuf->type == BFS_ERROR) { - if (!eval_should_ignore(&state, ftwbuf->error)) { - eval_error(&state, "%s.\n", xstrerror(ftwbuf->error)); - } state.action = BFTW_PRUNE; + + if (ftwbuf->error == ELOOP && ftwbuf->loopoff > 0) { + char *loop = strndup(ftwbuf->path, ftwbuf->loopoff); + if (loop) { + eval_error(&state, "Filesystem loop back to ${di}%pq${rs}\n", loop); + free(loop); + goto done; + } + } else if (eval_should_ignore(&state, ftwbuf->error)) { + goto done; + } + + eval_error(&state, "%s.\n", xstrerror(ftwbuf->error)); goto done; } @@ -1618,12 +1706,16 @@ int bfs_eval(struct bfs_ctx *ctx) { }; if (ctx->status) { - args.bar = bfs_bar_show(); - if (!args.bar) { - bfs_warning(ctx, "Couldn't show status bar: %s.\n\n", errstr()); - } + eval_show_bar(&args); } +#ifdef SIGINFO + int siginfo = SIGINFO; +#else + int siginfo = SIGUSR1; +#endif + struct sighook *info_hook = sighook(siginfo, eval_siginfo, &args, SH_CONTINUE); + struct trie seen; if (ctx->unique) { trie_init(&seen); @@ -1691,7 +1783,14 @@ int bfs_eval(struct bfs_ctx *ctx) { trie_destroy(&seen); } - bfs_bar_hide(args.bar); + sigunhook(info_hook); + if (args.bar) { + eval_hide_bar(&args); + } + + if (ctx->ignore_errors && args.nerrors > 0) { + bfs_warning(ctx, "Suppressed errors: %zu\n", args.nerrors); + } return args.ret; } @@ -9,8 +9,6 @@ #ifndef BFS_EVAL_H #define BFS_EVAL_H -#include "prelude.h" - struct bfs_ctx; struct bfs_expr; @@ -22,9 +20,9 @@ struct bfs_eval; /** * Expression evaluation function. * - * @param expr + * @expr * The current expression. - * @param state + * @state * The current evaluation state. * @return * The result of the test. @@ -34,7 +32,7 @@ typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state); /** * Evaluate the command line. * - * @param ctx + * @ctx * The bfs context to evaluate. * @return * EXIT_SUCCESS on success, otherwise on failure. @@ -1,9 +1,10 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "exec.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" @@ -11,6 +12,7 @@ #include "diag.h" #include "dstring.h" #include "xspawn.h" + #include <errno.h> #include <fcntl.h> #include <stdarg.h> @@ -22,7 +24,7 @@ #include <unistd.h> /** Print some debugging info. */ -attr(printf(2, 3)) +_printf(2, 3) static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) { const struct bfs_ctx *ctx = execbuf->ctx; @@ -234,7 +236,7 @@ static char *bfs_exec_format_arg(char *arg, const char *path) { char *last = arg; do { - if (dstrncat(&ret, last, match - last) != 0) { + if (dstrxcat(&ret, last, match - last) != 0) { goto err; } if (dstrcat(&ret, path) != 0) { @@ -67,11 +67,11 @@ struct bfs_exec { /** * Parse an exec action. * - * @param argv + * @argv * The (bfs) command line argument to parse. - * @param flags + * @flags * Any flags for this exec action. - * @param ctx + * @ctx * The bfs context. * @return * The parsed exec action, or NULL on failure. @@ -81,9 +81,9 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs /** * Execute the command for a file. * - * @param execbuf + * @execbuf * The parsed exec action. - * @param ftwbuf + * @ftwbuf * The bftw() data for the current file. * @return 0 if the command succeeded, -1 if it failed. If the command could * be executed, -1 is returned, and errno will be non-zero. For @@ -94,7 +94,7 @@ int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf); /** * Finish executing any commands. * - * @param execbuf + * @execbuf * The parsed exec action. * @return 0 on success, -1 if any errors were encountered. */ @@ -2,6 +2,7 @@ // SPDX-License-Identifier: 0BSD #include "expr.h" + #include "alloc.h" #include "ctx.h" #include "diag.h" @@ -10,9 +11,12 @@ #include "list.h" #include "printf.h" #include "xregex.h" + #include <string.h> -struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv) { +struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) { + bfs_assert(kind != BFS_PATH); + struct bfs_expr *expr = arena_alloc(&ctx->expr_arena); if (!expr) { return NULL; @@ -22,6 +26,7 @@ struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t expr->eval_fn = eval_fn; expr->argc = argc; expr->argv = argv; + expr->kind = kind; expr->probability = 0.5; SLIST_PREPEND(&ctx->expr_list, expr, freelist); @@ -63,8 +68,7 @@ void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child) { } void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children) { - while (!SLIST_EMPTY(children)) { - struct bfs_expr *child = SLIST_POP(children); + drain_slist (struct bfs_expr, child, children) { bfs_expr_append(expr, child); } } @@ -8,14 +8,38 @@ #ifndef BFS_EXPR_H #define BFS_EXPR_H -#include "prelude.h" #include "color.h" #include "eval.h" #include "stat.h" + #include <sys/types.h> #include <time.h> /** + * Argument/token/expression kinds. + */ +enum bfs_kind { + /** A regular argument. */ + BFS_ARG, + + /** A flag (-H, -L, etc.). */ + BFS_FLAG, + + /** A root path. */ + BFS_PATH, + + /** An option (-follow, -mindepth, etc.). */ + BFS_OPTION, + /** A test (-name, -size, etc.). */ + BFS_TEST, + /** An action (-print, -exec, etc.). */ + BFS_ACTION, + + /** An operator (-and, -or, etc.). */ + BFS_OPERATOR, +}; + +/** * Integer comparison modes. */ enum bfs_int_cmp { @@ -97,6 +121,8 @@ struct bfs_expr { size_t argc; /** The command line arguments comprising this expression. */ char **argv; + /** The kind of expression this is. */ + enum bfs_kind kind; /** The number of files this expression keeps open between evaluations. */ int persistent_fds; @@ -123,7 +149,7 @@ struct bfs_expr { /** Total time spent running this predicate. */ struct timespec elapsed; - /** Auxilliary data for the evaluation function. */ + /** Auxiliary data for the evaluation function. */ union { /** Child expressions. */ struct bfs_exprs children; @@ -207,7 +233,7 @@ struct bfs_ctx; /** * Create a new expression. */ -struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv); +struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv, enum bfs_kind kind); /** * @return Whether this type of expression has children. diff --git a/src/fsade.c b/src/fsade.c index 7310141..dfdf125 100644 --- a/src/fsade.c +++ b/src/fsade.c @@ -1,14 +1,16 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "fsade.h" + #include "atomic.h" +#include "bfs.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> @@ -26,17 +28,26 @@ # include <selinux/selinux.h> #endif -#if BFS_USE_SYS_EXTATTR_H +#if __has_include(<sys/extattr.h>) # include <sys/extattr.h> -#elif BFS_USE_SYS_XATTR_H +# define BFS_USE_EXTATTR true +#elif __has_include(<sys/xattr.h>) # include <sys/xattr.h> +# define BFS_USE_XATTR true +#endif + +#ifndef BFS_USE_EXTATTR +# define BFS_USE_EXTATTR false +#endif +#ifndef BFS_USE_XATTR +# define BFS_USE_XATTR false #endif /** * Many of the APIs used here don't have *at() variants, but we can try to * emulate something similar if /proc/self/fd is available. */ -attr(maybe_unused) +_maybe_unused static const char *fake_at(const struct BFTW *ftwbuf) { static atomic int proc_works = -1; @@ -70,7 +81,7 @@ fail: return ftwbuf->path; } -attr(maybe_unused) +_maybe_unused static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { if (path != ftwbuf->path) { dstrfree((dchar *)path); @@ -80,7 +91,7 @@ static void free_fake_at(const struct BFTW *ftwbuf, const char *path) { /** * Check if an error was caused by the absence of support or data for a feature. */ -attr(maybe_unused) +_maybe_unused static bool is_absence_error(int error) { // If the OS doesn't support the feature, it's obviously not enabled for // any files @@ -160,7 +171,7 @@ static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) { } /** Unified interface for acl_get_tag_type(). */ -attr(maybe_unused) +_maybe_unused static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) { #if BFS_HAS_ACL_GET_TAG_TYPE return acl_get_tag_type(entry, tag); @@ -344,7 +355,7 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) { #if BFS_CAN_CHECK_XATTRS -#if BFS_USE_SYS_EXTATTR_H +#if BFS_USE_EXTATTR /** Wrapper for extattr_list_{file,link}. */ static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) { @@ -390,13 +401,13 @@ static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespa #endif } -#endif // BFS_USE_SYS_EXTATTR_H +#endif // BFS_USE_EXTATTR int bfs_check_xattrs(const struct BFTW *ftwbuf) { const char *path = fake_at(ftwbuf); ssize_t len; -#if BFS_USE_SYS_EXTATTR_H +#if BFS_USE_EXTATTR len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM); if (len <= 0) { len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER); @@ -432,7 +443,7 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) { const char *path = fake_at(ftwbuf); ssize_t len; -#if BFS_USE_SYS_EXTATTR_H +#if BFS_USE_EXTATTR len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name); if (len < 0) { len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name); diff --git a/src/fsade.h b/src/fsade.h index 4465017..fbe02d8 100644 --- a/src/fsade.h +++ b/src/fsade.h @@ -9,7 +9,7 @@ #ifndef BFS_FSADE_H #define BFS_FSADE_H -#include "prelude.h" +#include "bfs.h" #define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL) @@ -17,14 +17,18 @@ #define BFS_CAN_CHECK_CONTEXT BFS_WITH_LIBSELINUX -#define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H) +#if __has_include(<sys/extattr.h>) || __has_include(<sys/xattr.h>) +# define BFS_CAN_CHECK_XATTRS true +#else +# define BFS_CAN_CHECK_XATTRS false +#endif struct BFTW; /** * Check if a file has a non-trivial Access Control List. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -34,7 +38,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf); /** * Check if a file has a non-trivial capability set. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -44,7 +48,7 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf); /** * Check if a file has any extended attributes set. * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -54,9 +58,9 @@ int bfs_check_xattrs(const struct BFTW *ftwbuf); /** * Check if a file has an extended attribute with the given name. * - * @param ftwbuf + * @ftwbuf * The file to check. - * @param name + * @name * The name of the xattr to check. * @return * 1 if it does, 0 if it doesn't, or -1 if an error occurred. @@ -66,7 +70,7 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name); /** * Get a file's SELinux context * - * @param ftwbuf + * @ftwbuf * The file to check. * @return * The file's SELinux context, or NULL on failure. @@ -118,22 +118,25 @@ * [1]: https://arxiv.org/abs/2201.02179 */ -#include "prelude.h" #include "ioq.h" + #include "alloc.h" #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "bit.h" #include "diag.h" #include "dir.h" #include "stat.h" #include "thread.h" + #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdint.h> #include <stdlib.h> #include <sys/stat.h> +#include <unistd.h> #if BFS_WITH_LIBURING # include <liburing.h> @@ -180,8 +183,7 @@ typedef atomic uintptr_t ioq_slot; /** Amount to add for an additional skip. */ #define IOQ_SKIP_ONE (~IOQ_BLOCKED) -// Need room for two flag bits -bfs_static_assert(alignof(struct ioq_ent) >= (1 << 2)); +static_assert(alignof(struct ioq_ent) >= (1 << 2), "struct ioq_ent is underaligned"); /** * An MPMC queue of I/O commands. @@ -201,7 +203,7 @@ struct ioqq { cache_align atomic size_t tail; /** The circular buffer itself. */ - cache_align ioq_slot slots[]; + cache_align ioq_slot slots[]; // _counted_by(slot_mask + 1) }; /** Destroy an I/O command queue. */ @@ -258,17 +260,45 @@ static struct ioqq *ioqq_create(size_t size) { /** Get the monitor associated with a slot. */ static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) { - size_t i = slot - ioqq->slots; + uint32_t i = slot - ioqq->slots; + + // Hash the index to de-correlate waiters + // https://nullprogram.com/blog/2018/07/31/ + // https://github.com/skeeto/hash-prospector/issues/19#issuecomment-1120105785 + i ^= i >> 16; + i *= UINT32_C(0x21f0aaad); + i ^= i >> 15; + i *= UINT32_C(0x735a2d97); + i ^= i >> 15; + return &ioqq->monitors[i & ioqq->monitor_mask]; } /** Atomically wait for a slot to change. */ -attr(noinline) +_noinline static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) { + uintptr_t ret; + + // Try spinning a few times (with exponential backoff) before blocking + _nounroll + for (int i = 1; i < 1024; i *= 2) { + _nounroll + for (int j = 0; j < i; ++j) { + spin_loop(); + } + + // Check if the slot changed + ret = load(slot, relaxed); + if (ret != value) { + return ret; + } + } + + // Nothing changed, start blocking struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); mutex_lock(&monitor->mutex); - uintptr_t ret = load(slot, relaxed); + ret = load(slot, relaxed); if (ret != value) { goto done; } @@ -293,7 +323,7 @@ done: } /** Wake up any threads waiting on a slot. */ -attr(noinline) +_noinline static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) { struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot); @@ -313,9 +343,11 @@ static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) { cond_broadcast(&monitor->cond); } -/** Branch-free (slot & IOQ_SKIP) ? ~IOQ_BLOCKED : 0 */ -static uintptr_t ioq_skip_mask(uintptr_t slot) { - return -(slot >> IOQ_SKIP_BIT) << 1; +/** Branch-free ((slot & IOQ_SKIP) ? skip : full) & ~IOQ_BLOCKED */ +static uintptr_t ioq_slot_blend(uintptr_t slot, uintptr_t skip, uintptr_t full) { + uintptr_t mask = -(slot >> IOQ_SKIP_BIT); + uintptr_t ret = (skip & mask) | (full & ~mask); + return ret & ~IOQ_BLOCKED; } /** Push an entry into a slot. */ @@ -323,19 +355,18 @@ static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent uintptr_t prev = load(slot, relaxed); while (true) { - size_t skip_mask = ioq_skip_mask(prev); - size_t full_mask = ~skip_mask & ~IOQ_BLOCKED; - if (prev & full_mask) { + uintptr_t full = ioq_slot_blend(prev, 0, prev); + if (full) { // full(ptr) → wait prev = ioq_slot_wait(ioqq, slot, prev); continue; } // empty → full(ptr) - uintptr_t next = ((uintptr_t)ent >> 1) & full_mask; + uintptr_t next = (uintptr_t)ent >> 1; // skip(1) → empty // skip(n) → skip(n - 1) - next |= (prev - IOQ_SKIP_ONE) & skip_mask; + next = ioq_slot_blend(prev, prev - IOQ_SKIP_ONE, next); if (compare_exchange_weak(slot, &prev, next, release, relaxed)) { break; @@ -353,13 +384,20 @@ static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool block) { uintptr_t prev = load(slot, relaxed); while (true) { +#if __has_builtin(__builtin_prefetch) + // Optimistically prefetch the pointer in this slot. If this + // slot is not full, this will prefetch an invalid address, but + // experimentally this is worth it on both Intel (Alder Lake) + // and AMD (Zen 2). + __builtin_prefetch((void *)(prev << 1), 1 /* write */); +#endif + // empty → skip(1) // skip(n) → skip(n + 1) // full(ptr) → full(ptr - 1) uintptr_t next = prev + IOQ_SKIP_ONE; - // skip(n) → ~IOQ_BLOCKED // full(ptr) → 0 - next &= ioq_skip_mask(next); + next = ioq_slot_blend(next, next, 0); if (block && next) { prev = ioq_slot_wait(ioqq, slot, prev); @@ -378,7 +416,7 @@ static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool bloc // empty → 0 // skip(n) → 0 // full(ptr) → ptr - prev &= ioq_skip_mask(~prev); + prev = ioq_slot_blend(prev, 0, prev); return (struct ioq_ent *)(prev << 1); } @@ -408,13 +446,6 @@ static void ioqq_push_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t s } while (size > 0); } -/** Pop an entry from the queue. */ -static struct ioq_ent *ioqq_pop(struct ioqq *ioqq, bool block) { - size_t i = fetch_add(&ioqq->tail, 1, relaxed); - ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask]; - return ioq_slot_pop(ioqq, slot, block); -} - /** Pop a batch of entries from the queue. */ static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size, bool block) { size_t mask = ioqq->slot_mask; @@ -430,30 +461,77 @@ static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t si #define IOQ_BATCH (FALSE_SHARING_SIZE / sizeof(ioq_slot)) /** - * A batch of entries to send all at once. + * A batch of I/O queue entries. */ struct ioq_batch { - /** The current batch size. */ - size_t size; + /** The start of the batch. */ + size_t head; + /** The end of the batch. */ + size_t tail; /** The array of entries. */ struct ioq_ent *entries[IOQ_BATCH]; }; -/** Send the batch to a queue. */ +/** Reset a batch. */ +static void ioq_batch_reset(struct ioq_batch *batch) { + batch->head = batch->tail = 0; +} + +/** Check if a batch is empty. */ +static bool ioq_batch_empty(const struct ioq_batch *batch) { + return batch->head >= batch->tail; +} + +/** Send a batch to a queue. */ static void ioq_batch_flush(struct ioqq *ioqq, struct ioq_batch *batch) { - if (batch->size > 0) { - ioqq_push_batch(ioqq, batch->entries, batch->size); - batch->size = 0; + if (batch->tail > 0) { + ioqq_push_batch(ioqq, batch->entries, batch->tail); + ioq_batch_reset(batch); } } -/** An an entry to a batch, flushing if necessary. */ +/** Push an entry to a batch, flushing if necessary. */ static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct ioq_ent *ent) { - if (batch->size >= IOQ_BATCH) { + batch->entries[batch->tail++] = ent; + + if (batch->tail >= IOQ_BATCH) { ioq_batch_flush(ioqq, batch); } +} + +/** Fill a batch from a queue. */ +static bool ioq_batch_fill(struct ioqq *ioqq, struct ioq_batch *batch, bool block) { + ioqq_pop_batch(ioqq, batch->entries, IOQ_BATCH, block); - batch->entries[batch->size++] = ent; + ioq_batch_reset(batch); + for (size_t i = 0; i < IOQ_BATCH; ++i) { + struct ioq_ent *ent = batch->entries[i]; + if (ent) { + batch->entries[batch->tail++] = ent; + } + } + + return batch->tail > 0; +} + +/** Pop an entry from a batch, filling it first if necessary. */ +static struct ioq_ent *ioq_batch_pop(struct ioqq *ioqq, struct ioq_batch *batch, bool block) { + if (ioq_batch_empty(batch)) { + // For non-blocking pops, make sure that each ioq_batch_pop() + // corresponds to a single (amortized) increment of ioqq->head. + // Otherwise, we start skipping many slots and batching ends up + // degrading performance. + if (!block && batch->head < IOQ_BATCH) { + ++batch->head; + return NULL; + } + + if (!ioq_batch_fill(ioqq, batch, block)) { + return NULL; + } + } + + return batch->entries[batch->head++]; } /** Sentinel stop command. */ @@ -502,15 +580,20 @@ struct ioq { struct arena xbufs; #endif - /** Pending I/O requests. */ + /** Pending I/O request queue. */ struct ioqq *pending; - /** Ready I/O responses. */ + /** Ready I/O response queue. */ struct ioqq *ready; + /** Pending request batch. */ + struct ioq_batch pending_batch; + /** Ready request batch. */ + struct ioq_batch ready_batch; + /** The number of background threads. */ size_t nthreads; /** The background threads themselves. */ - struct ioq_thread threads[]; + struct ioq_thread threads[] _counted_by(nthreads); }; /** Cancel a request if we need to. */ @@ -531,6 +614,14 @@ static bool ioq_check_cancel(struct ioq *ioq, struct ioq_ent *ent) { /** Dispatch a single request synchronously. */ static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) { switch (ent->op) { + case IOQ_NOP: + if (ent->nop.type == IOQ_NOP_HEAVY) { + // A fast, no-op syscall + getppid(); + } + ent->result = 0; + return; + case IOQ_CLOSE: ent->result = try(xclose(ent->close.fd)); return; @@ -579,23 +670,161 @@ struct ioq_ring_state { struct ioq_batch ready; }; +/** Reap a single CQE. */ +static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) { + struct ioq *ioq = state->ioq; + + struct ioq_ent *ent = io_uring_cqe_get_data(cqe); + ent->result = cqe->res; + + if (ent->result < 0) { + goto push; + } + + switch (ent->op) { + case IOQ_OPENDIR: { + int fd = ent->result; + if (ioq_check_cancel(ioq, ent)) { + xclose(fd); + goto push; + } + + struct ioq_opendir *args = &ent->opendir; + ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags)); + if (ent->result >= 0) { + // TODO: io_uring_prep_getdents() + bfs_polldir(args->dir); + } else { + xclose(fd); + } + + break; + } + +#if BFS_USE_STATX + case IOQ_STAT: { + struct ioq_stat *args = &ent->stat; + ent->result = try(bfs_statx_convert(args->buf, args->xbuf)); + break; + } +#endif + + default: + break; + } + +push: + ioq_batch_push(ioq->ready, &state->ready, ent); +} + +/** Wait for submitted requests to complete. */ +static void ioq_ring_drain(struct ioq_ring_state *state, size_t wait_nr) { + struct ioq *ioq = state->ioq; + struct io_uring *ring = state->ring; + + bfs_assert(wait_nr <= state->submitted); + + while (state->submitted > 0) { + struct io_uring_cqe *cqe; + if (wait_nr > 0) { + io_uring_wait_cqes(ring, &cqe, wait_nr, NULL, NULL); + } + + unsigned int head; + size_t seen = 0; + io_uring_for_each_cqe (ring, head, cqe) { + ioq_reap_cqe(state, cqe); + ++seen; + } + + io_uring_cq_advance(ring, seen); + state->submitted -= seen; + + if (seen >= wait_nr) { + break; + } + wait_nr -= seen; + } + + ioq_batch_flush(ioq->ready, &state->ready); +} + +/** Submit prepped SQEs, and wait for some to complete. */ +static void ioq_ring_submit(struct ioq_ring_state *state) { + struct io_uring *ring = state->ring; + + size_t unreaped = state->prepped + state->submitted; + size_t wait_nr = 0; + + if (state->prepped == 0 && unreaped > 0) { + // If we have no new SQEs, wait for at least one old one to + // complete, to avoid livelock + wait_nr = 1; + } + + if (unreaped > ring->sq.ring_entries) { + // Keep the completion queue below half full + wait_nr = unreaped - ring->sq.ring_entries; + } + + // Submit all prepped SQEs + while (state->prepped > 0) { + int ret = io_uring_submit_and_wait(state->ring, wait_nr); + if (ret <= 0) { + continue; + } + + state->submitted += ret; + state->prepped -= ret; + if (state->prepped > 0) { + // In the unlikely event of a short submission, any SQE + // links will be broken. Wait for all SQEs to complete + // to preserve any ordering requirements. + ioq_ring_drain(state, state->submitted); + wait_nr = 0; + } + } + + // Drain all the CQEs we waited for (and any others that are ready) + ioq_ring_drain(state, wait_nr); +} + +/** Reserve space for a number of SQEs, submitting if necessary. */ +static void ioq_reserve_sqes(struct ioq_ring_state *state, unsigned int count) { + while (io_uring_sq_space_left(state->ring) < count) { + ioq_ring_submit(state); + } +} + +/** Get an SQE, submitting if necessary. */ +static struct io_uring_sqe *ioq_get_sqe(struct ioq_ring_state *state) { + ioq_reserve_sqes(state, 1); + return io_uring_get_sqe(state->ring); +} + /** Dispatch a single request asynchronously. */ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, struct ioq_ent *ent) { - struct io_uring *ring = state->ring; enum ioq_ring_ops ops = state->ops; struct io_uring_sqe *sqe = NULL; switch (ent->op) { + case IOQ_NOP: + if (ent->nop.type == IOQ_NOP_HEAVY) { + sqe = ioq_get_sqe(state); + io_uring_prep_nop(sqe); + } + return sqe; + case IOQ_CLOSE: if (ops & IOQ_RING_CLOSE) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); io_uring_prep_close(sqe, ent->close.fd); } return sqe; case IOQ_OPENDIR: if (ops & IOQ_RING_OPENAT) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); struct ioq_opendir *args = &ent->opendir; int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY; io_uring_prep_openat(sqe, args->dfd, args->path, flags, 0); @@ -605,7 +834,7 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str case IOQ_CLOSEDIR: #if BFS_USE_UNWRAPDIR if (ops & IOQ_RING_CLOSE) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); io_uring_prep_close(sqe, bfs_unwrapdir(ent->closedir.dir)); } #endif @@ -614,10 +843,10 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str case IOQ_STAT: #if BFS_USE_STATX if (ops & IOQ_RING_STATX) { - sqe = io_uring_get_sqe(ring); + sqe = ioq_get_sqe(state); struct ioq_stat *args = &ent->stat; int flags = bfs_statx_flags(args->flags); - unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + unsigned int mask = bfs_statx_mask(); io_uring_prep_statx(sqe, args->dfd, args->path, flags, mask, args->xbuf); } #endif @@ -630,7 +859,7 @@ static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, str /** Check if ioq_ring_reap() has work to do. */ static bool ioq_ring_empty(struct ioq_ring_state *state) { - return !state->prepped && !state->submitted && !state->ready.size; + return !state->prepped && !state->submitted && ioq_batch_empty(&state->ready); } /** Prep a single SQE. */ @@ -658,121 +887,52 @@ static bool ioq_ring_prep(struct ioq_ring_state *state) { } struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; - struct ioq_ent *pending[IOQ_BATCH]; - - while (io_uring_sq_space_left(ring) >= IOQ_BATCH) { - bool block = ioq_ring_empty(state); - ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, block); - - bool any = false; - for (size_t i = 0; i < IOQ_BATCH; ++i) { - struct ioq_ent *ent = pending[i]; - if (ent == &IOQ_STOP) { - ioqq_push(ioq->pending, &IOQ_STOP); - state->stop = true; - goto done; - } else if (ent) { - ioq_prep_sqe(state, ent); - any = true; - } - } - if (!any) { - break; - } - } - -done: - return !ioq_ring_empty(state); -} - -/** Reap a single CQE. */ -static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) { - struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; - - struct ioq_ent *ent = io_uring_cqe_get_data(cqe); - ent->result = cqe->res; - io_uring_cqe_seen(ring, cqe); - --state->submitted; - - if (ent->result < 0) { - goto push; - } - - switch (ent->op) { - case IOQ_OPENDIR: { - int fd = ent->result; - if (ioq_check_cancel(ioq, ent)) { - xclose(fd); - goto push; - } - - struct ioq_opendir *args = &ent->opendir; - ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags)); - if (ent->result >= 0) { - // TODO: io_uring_prep_getdents() - bfs_polldir(args->dir); - } else { - xclose(fd); - } + struct ioq_batch pending; + ioq_batch_reset(&pending); + while (true) { + bool block = ioq_ring_empty(state); + struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, block); + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, ent); + state->stop = true; break; - } - -#if BFS_USE_STATX - case IOQ_STAT: { - struct ioq_stat *args = &ent->stat; - ent->result = try(bfs_statx_convert(args->buf, args->xbuf)); + } else if (ent) { + ioq_prep_sqe(state, ent); + } else { break; } -#endif - - default: - break; } -push: - ioq_batch_push(ioq->ready, &state->ready, ent); + bfs_assert(ioq_batch_empty(&pending)); + return !ioq_ring_empty(state); } -/** Reap a batch of CQEs. */ -static void ioq_ring_reap(struct ioq_ring_state *state) { - struct ioq *ioq = state->ioq; - struct io_uring *ring = state->ring; +/** io_uring worker loop. */ +static int ioq_ring_work(struct ioq_thread *thread) { + struct io_uring *ring = &thread->ring; - while (state->prepped) { - int ret = io_uring_submit_and_wait(ring, 1); - if (ret > 0) { - state->prepped -= ret; - state->submitted += ret; +#ifdef IORING_SETUP_R_DISABLED + if (ring->flags & IORING_SETUP_R_DISABLED) { + if (io_uring_enable_rings(ring) != 0) { + return -1; } } +#endif - while (state->submitted) { - struct io_uring_cqe *cqe; - if (io_uring_wait_cqe(ring, &cqe) < 0) { - continue; - } - - ioq_reap_cqe(state, cqe); - } - - ioq_batch_flush(ioq->ready, &state->ready); -} - -/** io_uring worker loop. */ -static void ioq_ring_work(struct ioq_thread *thread) { struct ioq_ring_state state = { .ioq = thread->parent, - .ring = &thread->ring, + .ring = ring, .ops = thread->ring_ops, }; while (ioq_ring_prep(&state)) { - ioq_ring_reap(&state); + ioq_ring_submit(&state); } + + ioq_ring_drain(&state, state.submitted); + return 0; } #endif // BFS_WITH_LIBURING @@ -781,30 +941,29 @@ static void ioq_ring_work(struct ioq_thread *thread) { static void ioq_sync_work(struct ioq_thread *thread) { struct ioq *ioq = thread->parent; - bool stop = false; - while (!stop) { - struct ioq_ent *pending[IOQ_BATCH]; - ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, true); - - struct ioq_batch ready; - ready.size = 0; - - for (size_t i = 0; i < IOQ_BATCH; ++i) { - struct ioq_ent *ent = pending[i]; - if (ent == &IOQ_STOP) { - ioqq_push(ioq->pending, &IOQ_STOP); - stop = true; - break; - } else if (ent) { - if (!ioq_check_cancel(ioq, ent)) { - ioq_dispatch_sync(ioq, ent); - } - ioq_batch_push(ioq->ready, &ready, ent); - } + struct ioq_batch pending, ready; + ioq_batch_reset(&pending); + ioq_batch_reset(&ready); + + while (true) { + if (ioq_batch_empty(&pending)) { + ioq_batch_flush(ioq->ready, &ready); } - ioq_batch_flush(ioq->ready, &ready); + struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, true); + if (ent == &IOQ_STOP) { + ioqq_push(ioq->pending, ent); + break; + } + + if (!ioq_check_cancel(ioq, ent)) { + ioq_dispatch_sync(ioq, ent); + } + ioq_batch_push(ioq->ready, &ready, ent); } + + bfs_assert(ioq_batch_empty(&pending)); + ioq_batch_flush(ioq->ready, &ready); } /** Background thread entry point. */ @@ -813,8 +972,9 @@ static void *ioq_work(void *ptr) { #if BFS_WITH_LIBURING if (thread->ring_err == 0) { - ioq_ring_work(thread); - return NULL; + if (ioq_ring_work(thread) == 0) { + return NULL; + } } #endif @@ -822,6 +982,27 @@ static void *ioq_work(void *ptr) { return NULL; } +#if BFS_WITH_LIBURING +/** Test whether some io_uring setup flags are supported. */ +static bool ioq_ring_probe_flags(struct io_uring_params *params, unsigned int flags) { + unsigned int saved = params->flags; + params->flags |= flags; + + struct io_uring ring; + int ret = io_uring_queue_init_params(2, &ring, params); + if (ret == 0) { + io_uring_queue_exit(&ring); + } + + if (ret == -EINVAL) { + params->flags = saved; + return false; + } + + return true; +} +#endif + /** Initialize io_uring thread state. */ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { #if BFS_WITH_LIBURING @@ -835,11 +1016,31 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { return -1; } - // Share io-wq workers between rings struct io_uring_params params = {0}; + if (prev) { - params.flags |= IORING_SETUP_ATTACH_WQ; + // Share io-wq workers between rings + params.flags = prev->ring.flags | IORING_SETUP_ATTACH_WQ; params.wq_fd = prev->ring.ring_fd; + } else { +#ifdef IORING_SETUP_SUBMIT_ALL + // Don't abort submission just because an inline request fails + ioq_ring_probe_flags(¶ms, IORING_SETUP_SUBMIT_ALL); +#endif + +#ifdef IORING_SETUP_R_DISABLED + // Don't enable the ring yet (needed for SINGLE_ISSUER) + if (ioq_ring_probe_flags(¶ms, IORING_SETUP_R_DISABLED)) { +# ifdef IORING_SETUP_SINGLE_ISSUER + // Allow optimizations assuming only one task submits SQEs + ioq_ring_probe_flags(¶ms, IORING_SETUP_SINGLE_ISSUER); +# endif +# ifdef IORING_SETUP_DEFER_TASKRUN + // Don't interrupt us aggressively with completion events + ioq_ring_probe_flags(¶ms, IORING_SETUP_DEFER_TASKRUN); +# endif + } +#endif } // Use a page for each SQE ring @@ -877,6 +1078,7 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { return -1; } +#if BFS_HAS_IO_URING_MAX_WORKERS // Limit the number of io_uring workers unsigned int values[] = { ioq->nthreads, // [IO_WQ_BOUND] @@ -885,6 +1087,8 @@ static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) { io_uring_register_iowq_max_workers(&thread->ring, values); #endif +#endif // BFS_WITH_LIBURING + return 0; } @@ -898,7 +1102,8 @@ static void ioq_ring_exit(struct ioq_thread *thread) { } /** Create an I/O queue thread. */ -static int ioq_thread_create(struct ioq *ioq, struct ioq_thread *thread) { +static int ioq_thread_create(struct ioq *ioq, size_t i) { + struct ioq_thread *thread = &ioq->threads[i]; thread->parent = ioq; ioq_ring_init(ioq, thread); @@ -908,6 +1113,11 @@ static int ioq_thread_create(struct ioq *ioq, struct ioq_thread *thread) { return -1; } + char name[16]; + if (snprintf(name, sizeof(name), "ioq-%zu", i) >= 0) { + thread_setname(thread->id, name); + } + return 0; } @@ -942,7 +1152,7 @@ struct ioq *ioq_create(size_t depth, size_t nthreads) { ioq->nthreads = nthreads; for (size_t i = 0; i < nthreads; ++i) { - if (ioq_thread_create(ioq, &ioq->threads[i]) != 0) { + if (ioq_thread_create(ioq, i) != 0) { ioq->nthreads = i; goto fail; } @@ -984,6 +1194,18 @@ static struct ioq_ent *ioq_request(struct ioq *ioq, enum ioq_op op, void *ptr) { return ent; } +int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr) { + struct ioq_ent *ent = ioq_request(ioq, IOQ_NOP, ptr); + if (!ent) { + return -1; + } + + ent->nop.type = type; + + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); + return 0; +} + int ioq_close(struct ioq *ioq, int fd, void *ptr) { struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSE, ptr); if (!ent) { @@ -992,7 +1214,7 @@ int ioq_close(struct ioq *ioq, int fd, void *ptr) { ent->close.fd = fd; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1008,7 +1230,7 @@ int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, args->path = path; args->flags = flags; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1020,7 +1242,7 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr) { ent->closedir.dir = dir; - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } @@ -1044,16 +1266,23 @@ int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags fla } #endif - ioqq_push(ioq->pending, ent); + ioq_batch_push(ioq->pending, &ioq->pending_batch, ent); return 0; } +void ioq_submit(struct ioq *ioq) { + ioq_batch_flush(ioq->pending, &ioq->pending_batch); +} + struct ioq_ent *ioq_pop(struct ioq *ioq, bool block) { + // Don't forget to submit before popping + bfs_assert(ioq_batch_empty(&ioq->pending_batch)); + if (ioq->size == 0) { return NULL; } - return ioqq_pop(ioq->ready, block); + return ioq_batch_pop(ioq->ready, &ioq->ready_batch, block); } void ioq_free(struct ioq *ioq, struct ioq_ent *ent) { @@ -1071,7 +1300,8 @@ void ioq_free(struct ioq *ioq, struct ioq_ent *ent) { void ioq_cancel(struct ioq *ioq) { if (!exchange(&ioq->cancel, true, relaxed)) { - ioqq_push(ioq->pending, &IOQ_STOP); + ioq_batch_push(ioq->pending, &ioq->pending_batch, &IOQ_STOP); + ioq_submit(ioq); } } @@ -8,9 +8,10 @@ #ifndef BFS_IOQ_H #define BFS_IOQ_H -#include "prelude.h" +#include "bfs.h" #include "dir.h" #include "stat.h" + #include <stddef.h> /** @@ -22,6 +23,8 @@ struct ioq; * I/O queue operations. */ enum ioq_op { + /** ioq_nop(). */ + IOQ_NOP, /** ioq_close(). */ IOQ_CLOSE, /** ioq_opendir(). */ @@ -33,18 +36,21 @@ enum ioq_op { }; /** - * The I/O queue implementation needs two tag bits in each pointer to a struct - * ioq_ent, so we need to ensure at least 4-byte alignment. The natural - * alignment is enough on most architectures, but not m68k, so over-align it. + * ioq_nop() types. */ -#define IOQ_ENT_ALIGN alignas(4) +enum ioq_nop_type { + /** A lightweight nop that avoids syscalls. */ + IOQ_NOP_LIGHT, + /** A heavyweight nop that involves a syscall. */ + IOQ_NOP_HEAVY, +}; /** * An I/O queue entry. */ struct ioq_ent { /** The I/O operation. */ - IOQ_ENT_ALIGN enum ioq_op op; + cache_align enum ioq_op op; /** The return value (on success) or negative error code (on failure). */ int result; @@ -54,6 +60,10 @@ struct ioq_ent { /** Operation-specific arguments. */ union { + /** ioq_nop() args. */ + struct ioq_nop { + enum ioq_nop_type type; + } nop; /** ioq_close() args. */ struct ioq_close { int fd; @@ -83,9 +93,9 @@ struct ioq_ent { /** * Create an I/O queue. * - * @param depth + * @depth * The maximum depth of the queue. - * @param nthreads + * @nthreads * The maximum number of background threads. * @return * The new I/O queue, or NULL on failure. @@ -98,13 +108,27 @@ struct ioq *ioq_create(size_t depth, size_t nthreads); size_t ioq_capacity(const struct ioq *ioq); /** + * A no-op, for benchmarking. + * + * @ioq + * The I/O queue. + * @type + * The type of operation to perform. + * @ptr + * An arbitrary pointer to associate with the request. + * @return + * 0 on success, or -1 on failure. + */ +int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr); + +/** * Asynchronous close(). * - * @param ioq + * @ioq * The I/O queue. - * @param fd + * @fd * The fd to close. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -114,17 +138,17 @@ int ioq_close(struct ioq *ioq, int fd, void *ptr); /** * Asynchronous bfs_opendir(). * - * @param ioq + * @ioq * The I/O queue. - * @param dir + * @dir * The allocated directory. - * @param dfd + * @dfd * The base file descriptor. - * @param path + * @path * The path to open, relative to dfd. - * @param flags + * @flags * Flags that control which directory entries are listed. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -134,11 +158,11 @@ int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, /** * Asynchronous bfs_closedir(). * - * @param ioq + * @ioq * The I/O queue. - * @param dir + * @dir * The directory to close. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -148,17 +172,17 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr); /** * Asynchronous bfs_stat(). * - * @param ioq + * @ioq * The I/O queue. - * @param dfd + * @dfd * The base file descriptor. - * @param path + * @path * The path to stat, relative to dfd. - * @param flags + * @flags * Flags that affect the lookup. - * @param buf + * @buf * A place to store the stat buffer, if successful. - * @param ptr + * @ptr * An arbitrary pointer to associate with the request. * @return * 0 on success, or -1 on failure. @@ -166,9 +190,14 @@ int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr); int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr); /** + * Submit any buffered requests. + */ +void ioq_submit(struct ioq *ioq); + +/** * Pop a response from the queue. * - * @param ioq + * @ioq * The I/O queue. * @return * The next response, or NULL. @@ -178,9 +207,9 @@ struct ioq_ent *ioq_pop(struct ioq *ioq, bool block); /** * Free a queue entry. * - * @param ioq + * @ioq * The I/O queue. - * @param ent + * @ent * The entry to free. */ void ioq_free(struct ioq *ioq, struct ioq_ent *ent); @@ -83,13 +83,14 @@ #define BFS_LIST_H #include "diag.h" + #include <stddef.h> #include <string.h> /** * Initialize a singly-linked list. * - * @param list + * @list * The list to initialize. * * --- @@ -116,9 +117,9 @@ /** * Initialize a singly-linked list item. * - * @param item + * @item * The item to initialize. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * * --- @@ -200,7 +201,7 @@ /** * Get the head of a singly-linked list. * - * @param list + * @list * The list in question. * @return * The first item in the list. @@ -227,9 +228,9 @@ /** * Get the tail of a singly-linked list. * - * @param list + * @list * The list in question. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * The last item in the list. @@ -246,11 +247,11 @@ /** * Check if an item is attached to a singly-linked list. * - * @param list + * @list * The list to check. - * @param item + * @item * The item to check. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * Whether the item is attached to the list. @@ -267,13 +268,13 @@ /** * Insert an item into a singly-linked list. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * A pointer to the item to insert after, e.g. &list->head or list->tail. - * @param item + * @item * The item to insert. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * A cursor for the next item. @@ -294,11 +295,11 @@ /** * Add an item to the tail of a singly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to append. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define SLIST_APPEND(list, ...) \ @@ -310,11 +311,11 @@ /** * Add an item to the head of a singly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to prepend. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define SLIST_PREPEND(list, ...) \ @@ -324,27 +325,43 @@ LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__)) /** + * Splice a singly-linked list into another. + * + * @dest + * The destination list. + * @cursor + * A pointer to the item to splice after, e.g. &list->head or list->tail. + * @src + * The source list. + */ +#define SLIST_SPLICE(dest, cursor, src) \ + LIST_VOID_(SLIST_SPLICE_((dest), (cursor), (src))) + +#define SLIST_SPLICE_(dest, cursor, src) \ + *src->tail = *cursor, \ + *cursor = src->head, \ + dest->tail = *dest->tail ? src->tail : dest->tail, \ + SLIST_INIT(src) + +/** * Add an entire singly-linked list to the tail of another. * - * @param dest + * @dest * The destination list. - * @param src + * @src * The source list. */ #define SLIST_EXTEND(dest, src) \ - SLIST_EXTEND_((dest), (src)) - -#define SLIST_EXTEND_(dest, src) \ - (src->head ? (*dest->tail = src->head, dest->tail = src->tail, SLIST_INIT(src)) : (void)0) + SLIST_SPLICE(dest, (dest)->tail, src) /** * Remove an item from a singly-linked list. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * A pointer to the item to remove, either &list->head or &prev->next. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. * @return * The removed item. @@ -357,10 +374,10 @@ #define SLIST_REMOVE__(list, cursor, next) \ (list->tail = (*cursor)->next ? list->tail : cursor, \ - slist_remove_impl(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) + slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor))) // Helper for SLIST_REMOVE() -static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_t size) { +static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) { // ret = *cursor; // *cursor = ret->next; memcpy(cursor, next, size); @@ -372,9 +389,9 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Pop the head off a singly-linked list. * - * @param list + * @list * The list to modify. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. * @return * The popped item, or NULL if the list was empty. @@ -391,13 +408,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Loop over the items in a singly-linked list. * - * @param type + * @type * The list item type. - * @param item + * @item * The induction variable name. - * @param list + * @list * The list to iterate. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. */ #define for_slist(type, item, ...) \ @@ -412,9 +429,24 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ item = _next) /** + * Loop over a singly-linked list, popping each item. + * + * @type + * The list item type. + * @item + * The induction variable name. + * @list + * The list to drain. + * @node (optional) + * If specified, use head->node.next rather than head->next. + */ +#define drain_slist(type, item, ...) \ + for (type *item; (item = SLIST_POP(__VA_ARGS__));) + +/** * Initialize a doubly-linked list. * - * @param list + * @list * The list to initialize. */ #define LIST_INIT(list) \ @@ -433,9 +465,9 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Initialize a doubly-linked list item. * - * @param item + * @item * The item to initialize. - * @param node (optional) + * @node (optional) * If specified, use item->node.next rather than item->next. */ #define LIST_ITEM_INIT(...) \ @@ -465,11 +497,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Add an item to the tail of a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to append. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_APPEND(list, ...) \ @@ -478,11 +510,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Add an item to the head of a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to prepend. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_PREPEND(list, ...) \ @@ -491,11 +523,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Check if an item is attached to a doubly-linked list. * - * @param list + * @list * The list to check. - * @param item + * @item * The item to check. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. * @return * Whether the item is attached to the list. @@ -512,13 +544,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Insert into a doubly-linked list after the given cursor. * - * @param list + * @list * The list to modify. - * @param cursor + * @cursor * Insert after this element. - * @param item + * @item * The item to insert. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_INSERT(list, cursor, ...) \ @@ -537,11 +569,11 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Remove an item from a doubly-linked list. * - * @param list + * @list * The list to modify. - * @param item + * @item * The item to remove. - * @param node (optional) + * @node (optional) * If specified, use item->node.{prev,next} rather than item->{prev,next}. */ #define LIST_REMOVE(list, ...) \ @@ -558,13 +590,13 @@ static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_ /** * Loop over the items in a doubly-linked list. * - * @param type + * @type * The list item type. - * @param item + * @item * The induction variable name. - * @param list + * @list * The list to iterate. - * @param node (optional) + * @node (optional) * If specified, use head->node.next rather than head->next. */ #define for_list(type, item, ...) \ @@ -20,13 +20,14 @@ * - bftw.[ch] (an extended version of nftw(3)) * * - Utilities: + * - prelude.h (feature test macros; automatically included) * - alloc.[ch] (memory allocation) * - atomic.h (atomic operations) * - bar.[ch] (a terminal status bar) * - bit.h (bit manipulation) + * - bfs.h (configuration and fundamental utilities) * - bfstd.[ch] (standard library wrappers/polyfills) * - color.[ch] (for pretty terminal colors) - * - prelude.h (configuration and feature/platform detection) * - diag.[ch] (formats diagnostic messages) * - dir.[ch] (a directory API facade) * - dstring.[ch] (a dynamic string library) @@ -41,17 +42,18 @@ * - thread.h (multi-threading) * - trie.[ch] (a trie set/map implementation) * - typo.[ch] (fuzzy matching for typos) + * - version.c (embeds version information) * - xregex.[ch] (regular expression support) * - xspawn.[ch] (spawns processes) * - xtime.[ch] (date/time handling utilities) */ -#include "prelude.h" #include "bfstd.h" #include "ctx.h" #include "diag.h" #include "eval.h" #include "parse.h" + #include <errno.h> #include <fcntl.h> #include <locale.h> @@ -1,24 +1,28 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "mtab.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "stat.h" #include "trie.h" + #include <errno.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> -#if !defined(BFS_USE_MNTENT) && BFS_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 +#ifndef BFS_USE_MNTENT +# define BFS_USE_MNTENT BFS_HAS_GETMNTENT_1 +#endif +#ifndef BFS_USE_MNTINFO +# define BFS_USE_MNTINFO (!BFS_USE_MNTENT && BFS_HAS_GETMNTINFO) +#endif +#ifndef BFS_USE_MNTTAB +# define BFS_USE_MNTTAB (!BFS_USE_MNTINFO && BFS_HAS_GETMNTENT_2) #endif #if BFS_USE_MNTENT @@ -65,7 +69,7 @@ struct bfs_mtab { /** * Add an entry to the mount table. */ -attr(maybe_unused) +_maybe_unused static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) { size_t path_size = strlen(path) + 1; size_t type_size = strlen(type) + 1; @@ -252,10 +256,7 @@ static int bfs_mtab_fill_types(struct bfs_mtab *mtab) { continue; } - struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev)); - if (leaf) { - leaf->value = mount->type; - } else { + if (trie_set_mem(&mtab->types, &sb.mnt_id, sizeof(sb.mnt_id), mount->type) != 0) { goto fail; } } @@ -278,9 +279,9 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb } } - const struct trie_leaf *leaf = trie_find_mem(&mtab->types, &statbuf->dev, sizeof(statbuf->dev)); - if (leaf) { - return leaf->value; + const char *type = trie_get_mem(&mtab->types, &statbuf->mnt_id, sizeof(statbuf->mnt_id)); + if (type) { + return type; } else { return "unknown"; } @@ -8,8 +8,6 @@ #ifndef BFS_MTAB_H #define BFS_MTAB_H -#include "prelude.h" - struct bfs_stat; /** @@ -28,9 +26,9 @@ struct bfs_mtab *bfs_mtab_parse(void); /** * Determine the file system type that a file is on. * - * @param mtab + * @mtab * The current mount table. - * @param statbuf + * @statbuf * The bfs_stat() buffer for the file in question. * @return * The type of file system containing this file, "unknown" if not known, @@ -41,9 +39,9 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb /** * Check if a file could be a mount point. * - * @param mtab + * @mtab * The current mount table. - * @param name + * @name * The name of the file to check. * @return * Whether the named file could be a mount point. @@ -25,8 +25,10 @@ * effects are reachable at all, skipping the traversal if not. */ -#include "prelude.h" #include "opt.h" + +#include "bfs.h" +#include "bfstd.h" #include "bftw.h" #include "bit.h" #include "color.h" @@ -38,6 +40,8 @@ #include "expr.h" #include "list.h" #include "pwcache.h" +#include "xspawn.h" + #include <errno.h> #include <limits.h> #include <stdarg.h> @@ -102,42 +106,23 @@ enum pred_type { PRED_TYPES, }; -/** Get the name of a predicate type. */ -static const char *pred_type_name(enum pred_type type) { - switch (type) { - case READABLE_PRED: - return "-readable"; - case WRITABLE_PRED: - return "-writable"; - case EXECUTABLE_PRED: - return "-executable"; - case ACL_PRED: - return "-acl"; - case CAPABLE_PRED: - return "-capable"; - case EMPTY_PRED: - return "-empty"; - case HIDDEN_PRED: - return "-hidden"; - case NOGROUP_PRED: - return "-nogroup"; - case NOUSER_PRED: - return "-nouser"; - case SPARSE_PRED: - return "-sparse"; - case XATTR_PRED: - return "-xattr"; - - case PRED_TYPES: - break; - } - - bfs_bug("Unknown predicate %d", (int)type); - return "???"; -} +/** Predicate type names. */ +static const char *const pred_names[] = { + [READABLE_PRED] = "-readable", + [WRITABLE_PRED] = "-writable", + [EXECUTABLE_PRED] = "-executable", + [ACL_PRED] = "-acl", + [CAPABLE_PRED] = "-capable", + [EMPTY_PRED] = "-empty", + [HIDDEN_PRED] = "-hidden", + [NOGROUP_PRED] = "-nogroup", + [NOUSER_PRED] = "-nouser", + [SPARSE_PRED] = "-sparse", + [XATTR_PRED] = "-xattr", +}; /** - * A contrained integer range. + * A constrained integer range. */ struct df_range { /** The (inclusive) minimum value. */ @@ -192,11 +177,17 @@ static void constrain_min(struct df_range *range, long long value) { range->min = max_value(range->min, value); } -/** Contrain the maximum of a range. */ +/** Constrain the maximum of a range. */ static void constrain_max(struct df_range *range, long long value) { range->max = min_value(range->max, value); } +/** Constrain a range to a single value. */ +static void constrain_range(struct df_range *range, long long value) { + constrain_min(range, value); + constrain_max(range, value); +} + /** Remove a single value from a range. */ static void range_remove(struct df_range *range, long long value) { if (range->min == value) { @@ -242,29 +233,15 @@ enum range_type { RANGE_TYPES, }; -/** Get the name of a range type. */ -static const char *range_type_name(enum range_type type) { - switch (type) { - case DEPTH_RANGE: - return "-depth"; - case GID_RANGE: - return "-gid"; - case INUM_RANGE: - return "-inum"; - case LINKS_RANGE: - return "-links"; - case SIZE_RANGE: - return "-size"; - case UID_RANGE: - return "-uid"; - - case RANGE_TYPES: - break; - } - - bfs_bug("Unknown range %d", (int)type); - return "???"; -} +/** Range type names. */ +static const char *const range_names[] = { + [DEPTH_RANGE] = "-depth", + [GID_RANGE] = "-gid", + [INUM_RANGE] = "-inum", + [LINKS_RANGE] = "-links", + [SIZE_RANGE] = "-size", + [UID_RANGE] = "-uid", +}; /** * The data flow analysis domain. @@ -333,27 +310,27 @@ static void df_init_top(struct df_domain *value) { /** Check for the top element. */ static bool df_is_top(const struct df_domain *value) { - for (int i = 0; i < PRED_TYPES; ++i) { - if (value->preds[i] != PRED_TOP) { - return false; - } - } + for (int i = 0; i < PRED_TYPES; ++i) { + if (value->preds[i] != PRED_TOP) { + return false; + } + } - for (int i = 0; i < RANGE_TYPES; ++i) { - if (!range_is_top(&value->ranges[i])) { - return false; - } - } + for (int i = 0; i < RANGE_TYPES; ++i) { + if (!range_is_top(&value->ranges[i])) { + return false; + } + } - if (value->types != ~0U) { - return false; - } + if (value->types != ~0U) { + return false; + } - if (value->xtypes != ~0U) { - return false; - } + if (value->xtypes != ~0U) { + return false; + } - return true; + return true; } /** Compute the union of two fact sets. */ @@ -397,7 +374,7 @@ struct bfs_opt { }; /** Log an optimization. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_debug(struct bfs_opt *opt, const char *format, ...) { if (bfs_debug_prefix(opt->ctx, DEBUG_OPT)) { for (int i = 0; i < opt->depth; ++i) { @@ -415,7 +392,7 @@ static bool opt_debug(struct bfs_opt *opt, const char *format, ...) { } /** Log a recursive call. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_enter(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; if (depth > 0) { @@ -435,7 +412,7 @@ static bool opt_enter(struct bfs_opt *opt, const char *format, ...) { } /** Log a recursive return. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_leave(struct bfs_opt *opt, const char *format, ...) { bool debug = false; int depth = opt->depth; @@ -459,7 +436,7 @@ static bool opt_leave(struct bfs_opt *opt, const char *format, ...) { } /** Log a shallow visit. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_visit(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; if (depth > 0) { @@ -479,7 +456,7 @@ static bool opt_visit(struct bfs_opt *opt, const char *format, ...) { } /** Log the deletion of an expression. */ -attr(printf(2, 3)) +_printf(2, 3) static bool opt_delete(struct bfs_opt *opt, const char *format, ...) { int depth = opt->depth; @@ -503,7 +480,7 @@ typedef bool dump_fn(struct bfs_opt *opt, const char *format, ...); /** Print a df_pred. */ static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum pred_type type) { - dump(opt, "${blu}%s${rs}: ", pred_type_name(type)); + dump(opt, "${blu}%s${rs}: ", pred_names[type]); FILE *file = opt->ctx->cerr->file; switch (value->preds[type]) { @@ -524,7 +501,7 @@ static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain /** Print a df_range. */ static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) { - dump(opt, "${blu}%s${rs}: ", range_type_name(type)); + dump(opt, "${blu}%s${rs}: ", range_names[type]); FILE *file = opt->ctx->cerr->file; const struct df_range *range = &value->ranges[type]; @@ -641,22 +618,26 @@ static bool is_const(const struct bfs_expr *expr) { } /** Warn about an expression. */ -attr(printf(3, 4)) -static void opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { +_printf(3, 4) +static bool opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) { if (!opt->warn) { - return; + return false; } if (bfs_expr_is_parent(expr) || is_const(expr)) { - return; + return false; } - if (bfs_expr_warning(opt->ctx, expr)) { - va_list args; - va_start(args, format); - bfs_vwarning(opt->ctx, format, args); - va_end(args); + if (!bfs_expr_warning(opt->ctx, expr)) { + return false; } + + va_list args; + va_start(args, format); + bfs_vwarning(opt->ctx, format, args); + va_end(args); + + return true; } /** Remove and return an expression's children. */ @@ -756,9 +737,7 @@ static struct bfs_expr *visit_and(struct bfs_opt *opt, struct bfs_expr *expr, co df_init_bottom(&opt->after_false); struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -790,9 +769,7 @@ static struct bfs_expr *visit_or(struct bfs_opt *opt, struct bfs_expr *expr, con df_init_bottom(&opt->after_true); struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -822,9 +799,7 @@ static struct bfs_expr *visit_comma(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_opt nested = *opt; - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { nested.ignore_result = opt->ignore_result; } else { @@ -1360,7 +1335,7 @@ static struct bfs_expr *opt_const(struct bfs_opt *opt, bool value) { static bfs_eval_fn *const fns[] = {eval_false, eval_true}; static char *fake_args[] = {"-false", "-true"}; - struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value]); + struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value], BFS_TEST); return visit_shallow(opt, expr, &annotate); } @@ -1374,7 +1349,7 @@ static struct bfs_expr *negate_expr(struct bfs_opt *opt, struct bfs_expr *expr, return opt_const(opt, true); } - struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv); + struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv, BFS_OPERATOR); if (!ret) { return NULL; } @@ -1403,8 +1378,7 @@ static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { opt_enter(opt, "%pe\n", child); child = negate_expr(opt, child, argv); @@ -1422,18 +1396,16 @@ static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *exp /** Sink a negation into a comma expression. */ static struct bfs_expr *sink_not_comma(struct bfs_opt *opt, struct bfs_expr *expr) { - bfs_assert(expr->eval_fn == eval_comma); - - opt_enter(opt, "%pe\n", expr); - char **argv = expr->argv; expr = only_child(expr); + opt_enter(opt, "%pe\n", expr); + + bfs_assert(expr->eval_fn == eval_comma); struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (SLIST_EMPTY(&children)) { opt_enter(opt, "%pe\n", child); opt_debug(opt, "sink\n"); @@ -1461,7 +1433,6 @@ static struct bfs_expr *canonicalize_not(struct bfs_opt *opt, struct bfs_expr *e if (rhs->eval_fn == eval_not) { opt_debug(opt, "double negation\n"); - rhs = only_child(expr); return only_child(rhs); } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) { return sink_not_andor(opt, expr); @@ -1483,8 +1454,7 @@ static struct bfs_expr *canonicalize_assoc(struct bfs_opt *opt, struct bfs_expr struct bfs_exprs flat; SLIST_INIT(&flat); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (child->eval_fn == expr->eval_fn) { struct bfs_expr *head = SLIST_HEAD(&child->children); struct bfs_expr *tail = SLIST_TAIL(&child->children); @@ -1592,8 +1562,7 @@ static struct bfs_expr *reorder_andor(struct bfs_opt *opt, struct bfs_expr *expr struct bfs_exprs pure; SLIST_INIT(&pure); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { if (child->pure) { SLIST_APPEND(&pure, child); } else { @@ -1634,8 +1603,7 @@ static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enu switch (expr->int_cmp) { case BFS_INT_EQUAL: - constrain_min(true_range, value); - constrain_max(true_range, value); + constrain_range(true_range, value); range_remove(false_range, value); break; @@ -1655,14 +1623,31 @@ static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enu /** Transfer function for -{execut,read,writ}able. */ static struct bfs_expr *data_flow_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { - if (expr->num & R_OK) { + switch (expr->num) { + case R_OK: data_flow_pred(opt, READABLE_PRED, true); - } - if (expr->num & W_OK) { + break; + case W_OK: data_flow_pred(opt, WRITABLE_PRED, true); - } - if (expr->num & X_OK) { + break; + case X_OK: data_flow_pred(opt, EXECUTABLE_PRED, true); + break; + default: + bfs_bug("Unknown access() mode %lld", expr->num); + break; + } + + return expr; +} + +/** Transfer function for -empty. */ +static struct bfs_expr *data_flow_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.types &= (1 << BFS_REG) | (1 << BFS_DIR); + + if (opt->before.types == (1 << BFS_REG)) { + constrain_range(&opt->after_true.ranges[SIZE_RANGE], 0); + range_remove(&opt->after_false.ranges[SIZE_RANGE], 0); } return expr; @@ -1675,7 +1660,7 @@ static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr gid_t gid = range->min; bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid); if (errno == 0) { - data_flow_pred(opt, NOGROUP_PRED, nogroup); + constrain_pred(&opt->after_true.preds[NOGROUP_PRED], nogroup); } } @@ -1706,11 +1691,16 @@ static struct bfs_expr *data_flow_links(struct bfs_opt *opt, struct bfs_expr *ex return expr; } +/** Transfer function for -lname. */ +static struct bfs_expr *data_flow_lname(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + opt->after_true.types &= 1 << BFS_LNK; + return expr; +} + /** Transfer function for -samefile. */ static struct bfs_expr *data_flow_samefile(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { struct df_range *true_range = &opt->after_true.ranges[INUM_RANGE]; - constrain_min(true_range, expr->ino); - constrain_max(true_range, expr->ino); + constrain_range(true_range, expr->ino); struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE]; range_remove(false_range, expr->ino); @@ -1744,7 +1734,7 @@ static struct bfs_expr *data_flow_uid(struct bfs_opt *opt, struct bfs_expr *expr uid_t uid = range->min; bool nouser = !bfs_getpwuid(opt->ctx->users, uid); if (errno == 0) { - data_flow_pred(opt, NOUSER_PRED, nouser); + constrain_pred(&opt->after_true.preds[NOUSER_PRED], nouser); } } @@ -1818,12 +1808,45 @@ static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *ex return visit_leave(opt, expr, visitor); } -/** Data flow visitor function. */ -static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { - if (opt->ignore_result && expr->pure) { +/** Ignore an expression (and possibly warn/prompt). */ +static struct bfs_expr *opt_ignore(struct bfs_opt *opt, struct bfs_expr *expr, bool delete) { + if (delete) { + opt_delete(opt, "%pe [ignored result]\n", expr); + } else { opt_debug(opt, "ignored result\n"); - opt_warning(opt, expr, "The result of this expression is ignored.\n\n"); + } + + if (expr->kind != BFS_TEST) { + goto done; + } + + if (!opt_warning(opt, expr, "The result of this expression is ignored.\n")) { + goto done; + } + + struct bfs_ctx *ctx = opt->ctx; + if (ctx->interactive && ctx->dangerous) { + bfs_warning(ctx, "Do you want to continue? "); + if (ynprompt() <= 0) { + errno = 0; + return NULL; + } + } + + fprintf(stderr, "\n"); + +done: + if (!delete && expr->pure) { + // If we're not deleting the expression entirely, replace it with -false expr = opt_const(opt, false); + } + return expr; +} + +/** Data flow visitor function. */ +static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) { + if (opt->ignore_result) { + expr = opt_ignore(opt, expr, false); if (!expr) { return NULL; } @@ -1893,9 +1916,11 @@ static const struct visitor data_flow = { .leave = data_flow_leave, .table = (const struct visitor_table[]) { {eval_access, data_flow_access}, + {eval_empty, data_flow_empty}, {eval_gid, data_flow_gid}, {eval_inum, data_flow_inum}, {eval_links, data_flow_links}, + {eval_lname, data_flow_lname}, {eval_samefile, data_flow_samefile}, {eval_size, data_flow_size}, {eval_type, data_flow_type}, @@ -1944,8 +1969,7 @@ static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - struct bfs_expr *child; - while ((child = SLIST_POP(&children))) { + drain_slist (struct bfs_expr, child, &children) { opt_enter(opt, "%pe\n", child); child = negate_expr(opt, child, &fake_not_arg); @@ -1958,6 +1982,10 @@ static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *exp } expr = visit_shallow(opt, expr, &annotate); + if (!expr) { + return NULL; + } + return negate_expr(opt, expr, &fake_not_arg); } @@ -1987,16 +2015,15 @@ static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (child == ignorable) { ignore = true; } if (ignore) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2009,8 +2036,8 @@ static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, bfs_expr_append(expr, child); if (child->always_false) { - while ((child = SLIST_POP(&children))) { - opt_delete(opt, "%pe [short-circuit]\n", child); + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); } } } @@ -2035,16 +2062,15 @@ static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (child == ignorable) { ignore = true; } if (ignore) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2057,8 +2083,8 @@ static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, bfs_expr_append(expr, child); if (child->always_true) { - while ((child = SLIST_POP(&children))) { - opt_delete(opt, "%pe [short-circuit]\n", child); + drain_slist (struct bfs_expr, dead, &children) { + opt_delete(opt, "%pe [short-circuit]\n", dead); } } } @@ -2080,12 +2106,11 @@ static struct bfs_expr *simplify_comma(struct bfs_opt *opt, struct bfs_expr *exp struct bfs_exprs children; foster_children(expr, &children); - while (!SLIST_EMPTY(&children)) { - struct bfs_expr *child = SLIST_POP(&children); - + drain_slist (struct bfs_expr, child, &children) { if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) { - opt_delete(opt, "%pe [ignored result]\n", child); - opt_warning(opt, child, "The result of this expression is ignored.\n\n"); + if (!opt_ignore(opt, child, true)) { + return NULL; + } continue; } @@ -2136,6 +2161,8 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { }; struct df_domain impure; + df_init_top(&opt->after_true); + df_init_top(&opt->after_false); for (int i = 0; i < 3; ++i) { struct bfs_opt nested = *opt; @@ -2149,9 +2176,11 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { continue; } + const struct visitor *visitor = passes[j].visitor; + // Skip reordering the first time through the passes, to // make warnings more understandable - if (passes[j].visitor == &reorder) { + if (visitor == &reorder) { if (i == 0) { continue; } else { @@ -2159,10 +2188,15 @@ static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) { } } - expr = visit(&nested, expr, passes[j].visitor); + expr = visit(&nested, expr, visitor); if (!expr) { return NULL; } + + if (visitor == &data_flow) { + opt->after_true = nested.after_true; + opt->after_false = nested.after_false; + } } opt_leave(&nested, NULL); @@ -13,7 +13,7 @@ struct bfs_ctx; /** * Apply optimizations to the command line. * - * @param ctx + * @ctx * The bfs context to optimize. * @return * 0 if successful, -1 on error. diff --git a/src/parse.c b/src/parse.c index 539aa05..5ec4c0e 100644 --- a/src/parse.c +++ b/src/parse.c @@ -8,9 +8,10 @@ * flags like always-true options, and skipping over paths wherever they appear. */ -#include "prelude.h" #include "parse.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" @@ -31,6 +32,7 @@ #include "xregex.h" #include "xspawn.h" #include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <fnmatch.h> @@ -78,14 +80,10 @@ struct bfs_parser { /** Whether stdout is a terminal. */ bool stdout_tty; - /** Whether this session is interactive (stdin and stderr are each a terminal). */ - bool interactive; /** Whether -color or -nocolor has been passed. */ enum use_color use_color; /** Whether a -print action is implied. */ bool implicit_print; - /** Whether the default root "." should be used. */ - bool implicit_root; /** Whether the expression has started. */ bool expr_started; /** Whether an information option like -help or -version was passed. */ @@ -95,44 +93,26 @@ struct bfs_parser { /** The last non-path argument. */ char **last_arg; - /** A "-depth"-type argument, if any. */ - char **depth_arg; - /** A "-limit" argument, if any. */ - char **limit_arg; - /** A "-prune" argument, if any. */ - char **prune_arg; - /** A "-mount" argument, if any. */ - char **mount_arg; - /** An "-xdev" argument, if any. */ - char **xdev_arg; - /** A "-files0-from -" argument, if any. */ - char **files0_stdin_arg; - /** An "-ok"-type expression, if any. */ - const struct bfs_expr *ok_expr; + /** A "-depth"-type expression, if any. */ + const struct bfs_expr *depth_expr; + /** A "-limit" expression, if any. */ + const struct bfs_expr *limit_expr; + /** A "-prune" expression, if any. */ + const struct bfs_expr *prune_expr; + /** A "-mount" expression, if any. */ + const struct bfs_expr *mount_expr; + /** An "-xdev" expression, if any. */ + const struct bfs_expr *xdev_expr; + /** A "-files0-from" expression, if any. */ + const struct bfs_expr *files0_expr; + /** An expression that consumes stdin, if any. */ + const struct bfs_expr *stdin_expr; /** The current time (maybe modified by -daystart). */ struct timespec now; }; /** - * Possible token types. - */ -enum token_type { - /** A flag. */ - T_FLAG, - /** A root path. */ - T_PATH, - /** An option. */ - T_OPTION, - /** A test. */ - T_TEST, - /** An action. */ - T_ACTION, - /** An operator. */ - T_OPERATOR, -}; - -/** * Print a low-level error message during parsing. */ static void parse_perror(const struct bfs_parser *parser, const char *str) { @@ -158,9 +138,8 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, /** * Print an error message during parsing. */ -attr(printf(2, 3)) +_printf(2, 3) static void parse_error(const struct bfs_parser *parser, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -170,7 +149,6 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ... va_list args; va_start(args, format); - errno = error; bfs_verror(parser->ctx, format, args); va_end(args); } @@ -178,9 +156,8 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ... /** * Print an error about some command line arguments. */ -attr(printf(4, 5)) +_printf(4, 5) static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -190,7 +167,6 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -198,20 +174,18 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ /** * Print an error about conflicting command line arguments. */ -attr(printf(6, 7)) -static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; +_printf(4, 5) +static void parse_conflict_error(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); bfs_argv_error(ctx, highlight); va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -219,16 +193,14 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, /** * Print an error about an expression. */ -attr(printf(3, 4)) +_printf(3, 4) static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bfs_expr_error(ctx, expr); va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -236,9 +208,8 @@ static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_e /** * Print a warning message during parsing. */ -attr(printf(2, 3)) +_printf(2, 3) static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -250,7 +221,6 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(parser->ctx, format, args); va_end(args); return ret; @@ -259,22 +229,20 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . /** * Print a warning about conflicting command line arguments. */ -attr(printf(6, 7)) -static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; +_printf(4, 5) +static bool parse_conflict_warning(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) { const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; init_highlight(ctx, highlight); - highlight_args(ctx, argv1, argc1, highlight); - highlight_args(ctx, argv2, argc2, highlight); + highlight_args(ctx, expr1->argv, expr1->argc, highlight); + highlight_args(ctx, expr2->argv, expr2->argc, highlight); if (!bfs_argv_warning(ctx, highlight)) { return false; } va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(ctx, format, args); va_end(args); return ret; @@ -283,9 +251,8 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1 /** * Print a warning about an expression. */ -attr(printf(3, 4)) +_printf(3, 4) static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; if (!bfs_expr_warning(ctx, expr)) { @@ -294,17 +261,31 @@ static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(ctx, format, args); va_end(args); return ret; } /** + * Report an error if stdin is already consumed, then consume it. + */ +static bool consume_stdin(struct bfs_parser *parser, const struct bfs_expr *expr) { + if (parser->stdin_expr) { + parse_conflict_error(parser, parser->stdin_expr, expr, + "%pX and %pX can't both use standard input.\n", + parser->stdin_expr, expr); + return false; + } + + parser->stdin_expr = expr; + return true; +} + +/** * Allocate a new expression. */ -static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv) { - struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv); +static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) { + struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv, kind); if (!expr) { parse_perror(parser, "bfs_expr_new()"); } @@ -315,7 +296,7 @@ static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval * Create a new unary expression. */ static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv); + struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR); if (!expr) { return NULL; } @@ -329,7 +310,7 @@ static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval * Create a new binary expression. */ static struct bfs_expr *new_binary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) { - struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv); + struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR); if (!expr) { return NULL; } @@ -414,15 +395,20 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser); /** * Advance by a single token. */ -static char **parser_advance(struct bfs_parser *parser, enum token_type type, size_t argc) { - if (type != T_FLAG && type != T_PATH) { +static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size_t argc) { + struct bfs_ctx *ctx = parser->ctx; + + if (kind != BFS_FLAG && kind != BFS_PATH) { parser->expr_started = true; } - if (type != T_PATH) { + if (kind != BFS_PATH) { parser->last_arg = parser->argv; } + size_t i = parser->argv - ctx->argv; + ctx->kinds[i] = kind; + char **argv = parser->argv; parser->argv += argc; return argv; @@ -446,7 +432,6 @@ static int parse_root(struct bfs_parser *parser, const char *path) { return -1; } - parser->implicit_root = false; return 0; } @@ -465,7 +450,7 @@ static int skip_paths(struct bfs_parser *parser) { // find uses -- to separate flags from the rest // of the command line. We allow mixing flags // and paths/predicates, so we just ignore --. - parser_advance(parser, T_FLAG, 1); + parser_advance(parser, BFS_FLAG, 1); continue; } if (strcmp(arg, "-") != 0) { @@ -497,7 +482,7 @@ static int skip_paths(struct bfs_parser *parser) { return -1; } - parser_advance(parser, T_PATH, 1); + parser_advance(parser, BFS_PATH, 1); } } @@ -517,20 +502,14 @@ enum int_flags { * Parse an integer. */ static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) { - // strtoll() skips leading spaces, but we want to reject them - if (xisspace(str[0])) { - goto bad; - } - int base = flags & IF_BASE_MASK; if (base == 0) { base = 10; } char *endptr; - errno = 0; - long long value = strtoll(str, &endptr, base); - if (errno != 0) { + long long value; + if (xstrtoll(str, &endptr, base, &value) != 0) { if (errno == ERANGE) { goto range; } else { @@ -538,13 +517,6 @@ static const char *parse_int(const struct bfs_parser *parser, char **arg, const } } - // https://github.com/llvm/llvm-project/issues/64946 - sanitize_init(&endptr); - - if (endptr == str) { - goto bad; - } - if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') { goto bad; } @@ -641,8 +613,8 @@ static bool looks_like_icmp(const char *str) { * Parse a single flag. */ static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) { - char **argv = parser_advance(parser, T_FLAG, argc); - return parse_new_expr(parser, eval_true, argc, argv); + char **argv = parser_advance(parser, BFS_FLAG, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_FLAG); } /** @@ -657,9 +629,11 @@ static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) { */ static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) { const char *arg = parser->argv[0]; + char flag = arg[strlen(arg) - 1]; + const char *value = parser->argv[1]; if (!value) { - parse_error(parser, "${cyn}%s${rs} needs a value.\n", arg); + parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag); return NULL; } @@ -667,11 +641,34 @@ static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) { } /** + * Parse a prefix flag like -O3, -j8, etc. + */ +static struct bfs_expr *parse_prefix_flag(struct bfs_parser *parser, char flag, bool allow_separate, const char **value) { + const char *arg = parser->argv[0]; + + const char *suffix = strchr(arg, flag) + 1; + if (*suffix) { + *value = suffix; + return parse_nullary_flag(parser); + } + + suffix = parser->argv[1]; + if (allow_separate && suffix) { + *value = suffix; + } else { + parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag); + return NULL; + } + + return parse_unary_flag(parser); +} + +/** * Parse a single option. */ static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) { - char **argv = parser_advance(parser, T_OPTION, argc); - return parse_new_expr(parser, eval_true, argc, argv); + char **argv = parser_advance(parser, BFS_OPTION, argc); + return parse_new_expr(parser, eval_true, argc, argv, BFS_OPTION); } /** @@ -699,8 +696,8 @@ static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) { * Parse a single test. */ static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(parser, T_TEST, argc); - return parse_new_expr(parser, eval_fn, argc, argv); + char **argv = parser_advance(parser, BFS_TEST, argc); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_TEST); } /** @@ -728,7 +725,7 @@ static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn * Parse a single action. */ static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) { - char **argv = parser_advance(parser, T_ACTION, argc); + char **argv = parser_advance(parser, BFS_ACTION, argc); if (parser->excluding) { parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n"); @@ -739,7 +736,7 @@ static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eva parser->implicit_print = false; } - return parse_new_expr(parser, eval_fn, argc, argv); + return parse_new_expr(parser, eval_fn, argc, argv, BFS_ACTION); } /** @@ -811,7 +808,8 @@ static bool parse_debug_flag(const char *flag, size_t len, const char *expected) static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) { struct bfs_ctx *ctx = parser->ctx; - struct bfs_expr *expr = parse_unary_flag(parser); + const char *flags; + struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags); if (!expr) { cfprintf(ctx->cerr, "\n"); debug_help(ctx->cerr); @@ -820,7 +818,7 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg bool unrecognized = false; - for (const char *flag = expr->argv[1], *next; flag; flag = next) { + for (const char *flag = flags, *next; flag; flag = next) { size_t len = strcspn(flag, ","); if (flag[len]) { next = flag + len + 1; @@ -868,21 +866,22 @@ static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg * Parse -On. */ static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg); if (!expr) { return NULL; } int *optlevel = &parser->ctx->optlevel; - if (strcmp(expr->argv[0], "-Ofast") == 0) { + if (strcmp(arg, "fast") == 0) { *optlevel = 4; - } else if (!parse_int(parser, expr->argv, expr->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) { + } else if (!parse_int(parser, expr->argv, arg, optlevel, IF_INT | IF_UNSIGNED)) { return NULL; } if (*optlevel > 4) { - parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", expr->argv[0] + 2); + parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", arg); } return expr; @@ -996,16 +995,16 @@ static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg switch (*tail) { case 'w': time *= 7; - fallthru; + _fallthrough; case 'd': time *= 24; - fallthru; + _fallthrough; case 'h': time *= 60; - fallthru; + _fallthrough; case 'm': time *= 60; - fallthru; + _fallthrough; case 's': break; default: @@ -1117,7 +1116,7 @@ static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bf // 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 + // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_14_01 expr->literal = strcspn(expr->pattern, "?*\\[") == len; return expr; @@ -1176,18 +1175,33 @@ static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int * Parse -delete. */ static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) { - parser->ctx->flags |= BFTW_POST_ORDER; - parser->depth_arg = parser->argv; - return parse_nullary_action(parser, eval_delete); + struct bfs_expr *expr = parse_nullary_action(parser, eval_delete); + if (!expr) { + return NULL; + } + + struct bfs_ctx *ctx = parser->ctx; + ctx->flags |= BFTW_POST_ORDER; + ctx->dangerous = true; + + parser->depth_expr = expr; + return expr; } /** * Parse -d. */ -static struct bfs_expr *parse_depth(struct bfs_parser *parser, int arg1, int arg2) { +static struct bfs_expr *parse_depth(struct bfs_parser *parser, int flag, int arg2) { + struct bfs_expr *expr = flag + ? parse_nullary_flag(parser) + : parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_POST_ORDER; - parser->depth_arg = parser->argv; - return parse_nullary_flag(parser); + parser->depth_expr = expr; + return expr; } /** @@ -1233,11 +1247,48 @@ static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg return expr; } +/** Check for unsafe relative paths in $PATH. */ +static const char *unsafe_path(const struct bfs_exec *execbuf) { + if (!(execbuf->flags & BFS_EXEC_CHDIR)) { + // Not -execdir or -okdir + return NULL; + } + + const char *exe = execbuf->tmpl_argv[0]; + if (strchr(exe, '/')) { + // No $PATH lookups for /foo or foo/bar + return NULL; + } + + if (strstr(exe, "{}")) { + // Substituted paths always contain a / + return NULL; + } + + const char *path = getenv("PATH"); + while (path) { + if (path[0] != '/') { + // Relative $PATH component! + return path; + } + + path = strchr(path, ':'); + if (path) { + ++path; + } + } + + // No relative components in $PATH + return NULL; +} + /** * Parse -exec(dir)?/-ok(dir)?. */ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) { - struct bfs_exec *execbuf = bfs_exec_parse(parser->ctx, parser->argv, flags); + struct bfs_ctx *ctx = parser->ctx; + + struct bfs_exec *execbuf = bfs_exec_parse(ctx, parser->argv, flags); if (!execbuf) { return NULL; } @@ -1253,29 +1304,21 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg // For pipe() in bfs_spawn() expr->ephemeral_fds = 2; - if (execbuf->flags & BFS_EXEC_CHDIR) { - // Check for relative paths in $PATH - const char *path = getenv("PATH"); - while (path) { - if (*path != '/') { - size_t len = strcspn(path, ":"); - char *comp = strndup(path, len); - if (comp) { - parse_expr_error(parser, expr, - "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp); - free(comp); - } else { - parse_perror(parser, "strndup()"); - } - return NULL; - } - - path = strchr(path, ':'); - if (path) { - ++path; - } + const char *unsafe = unsafe_path(execbuf); + if (unsafe) { + size_t len = strcspn(unsafe, ":"); + char *comp = strndup(unsafe, len); + if (comp) { + parse_expr_error(parser, expr, + "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp); + free(comp); + } else { + parse_perror(parser, "strndup()"); } + return NULL; + } + if (execbuf->flags & BFS_EXEC_CHDIR) { // To dup() the parent directory if (execbuf->flags & BFS_EXEC_MULTI) { ++expr->persistent_fds; @@ -1285,7 +1328,11 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg } if (execbuf->flags & BFS_EXEC_CONFIRM) { - parser->ok_expr = expr; + if (!consume_stdin(parser, expr)) { + return NULL; + } + } else { + ctx->dangerous = true; } return expr; @@ -1314,11 +1361,17 @@ static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2 * Parse -f PATH. */ static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_ctx *ctx = parser->ctx; + struct bfs_expr *expr = parse_unary_flag(parser); if (!expr) { return NULL; } + // Mark the path as a path, not a regular argument + size_t i = expr->argv - ctx->argv; + ctx->kinds[i + 1] = BFS_PATH; + if (parse_root(parser, expr->argv[1]) != 0) { return NULL; } @@ -1335,50 +1388,14 @@ static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, i return NULL; } - const char *from = expr->argv[1]; - - FILE *file; - if (strcmp(from, "-") == 0) { - file = stdin; - } else { - file = xfopen(from, O_RDONLY | O_CLOEXEC); - } - if (!file) { - parse_expr_error(parser, expr, "%s.\n", errstr()); - return NULL; - } - - while (true) { - char *path = xgetdelim(file, '\0'); - if (!path) { - if (errno) { - goto fail; - } else { - break; - } - } - - int ret = parse_root(parser, path); - free(path); - if (ret != 0) { - goto fail; - } - } - - if (file == stdin) { - parser->files0_stdin_arg = expr->argv; - } else { - fclose(file); - } - - parser->implicit_root = false; + // For compatibility with GNU find, + // + // bfs -files0-from a -files0-from b + // + // should *only* use b, not a. So stash the expression here and only + // process the last one at the end of parsing. + parser->files0_expr = expr; return expr; - -fail: - if (file != stdin) { - fclose(file); - } - return NULL; } /** @@ -1613,13 +1630,14 @@ static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2 * Parse -j<n>. */ static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *expr = parse_nullary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'j', false, &arg); if (!expr) { return NULL; } unsigned int n; - if (!parse_int(parser, expr->argv, expr->argv[0] + 2, &n, IF_INT | IF_UNSIGNED)) { + if (!parse_int(parser, expr->argv, arg, &n, IF_INT | IF_UNSIGNED)) { return NULL; } @@ -1647,11 +1665,11 @@ static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg } if (expr->num <= 0) { - parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]); + parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr); return NULL; } - parser->limit_arg = expr->argv; + parser->limit_expr = expr; return expr; } @@ -1684,11 +1702,8 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg return NULL; } - parse_expr_warning(parser, expr, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", expr->argv[0]); - bfs_warning(parser->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n"); - - parser->ctx->flags |= BFTW_PRUNE_MOUNTS; - parser->mount_arg = expr->argv; + parser->ctx->flags |= BFTW_SKIP_MOUNTS; + parser->mount_expr = expr; return expr; } @@ -1831,6 +1846,14 @@ static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int a } /** + * Parse -noerror. + */ +static struct bfs_expr *parse_noerror(struct bfs_parser *parser, int arg1, int arg2) { + parser->ctx->ignore_errors = true; + return parse_nullary_option(parser); +} + +/** * Parse -nogroup. */ static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) { @@ -1846,7 +1869,7 @@ static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int a * Parse -nohidden. */ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int arg2) { - struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg); + struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg, BFS_TEST); if (!hidden) { return NULL; } @@ -1859,9 +1882,15 @@ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int * Parse -noleaf. */ static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) { - parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", - BFS_COMMAND, parser->argv[0]); - return parse_nullary_option(parser); + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + + parse_expr_warning(parser, expr, + "${ex}%s${rs} does not apply the optimization that %px inhibits.\n\n", + BFS_COMMAND, expr); + return expr; } /** @@ -1894,6 +1923,8 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct return 0; } + mode_t umask = parser->ctx->umask; + expr->file_mode = 0; expr->dir_mode = 0; @@ -1914,7 +1945,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct // // PERMCOPY : "u" | "g" | "o" - // Parser machine parser + // State machine state enum { MODE_CLAUSE, MODE_WHO, @@ -1922,7 +1953,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct MODE_ACTION_APPLY, MODE_OP, MODE_PERM, - } mparser = MODE_CLAUSE; + } state = MODE_CLAUSE; enum { MODE_PLUS, @@ -1931,16 +1962,18 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct } op uninit(MODE_EQUALS); mode_t who uninit(0); + mode_t mask uninit(0); mode_t file_change uninit(0); mode_t dir_change uninit(0); const char *i = mode; while (true) { - switch (mparser) { + switch (state) { case MODE_CLAUSE: who = 0; - mparser = MODE_WHO; - fallthru; + mask = 0777; + state = MODE_WHO; + _fallthrough; case MODE_WHO: switch (*i) { @@ -1957,7 +1990,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct who |= 0777; break; default: - mparser = MODE_ACTION; + state = MODE_ACTION; continue; } break; @@ -1967,7 +2000,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct case MODE_EQUALS: expr->file_mode &= ~who; expr->dir_mode &= ~who; - fallthru; + _fallthrough; case MODE_PLUS: expr->file_mode |= file_change; expr->dir_mode |= dir_change; @@ -1977,37 +2010,40 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct expr->dir_mode &= ~dir_change; break; } - fallthru; + _fallthrough; case MODE_ACTION: if (who == 0) { who = 0777; + mask = who & ~umask; + } else { + mask = who; } switch (*i) { case '+': op = MODE_PLUS; - mparser = MODE_OP; + state = MODE_OP; break; case '-': op = MODE_MINUS; - mparser = MODE_OP; + state = MODE_OP; break; case '=': op = MODE_EQUALS; - mparser = MODE_OP; + state = MODE_OP; break; case ',': - if (mparser == MODE_ACTION_APPLY) { - mparser = MODE_CLAUSE; + if (state == MODE_ACTION_APPLY) { + state = MODE_CLAUSE; } else { goto fail; } break; case '\0': - if (mparser == MODE_ACTION_APPLY) { + if (state == MODE_ACTION_APPLY) { goto done; } else { goto fail; @@ -2036,32 +2072,32 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct default: file_change = 0; dir_change = 0; - mparser = MODE_PERM; + state = MODE_PERM; continue; } file_change |= (file_change << 6) | (file_change << 3); - file_change &= who; + file_change &= mask; dir_change |= (dir_change << 6) | (dir_change << 3); - dir_change &= who; - mparser = MODE_ACTION_APPLY; + dir_change &= mask; + state = MODE_ACTION_APPLY; break; case MODE_PERM: switch (*i) { case 'r': - file_change |= who & 0444; - dir_change |= who & 0444; + file_change |= mask & 0444; + dir_change |= mask & 0444; break; case 'w': - file_change |= who & 0222; - dir_change |= who & 0222; + file_change |= mask & 0222; + dir_change |= mask & 0222; break; case 'x': - file_change |= who & 0111; - fallthru; + file_change |= mask & 0111; + _fallthrough; case 'X': - dir_change |= who & 0111; + dir_change |= mask & 0111; break; case 's': if (who & 0700) { @@ -2080,7 +2116,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct } break; default: - mparser = MODE_ACTION_APPLY; + state = MODE_ACTION_APPLY; continue; } break; @@ -2122,7 +2158,7 @@ static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg ++mode; break; } - fallthru; + _fallthrough; default: expr->mode_cmp = BFS_MODE_EQUAL; break; @@ -2190,8 +2226,13 @@ static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int ar * Parse -prune. */ static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) { - parser->prune_arg = parser->argv; - return parse_nullary_action(parser, eval_prune); + struct bfs_expr *expr = parse_nullary_action(parser, eval_prune); + if (!expr) { + return NULL; + } + + parser->prune_expr = expr; + return expr; } /** @@ -2253,16 +2294,27 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int // See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html const char *type = expr->argv[1]; if (strcmp(type, "posix-basic") == 0 + || strcmp(type, "posix-minimal-basic") == 0 || strcmp(type, "ed") == 0 || strcmp(type, "sed") == 0) { parser->regex_type = BFS_REGEX_POSIX_BASIC; } else if (strcmp(type, "posix-extended") == 0) { parser->regex_type = BFS_REGEX_POSIX_EXTENDED; #if BFS_WITH_ONIGURUMA + } else if (strcmp(type, "awk") == 0 + || strcmp(type, "posix-awk") == 0) { + parser->regex_type = BFS_REGEX_AWK; + } else if (strcmp(type, "gnu-awk") == 0) { + parser->regex_type = BFS_REGEX_GNU_AWK; } else if (strcmp(type, "emacs") == 0) { parser->regex_type = BFS_REGEX_EMACS; } else if (strcmp(type, "grep") == 0) { parser->regex_type = BFS_REGEX_GREP; + } else if (strcmp(type, "egrep") == 0 + || strcmp(type, "posix-egrep") == 0) { + parser->regex_type = BFS_REGEX_EGREP; + } else if (strcmp(type, "findutils-default") == 0) { + parser->regex_type = BFS_REGEX_GNU_FIND; #endif } else if (strcmp(type, "help") == 0) { parser->just_info = true; @@ -2277,14 +2329,23 @@ static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int list_types: cfprintf(cfile, "Supported types are:\n\n"); - cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); - cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n"); - cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); + cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n"); + cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n"); + cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n"); + + cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n"); + #if BFS_WITH_ONIGURUMA - cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n"); - cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n"); + cfprintf(cfile, " ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n"); + + cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n"); + + cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n"); + cfprintf(cfile, " [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n"); + + cfprintf(cfile, " ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n"); #endif - cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n"); return NULL; } @@ -2322,13 +2383,13 @@ static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg struct bfs_ctx *ctx = parser->ctx; CFILE *cfile = ctx->cerr; - struct bfs_expr *expr = parse_unary_flag(parser); + const char *arg; + struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg); if (!expr) { cfprintf(cfile, "\n"); goto list_strategies; } - const char *arg = expr->argv[1]; if (strcmp(arg, "bfs") == 0) { ctx->strategy = BFTW_BFS; } else if (strcmp(arg, "dfs") == 0) { @@ -2549,9 +2610,14 @@ static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int * Parse -xdev. */ static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) { + struct bfs_expr *expr = parse_nullary_option(parser); + if (!expr) { + return NULL; + } + parser->ctx->flags |= BFTW_PRUNE_MOUNTS; - parser->xdev_arg = parser->argv; - return parse_nullary_option(parser); + parser->xdev_expr = expr; + return expr; } /** @@ -2744,8 +2810,9 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2 cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n"); cfprintf(cout, " Ignore files deeper/shallower than ${bld}N${rs}\n"); cfprintf(cout, " ${blu}-mount${rs}\n"); - cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs} for now, but will\n"); - cfprintf(cout, " skip mount points entirely in the future)\n"); + cfprintf(cout, " Exclude mount points entirely from the results\n"); + cfprintf(cout, " ${blu}-noerror${rs}\n"); + cfprintf(cout, " Ignore any errors that occur during traversal\n"); cfprintf(cout, " ${blu}-nohidden${rs}\n"); cfprintf(cout, " Exclude hidden files\n"); cfprintf(cout, " ${blu}-noleaf${rs}\n"); @@ -2922,18 +2989,60 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2 return NULL; } +/** Print the bfs "logo". */ +static void print_logo(CFILE *cout) { + if (!cout->colors) { + goto boring; + } + + size_t vwidth = xstrwidth(bfs_version); + dchar *spaces = dstrepeat(" ", vwidth); + dchar *lines = dstrepeat("─", vwidth); + if (!spaces || !lines) { + dstrfree(lines); + dstrfree(spaces); + goto boring; + } + + // We do ----\r<emoji> rather than <emoji>--- so we don't have to assume + // anything about the width of the emoji + cfprintf(cout, "╭─────%s╮\r📂\n", lines); + cfprintf(cout, "├${ex}b${rs} %s │\n", spaces); + cfprintf(cout, "╰├${ex}f${rs} ${bld}%s${rs} │\n", bfs_version); + cfprintf(cout, " ╰├${ex}s${rs} %s │\n", spaces); + cfprintf(cout, " ╰──%s─╯\n\n", lines); + + dstrfree(lines); + dstrfree(spaces); + return; + +boring: + printf("%s %s\n\n", BFS_COMMAND, bfs_version); +} + /** * "Parse" -version. */ static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) { - cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version); + print_logo(parser->ctx->cout); + + printf("Copyright © Tavian Barnes and the bfs contributors\n"); + printf("No rights reserved (https://opensource.org/license/0BSD)\n\n"); + + printf("CONFFLAGS := %s\n", bfs_confflags); + printf("CC := %s\n", bfs_cc); + printf("CPPFLAGS := %s\n", bfs_cppflags); + printf("CFLAGS := %s\n", bfs_cflags); + printf("LDFLAGS := %s\n", bfs_ldflags); + printf("LDLIBS := %s\n", bfs_ldlibs); - printf("%s\n", BFS_HOMEPAGE); + printf("\n%s\n", BFS_HOMEPAGE); parser->just_info = true; return NULL; } +/** Parser callback function type. */ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2); /** @@ -2941,137 +3050,139 @@ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2) */ struct table_entry { char *arg; - enum token_type type; + enum bfs_kind kind; parse_fn *parse; int arg1; int arg2; bool prefix; + bool needs_arg; }; /** * The parse table for primary expressions. */ static const struct table_entry parse_table[] = { - {"--", T_FLAG}, - {"--help", T_ACTION, parse_help}, - {"--version", T_ACTION, parse_version}, - {"-Bmin", T_TEST, parse_min, BFS_STAT_BTIME}, - {"-Bnewer", T_TEST, parse_newer, BFS_STAT_BTIME}, - {"-Bsince", T_TEST, parse_since, BFS_STAT_BTIME}, - {"-Btime", T_TEST, parse_time, BFS_STAT_BTIME}, - {"-D", T_FLAG, parse_debug}, - {"-E", T_FLAG, parse_regex_extended}, - {"-H", T_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false}, - {"-L", T_FLAG, parse_follow, BFTW_FOLLOW_ALL, false}, - {"-O", T_FLAG, parse_optlevel, 0, 0, true}, - {"-P", T_FLAG, parse_follow, 0, false}, - {"-S", T_FLAG, parse_search_strategy}, - {"-X", T_FLAG, parse_xargs_safe}, - {"-a", T_OPERATOR}, - {"-acl", T_TEST, parse_acl}, - {"-amin", T_TEST, parse_min, BFS_STAT_ATIME}, - {"-and", T_OPERATOR}, - {"-anewer", T_TEST, parse_newer, BFS_STAT_ATIME}, - {"-asince", T_TEST, parse_since, BFS_STAT_ATIME}, - {"-atime", T_TEST, parse_time, BFS_STAT_ATIME}, - {"-capable", T_TEST, parse_capable}, - {"-cmin", T_TEST, parse_min, BFS_STAT_CTIME}, - {"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME}, - {"-color", T_OPTION, parse_color, true}, - {"-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}, - {"-daystart", T_OPTION, parse_daystart}, - {"-delete", T_ACTION, parse_delete}, - {"-depth", T_OPTION, parse_depth_n}, - {"-empty", T_TEST, parse_empty}, - {"-exclude", T_OPERATOR}, - {"-exec", T_ACTION, parse_exec, 0}, - {"-execdir", T_ACTION, parse_exec, BFS_EXEC_CHDIR}, - {"-executable", T_TEST, parse_access, X_OK}, - {"-exit", T_ACTION, parse_exit}, - {"-f", T_FLAG, parse_f}, - {"-false", T_TEST, parse_const, false}, - {"-files0-from", T_OPTION, parse_files0_from}, - {"-flags", T_TEST, parse_flags}, - {"-fls", T_ACTION, parse_fls}, - {"-follow", T_OPTION, parse_follow, BFTW_FOLLOW_ALL, true}, - {"-fprint", T_ACTION, parse_fprint}, - {"-fprint0", T_ACTION, parse_fprint0}, - {"-fprintf", T_ACTION, parse_fprintf}, - {"-fstype", T_TEST, parse_fstype}, - {"-gid", T_TEST, parse_group}, - {"-group", T_TEST, parse_group}, - {"-help", T_ACTION, parse_help}, - {"-hidden", T_TEST, parse_hidden}, - {"-ignore_readdir_race", T_OPTION, parse_ignore_races, true}, - {"-ilname", T_TEST, parse_lname, true}, - {"-iname", T_TEST, parse_name, true}, - {"-inum", T_TEST, parse_inum}, - {"-ipath", T_TEST, parse_path, true}, - {"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE}, - {"-iwholename", T_TEST, parse_path, true}, - {"-j", T_FLAG, parse_jobs, 0, 0, true}, - {"-limit", T_ACTION, parse_limit}, - {"-links", T_TEST, parse_links}, - {"-lname", T_TEST, parse_lname, false}, - {"-ls", T_ACTION, parse_ls}, - {"-maxdepth", T_OPTION, parse_depth_limit, false}, - {"-mindepth", T_OPTION, parse_depth_limit, true}, - {"-mmin", T_TEST, parse_min, BFS_STAT_MTIME}, - {"-mnewer", T_TEST, parse_newer, BFS_STAT_MTIME}, - {"-mount", T_OPTION, parse_mount}, - {"-msince", T_TEST, parse_since, BFS_STAT_MTIME}, - {"-mtime", T_TEST, parse_time, BFS_STAT_MTIME}, - {"-name", T_TEST, parse_name, false}, - {"-newer", T_TEST, parse_newer, BFS_STAT_MTIME}, - {"-newer", T_TEST, parse_newerxy, 0, 0, true}, - {"-nocolor", T_OPTION, parse_color, false}, - {"-nogroup", T_TEST, parse_nogroup}, - {"-nohidden", T_TEST, parse_nohidden}, - {"-noignore_readdir_race", T_OPTION, parse_ignore_races, false}, - {"-noleaf", T_OPTION, parse_noleaf}, - {"-not", T_OPERATOR}, - {"-nouser", T_TEST, parse_nouser}, - {"-nowarn", T_OPTION, parse_warn, false}, - {"-o", T_OPERATOR}, - {"-ok", T_ACTION, parse_exec, BFS_EXEC_CONFIRM}, - {"-okdir", T_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR}, - {"-or", T_OPERATOR}, - {"-path", T_TEST, parse_path, false}, - {"-perm", T_TEST, parse_perm}, - {"-print", T_ACTION, parse_print}, - {"-print0", T_ACTION, parse_print0}, - {"-printf", T_ACTION, parse_printf}, - {"-printx", T_ACTION, parse_printx}, - {"-prune", T_ACTION, parse_prune}, - {"-quit", T_ACTION, parse_quit}, - {"-readable", T_TEST, parse_access, R_OK}, - {"-regex", T_TEST, parse_regex, 0}, - {"-regextype", T_OPTION, parse_regextype}, - {"-rm", T_ACTION, parse_delete}, - {"-s", T_FLAG, parse_s}, - {"-samefile", T_TEST, parse_samefile}, - {"-since", T_TEST, parse_since, BFS_STAT_MTIME}, - {"-size", T_TEST, parse_size}, - {"-sparse", T_TEST, parse_sparse}, - {"-status", T_OPTION, parse_status}, - {"-true", T_TEST, parse_const, true}, - {"-type", T_TEST, parse_type, false}, - {"-uid", T_TEST, parse_user}, - {"-unique", T_OPTION, parse_unique}, - {"-used", T_TEST, parse_used}, - {"-user", T_TEST, parse_user}, - {"-version", T_ACTION, parse_version}, - {"-warn", T_OPTION, parse_warn, true}, - {"-wholename", T_TEST, parse_path, false}, - {"-writable", T_TEST, parse_access, W_OK}, - {"-x", T_FLAG, parse_xdev}, - {"-xattr", T_TEST, parse_xattr}, - {"-xattrname", T_TEST, parse_xattrname}, - {"-xdev", T_OPTION, parse_xdev}, - {"-xtype", T_TEST, parse_type, true}, + {"--", BFS_FLAG}, + {"--help", BFS_ACTION, parse_help}, + {"--version", BFS_ACTION, parse_version}, + {"-Bmin", BFS_TEST, parse_min, BFS_STAT_BTIME}, + {"-Bnewer", BFS_TEST, parse_newer, BFS_STAT_BTIME}, + {"-Bsince", BFS_TEST, parse_since, BFS_STAT_BTIME}, + {"-Btime", BFS_TEST, parse_time, BFS_STAT_BTIME}, + {"-D", BFS_FLAG, parse_debug, .prefix = true}, + {"-E", BFS_FLAG, parse_regex_extended}, + {"-H", BFS_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false}, + {"-L", BFS_FLAG, parse_follow, BFTW_FOLLOW_ALL, false}, + {"-O", BFS_FLAG, parse_optlevel, .prefix = true}, + {"-P", BFS_FLAG, parse_follow, 0, false}, + {"-S", BFS_FLAG, parse_search_strategy, .prefix = true}, + {"-X", BFS_FLAG, parse_xargs_safe}, + {"-a", BFS_OPERATOR}, + {"-acl", BFS_TEST, parse_acl}, + {"-amin", BFS_TEST, parse_min, BFS_STAT_ATIME}, + {"-and", BFS_OPERATOR}, + {"-anewer", BFS_TEST, parse_newer, BFS_STAT_ATIME}, + {"-asince", BFS_TEST, parse_since, BFS_STAT_ATIME}, + {"-atime", BFS_TEST, parse_time, BFS_STAT_ATIME}, + {"-capable", BFS_TEST, parse_capable}, + {"-cmin", BFS_TEST, parse_min, BFS_STAT_CTIME}, + {"-cnewer", BFS_TEST, parse_newer, BFS_STAT_CTIME}, + {"-color", BFS_OPTION, parse_color, true}, + {"-context", BFS_TEST, parse_context, true}, + {"-csince", BFS_TEST, parse_since, BFS_STAT_CTIME}, + {"-ctime", BFS_TEST, parse_time, BFS_STAT_CTIME}, + {"-d", BFS_FLAG, parse_depth, true}, + {"-daystart", BFS_OPTION, parse_daystart}, + {"-delete", BFS_ACTION, parse_delete}, + {"-depth", BFS_OPTION, parse_depth_n, false}, + {"-empty", BFS_TEST, parse_empty}, + {"-exclude", BFS_OPERATOR}, + {"-exec", BFS_ACTION, parse_exec, 0}, + {"-execdir", BFS_ACTION, parse_exec, BFS_EXEC_CHDIR}, + {"-executable", BFS_TEST, parse_access, X_OK}, + {"-exit", BFS_ACTION, parse_exit}, + {"-f", BFS_FLAG, parse_f, .needs_arg = true}, + {"-false", BFS_TEST, parse_const, false}, + {"-files0-from", BFS_OPTION, parse_files0_from}, + {"-flags", BFS_TEST, parse_flags}, + {"-fls", BFS_ACTION, parse_fls}, + {"-follow", BFS_OPTION, parse_follow, BFTW_FOLLOW_ALL, true}, + {"-fprint", BFS_ACTION, parse_fprint}, + {"-fprint0", BFS_ACTION, parse_fprint0}, + {"-fprintf", BFS_ACTION, parse_fprintf}, + {"-fstype", BFS_TEST, parse_fstype}, + {"-gid", BFS_TEST, parse_group}, + {"-group", BFS_TEST, parse_group}, + {"-help", BFS_ACTION, parse_help}, + {"-hidden", BFS_TEST, parse_hidden}, + {"-ignore_readdir_race", BFS_OPTION, parse_ignore_races, true}, + {"-ilname", BFS_TEST, parse_lname, true}, + {"-iname", BFS_TEST, parse_name, true}, + {"-inum", BFS_TEST, parse_inum}, + {"-ipath", BFS_TEST, parse_path, true}, + {"-iregex", BFS_TEST, parse_regex, BFS_REGEX_ICASE}, + {"-iwholename", BFS_TEST, parse_path, true}, + {"-j", BFS_FLAG, parse_jobs, .prefix = true}, + {"-limit", BFS_ACTION, parse_limit}, + {"-links", BFS_TEST, parse_links}, + {"-lname", BFS_TEST, parse_lname, false}, + {"-ls", BFS_ACTION, parse_ls}, + {"-maxdepth", BFS_OPTION, parse_depth_limit, false}, + {"-mindepth", BFS_OPTION, parse_depth_limit, true}, + {"-mmin", BFS_TEST, parse_min, BFS_STAT_MTIME}, + {"-mnewer", BFS_TEST, parse_newer, BFS_STAT_MTIME}, + {"-mount", BFS_OPTION, parse_mount}, + {"-msince", BFS_TEST, parse_since, BFS_STAT_MTIME}, + {"-mtime", BFS_TEST, parse_time, BFS_STAT_MTIME}, + {"-name", BFS_TEST, parse_name, false}, + {"-newer", BFS_TEST, parse_newer, BFS_STAT_MTIME}, + {"-newer", BFS_TEST, parse_newerxy, .prefix = true}, + {"-nocolor", BFS_OPTION, parse_color, false}, + {"-noerror", BFS_OPTION, parse_noerror}, + {"-nogroup", BFS_TEST, parse_nogroup}, + {"-nohidden", BFS_TEST, parse_nohidden}, + {"-noignore_readdir_race", BFS_OPTION, parse_ignore_races, false}, + {"-noleaf", BFS_OPTION, parse_noleaf}, + {"-not", BFS_OPERATOR}, + {"-nouser", BFS_TEST, parse_nouser}, + {"-nowarn", BFS_OPTION, parse_warn, false}, + {"-o", BFS_OPERATOR}, + {"-ok", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM}, + {"-okdir", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR}, + {"-or", BFS_OPERATOR}, + {"-path", BFS_TEST, parse_path, false}, + {"-perm", BFS_TEST, parse_perm}, + {"-print", BFS_ACTION, parse_print}, + {"-print0", BFS_ACTION, parse_print0}, + {"-printf", BFS_ACTION, parse_printf}, + {"-printx", BFS_ACTION, parse_printx}, + {"-prune", BFS_ACTION, parse_prune}, + {"-quit", BFS_ACTION, parse_quit}, + {"-readable", BFS_TEST, parse_access, R_OK}, + {"-regex", BFS_TEST, parse_regex, 0}, + {"-regextype", BFS_OPTION, parse_regextype}, + {"-rm", BFS_ACTION, parse_delete}, + {"-s", BFS_FLAG, parse_s}, + {"-samefile", BFS_TEST, parse_samefile}, + {"-since", BFS_TEST, parse_since, BFS_STAT_MTIME}, + {"-size", BFS_TEST, parse_size}, + {"-sparse", BFS_TEST, parse_sparse}, + {"-status", BFS_OPTION, parse_status}, + {"-true", BFS_TEST, parse_const, true}, + {"-type", BFS_TEST, parse_type, false}, + {"-uid", BFS_TEST, parse_user}, + {"-unique", BFS_OPTION, parse_unique}, + {"-used", BFS_TEST, parse_used}, + {"-user", BFS_TEST, parse_user}, + {"-version", BFS_ACTION, parse_version}, + {"-warn", BFS_OPTION, parse_warn, true}, + {"-wholename", BFS_TEST, parse_path, false}, + {"-writable", BFS_TEST, parse_access, W_OK}, + {"-x", BFS_FLAG, parse_xdev}, + {"-xattr", BFS_TEST, parse_xattr}, + {"-xattrname", BFS_TEST, parse_xattrname}, + {"-xdev", BFS_OPTION, parse_xdev}, + {"-xtype", BFS_TEST, parse_type, true}, {0}, }; @@ -3092,6 +3203,83 @@ static const struct table_entry *table_lookup(const char *arg) { return NULL; } +/** Look up a single-character flag in the parse table. */ +static const struct table_entry *flag_lookup(char flag) { + for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { + enum bfs_kind kind = entry->kind; + if (kind == BFS_FLAG && entry->arg[1] == flag && !entry->arg[2]) { + return entry; + } + } + + return NULL; +} + +/** Check for a multi-flag argument like -LEXO2. */ +static bool is_flag_group(const char *arg) { + // We enforce that at least one flag in a flag group must be a capital + // letter, to avoid ambiguity with primary expressions + bool has_upper = false; + + // Flags that take an argument must appear last + bool needs_arg = false; + + for (size_t i = 1; arg[i]; ++i) { + char c = arg[i]; + if (c >= 'A' && c <= 'Z') { + has_upper = true; + } + + if (needs_arg) { + return false; + } + + const struct table_entry *entry = flag_lookup(c); + if (!entry || !entry->parse) { + return false; + } + + if (entry->prefix) { + // The rest is the flag's argument + break; + } + + needs_arg |= entry->needs_arg; + } + + return has_upper; +} + +/** Parse a multi-flag argument. */ +static struct bfs_expr *parse_flag_group(struct bfs_parser *parser) { + struct bfs_expr *expr = NULL; + + char **start = parser->argv; + char **end = start; + const char *arg = start[0]; + + for (size_t i = 1; arg[i]; ++i) { + parser->argv = start; + + const struct table_entry *entry = flag_lookup(arg[i]); + expr = entry->parse(parser, entry->arg1, entry->arg2); + + if (parser->argv > end) { + end = parser->argv; + } + + if (!expr || entry->prefix) { + break; + } + } + + if (expr) { + bfs_assert(parser->argv == end, "Didn't eat enough tokens"); + } + + return expr; +} + /** Search for a fuzzy match in the parse table. */ static const struct table_entry *table_lookup_fuzzy(const char *arg) { const struct table_entry *best = NULL; @@ -3114,6 +3302,8 @@ static const struct table_entry *table_lookup_fuzzy(const char *arg) { * | ACTION */ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { + struct bfs_ctx *ctx = parser->ctx; + // Paths are already skipped at this point const char *arg = parser->argv[0]; @@ -3130,15 +3320,19 @@ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { } } + if (is_flag_group(arg)) { + return parse_flag_group(parser); + } + match = table_lookup_fuzzy(arg); - CFILE *cerr = parser->ctx->cerr; + CFILE *cerr = ctx->cerr; parse_error(parser, "Unknown argument; did you mean "); - switch (match->type) { - case T_FLAG: + switch (match->kind) { + case BFS_FLAG: cfprintf(cerr, "${cyn}%s${rs}?", match->arg); break; - case T_OPERATOR: + case BFS_OPERATOR: cfprintf(cerr, "${red}%s${rs}?", match->arg); break; default: @@ -3146,7 +3340,7 @@ static struct bfs_expr *parse_primary(struct bfs_parser *parser) { break; } - if (!parser->interactive || !match->parse) { + if (!ctx->interactive || !match->parse) { fprintf(stderr, "\n"); goto unmatched; } @@ -3188,7 +3382,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { } if (strcmp(arg, "(") == 0) { - parser_advance(parser, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *expr = parse_expr(parser); if (!expr) { @@ -3205,7 +3399,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { return NULL; } - parser_advance(parser, T_OPERATOR, 1); + parser_advance(parser, BFS_OPERATOR, 1); return expr; } else if (strcmp(arg, "-exclude") == 0) { if (parser->excluding) { @@ -3213,7 +3407,7 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { return NULL; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); parser->excluding = true; struct bfs_expr *factor = parse_factor(parser); @@ -3224,9 +3418,9 @@ static struct bfs_expr *parse_factor(struct bfs_parser *parser) { parser->excluding = false; bfs_expr_append(parser->ctx->exclude, factor); - return parse_new_expr(parser, eval_true, parser->argv - argv, argv); + return parse_new_expr(parser, eval_true, parser->argv - argv, argv, BFS_OPERATOR); } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) { - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *factor = parse_factor(parser); if (!factor) { @@ -3266,7 +3460,7 @@ static struct bfs_expr *parse_term(struct bfs_parser *parser) { char **argv = &fake_and_arg; if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) { - argv = parser_advance(parser, T_OPERATOR, 1); + argv = parser_advance(parser, BFS_OPERATOR, 1); } struct bfs_expr *lhs = term; @@ -3303,7 +3497,7 @@ static struct bfs_expr *parse_clause(struct bfs_parser *parser) { break; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = clause; struct bfs_expr *rhs = parse_term(parser); @@ -3338,7 +3532,7 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) { break; } - char **argv = parser_advance(parser, T_OPERATOR, 1); + char **argv = parser_advance(parser, BFS_OPERATOR, 1); struct bfs_expr *lhs = expr; struct bfs_expr *rhs = parse_clause(parser); @@ -3352,10 +3546,79 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) { return expr; } +/** Handle -files0-from after parsing. */ +static int parse_files0_roots(struct bfs_parser *parser) { + const struct bfs_ctx *ctx = parser->ctx; + const struct bfs_expr *expr = parser->files0_expr; + + if (ctx->npaths > 0) { + bool highlight[ctx->argc]; + init_highlight(ctx, highlight); + highlight_args(ctx, expr->argv, expr->argc, highlight); + + for (size_t i = 0; i < ctx->argc; ++i) { + if (ctx->kinds[i] == BFS_PATH) { + highlight[i] = true; + } + } + + bfs_argv_error(ctx, highlight); + bfs_error(ctx, "Cannot combine %pX with explicit root paths.\n", expr); + return -1; + } + + const char *from = expr->argv[1]; + + FILE *file; + if (strcmp(from, "-") == 0) { + if (!consume_stdin(parser, expr)) { + return -1; + } + file = stdin; + } else { + file = xfopen(from, O_RDONLY | O_CLOEXEC); + } + if (!file) { + parse_expr_error(parser, expr, "%s.\n", errstr()); + return -1; + } + + while (true) { + char *path = xgetdelim(file, '\0'); + if (!path) { + if (errno) { + goto fail; + } else { + break; + } + } + + int ret = parse_root(parser, path); + free(path); + if (ret != 0) { + goto fail; + } + } + + if (file != stdin) { + fclose(file); + } + + return 0; + +fail: + if (file != stdin) { + fclose(file); + } + return -1; +} + /** * Parse the top-level expression. */ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { + struct bfs_ctx *ctx = parser->ctx; + if (skip_paths(parser) != 0) { return NULL; } @@ -3364,7 +3627,7 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { if (parser->argv[0]) { expr = parse_expr(parser); } else { - expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg); + expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg, BFS_TEST); } if (!expr) { return NULL; @@ -3375,16 +3638,26 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { return NULL; } + if (parser->files0_expr) { + if (parse_files0_roots(parser) != 0) { + return NULL; + } + } else if (ctx->npaths == 0) { + if (parse_root(parser, ".") != 0) { + return NULL; + } + } + if (parser->implicit_print) { - char **limit = parser->limit_arg; + const struct bfs_expr *limit = parser->limit_expr; if (limit) { - parse_argv_error(parser, parser->limit_arg, 2, - "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n", - limit[0], limit[0], limit[1]); + parse_expr_error(parser, limit, + "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n", + limit, limit); return NULL; } - struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg); + struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg, BFS_ACTION); if (!print) { return NULL; } @@ -3396,20 +3669,20 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { } } - if (parser->mount_arg && parser->xdev_arg) { - parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1, - "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", - parser->xdev_arg[0], parser->mount_arg[0]); + if (parser->mount_expr && parser->xdev_expr) { + parse_conflict_warning(parser, parser->mount_expr, parser->xdev_expr, + "%px is redundant in the presence of %px.\n\n", + parser->xdev_expr, parser->mount_expr); } - if (parser->ctx->warn && parser->depth_arg && parser->prune_arg) { - parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1, - "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", - parser->prune_arg[0], parser->depth_arg[0]); + if (ctx->warn && parser->depth_expr && parser->prune_expr) { + parse_conflict_warning(parser, parser->depth_expr, parser->prune_expr, + "%px does not work in the presence of %px.\n", + parser->prune_expr, parser->depth_expr); - if (parser->interactive) { - bfs_warning(parser->ctx, "Do you want to continue? "); - if (ynprompt() == 0) { + if (ctx->interactive) { + bfs_warning(ctx, "Do you want to continue? "); + if (ynprompt() <= 0) { return NULL; } } @@ -3417,13 +3690,6 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) { fprintf(stderr, "\n"); } - if (parser->ok_expr && parser->files0_stdin_arg) { - parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2, - "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n", - parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]); - return NULL; - } - return expr; } @@ -3605,6 +3871,12 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { goto fail; } + ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc); + if (!ctx->kinds) { + perror("zalloc()"); + goto fail; + } + enum use_color use_color = COLOR_AUTO; const char *no_color = getenv("NO_COLOR"); if (no_color && *no_color) { @@ -3643,6 +3915,7 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { } else { ctx->warn = stdin_tty; } + ctx->interactive = stdin_tty && stderr_tty; struct bfs_parser parser = { .ctx = ctx, @@ -3650,23 +3923,20 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { .command = ctx->argv[0], .regex_type = BFS_REGEX_POSIX_BASIC, .stdout_tty = stdout_tty, - .interactive = stdin_tty && stderr_tty, .use_color = use_color, .implicit_print = true, - .implicit_root = true, .just_info = false, .excluding = false, .last_arg = NULL, - .depth_arg = NULL, - .prune_arg = NULL, - .mount_arg = NULL, - .xdev_arg = NULL, - .files0_stdin_arg = NULL, - .ok_expr = NULL, + .depth_expr = NULL, + .prune_expr = NULL, + .mount_expr = NULL, + .xdev_expr = NULL, + .stdin_expr = NULL, .now = ctx->now, }; - ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg); + ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg, BFS_OPERATOR); if (!ctx->exclude) { goto fail; } @@ -3685,14 +3955,10 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) { } if (bfs_optimize(ctx) != 0) { - bfs_perror(ctx, "bfs_optimize()"); - goto fail; - } - - if (ctx->npaths == 0 && parser.implicit_root) { - if (parse_root(&parser, ".") != 0) { - goto fail; + if (errno != 0) { + bfs_perror(ctx, "bfs_optimize()"); } + goto fail; } if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) { diff --git a/src/parse.h b/src/parse.h index 6895c9f..fcc8234 100644 --- a/src/parse.h +++ b/src/parse.h @@ -11,9 +11,9 @@ /** * Parse the command line. * - * @param argc + * @argc * The number of arguments. - * @param argv + * @argv * The arguments to parse. * @return * A new bfs context, or NULL on failure. diff --git a/src/prelude.h b/src/prelude.h index 3521fe8..de89a6c 100644 --- a/src/prelude.h +++ b/src/prelude.h @@ -2,355 +2,129 @@ // SPDX-License-Identifier: 0BSD /** - * Configuration and feature/platform detection. + * Praeludium. + * + * This header is automatically included in every translation unit, before any + * other headers, so it can set feature test macros[1][2]. This sets up our own + * mini-dialect of C, which includes + * + * - Standard C17 and POSIX.1 2024 features + * - Portable and platform-specific extensions + * - Convenience macros like `bool`, `alignof`, etc. + * - Common compiler extensions like __has_include() + * + * Further bfs-specific utilities are defined in "bfs.h". + * + * [1]: https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html + * [2]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html */ #ifndef BFS_PRELUDE_H #define BFS_PRELUDE_H -// Possible __STDC_VERSION__ values - -#define C95 199409L -#define C99 199901L -#define C11 201112L -#define C17 201710L -#define C23 202311L - -// Get the static_assert() definition as well as __GLIBC__ -#include <assert.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 +// Feature test macros -#ifdef __has_include +/** + * Linux and BSD handle _POSIX_C_SOURCE differently: on Linux, it enables POSIX + * interfaces that are not visible by default. On BSD, it also *disables* most + * extensions, giving a strict POSIX environment. Since we want the extensions, + * we don't set _POSIX_C_SOURCE. + */ +// #define _POSIX_C_SOURCE 202405L -#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 +/** openat() etc. */ +#define _ATFILE_SOURCE 1 -#else // !__has_include +/** BSD-derived extensions. */ +#define _BSD_SOURCE 1 -#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__ +/** glibc successor to _BSD_SOURCE. */ +#define _DEFAULT_SOURCE 1 -#endif // !__has_include +/** GNU extensions. */ +#define _GNU_SOURCE 1 -#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 +/** Use 64-bit off_t. */ +#define _FILE_OFFSET_BITS 64 -// Stub out feature detection on old/incompatible compilers +/** Use 64-bit time_t. */ +#define _TIME_BITS 64 -#ifndef __has_feature -# define __has_feature(feat) false +/** macOS extensions. */ +#if __APPLE__ +# define _DARWIN_C_SOURCE 1 #endif -#ifndef __has_c_attribute -# define __has_c_attribute(attr) false +/** Solaris extensions. */ +#if __sun +# define __EXTENSIONS__ 1 +// https://illumos.org/man/3C/getpwnam#standard-conforming +# define _POSIX_PTHREAD_SEMANTICS 1 #endif -#ifndef __has_attribute -# define __has_attribute(attr) false +/** QNX extensions. */ +#if __QNX__ +# define _QNX_SOURCE 1 #endif -// Fundamental utilities +// Get the convenience macros that became standard spellings in C23 +#if __STDC_VERSION__ < 202311L -/** - * 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 +/** _Static_assert() => static_assert() */ +#include <assert.h> +/** _Alignas(), _Alignof() => alignas(), alignof() */ +#include <stdalign.h> +/** _Bool => bool, true, false */ +#include <stdbool.h> /** - * True sharing/constructive interference/smallest cache line size. + * C23 deprecates `noreturn void` in favour of `[[noreturn]] void`, so we expose + * _noreturn instead with the other attributes in "bfs.h". */ -#ifdef __GCC_CONSTRUCTIVE_SIZE -# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE -#else -# define TRUE_SHARING_SIZE 64 -#endif +// #include <stdnoreturn.h> -/** - * Alignment specifier that avoids false sharing. - */ -#define cache_align alignas(FALSE_SHARING_SIZE) +/** Part of <threads.h>, but we don't use anything else from it. */ +#define thread_local _Thread_local -// Wrappers for attributes +#endif // !C23 -/** - * Silence warnings about switch/case fall-throughs. - */ -#if __has_attribute(fallthrough) -# define fallthru __attribute__((fallthrough)) -#else -# define fallthru ((void)0) -#endif +// Feature detection -/** - * Silence warnings about unused declarations. - */ -#if __has_attribute(unused) -# define attr_maybe_unused __attribute__((unused)) -#else -# define attr_maybe_unused +// https://clang.llvm.org/docs/LanguageExtensions.html#has-attribute +#ifndef __has_attribute +# define __has_attribute(attr) false #endif -/** - * Warn if a value is unused. - */ -#if __has_attribute(warn_unused_result) -# define attr_nodiscard __attribute__((warn_unused_result)) -#else -# define attr_nodiscard +// https://clang.llvm.org/docs/LanguageExtensions.html#has-builtin +#ifndef __has_builtin +# define __has_builtin(builtin) false #endif -/** - * Hint to avoid inlining a function. - */ -#if __has_attribute(noinline) -# define attr_noinline __attribute__((noinline)) -#else -# define attr_noinline +// https://en.cppreference.com/w/c/language/attributes#Attribute_testing +#ifndef __has_c_attribute +# define __has_c_attribute(attr) false #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 +// https://clang.llvm.org/docs/LanguageExtensions.html#has-feature-and-has-extension +#ifndef __has_feature +# define __has_feature(feat) false #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) +// https://en.cppreference.com/w/c/preprocessor/include +#ifndef __has_include +# define __has_include(header) false #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 +// Sanitizer macros (GCC defines these but Clang does not) -/** - * 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 +#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__) +# define __SANITIZE_ADDRESS__ true #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) +#if __has_feature(memory_sanitizer) && !defined(__SANITIZE_MEMORY__) +# define __SANITIZE_MEMORY__ true #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(...) +#if __has_feature(thread_sanitizer) && !defined(__SANITIZE_THREAD__) +# define __SANITIZE_THREAD__ true #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 be09ebd..30ec201 100644 --- a/src/printf.c +++ b/src/printf.c @@ -1,9 +1,10 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "printf.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "bftw.h" #include "color.h" @@ -16,6 +17,7 @@ #include "mtab.h" #include "pwcache.h" #include "stat.h" + #include <errno.h> #include <grp.h> #include <pwd.h> @@ -89,7 +91,7 @@ static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) { (void)ret /** Return a dynamic format string. */ -attr(format_arg(2)) +_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); @@ -97,7 +99,7 @@ static const char *dyn_fmt(const char *str, const char *fake) { } /** Wrapper for fprintf(). */ -attr(printf(3, 4)) +_printf(3, 4) static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) { va_list args; va_start(args, fake); @@ -505,30 +507,25 @@ static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF } static const char *bfs_printf_type(enum bfs_type type) { - switch (type) { - case BFS_BLK: - return "b"; - case BFS_CHR: - return "c"; - case BFS_DIR: - return "d"; - case BFS_DOOR: - return "D"; - case BFS_FIFO: - return "p"; - case BFS_LNK: - return "l"; - case BFS_PORT: - return "P"; - case BFS_REG: - return "f"; - case BFS_SOCK: - return "s"; - case BFS_WHT: - return "w"; - default: - return "U"; + const char *const names[] = { + [BFS_BLK] = "b", + [BFS_CHR] = "c", + [BFS_DIR] = "d", + [BFS_DOOR] = "D", + [BFS_FIFO] = "p", + [BFS_LNK] = "l", + [BFS_PORT] = "P", + [BFS_REG] = "f", + [BFS_SOCK] = "s", + [BFS_WHT] = "w", + }; + + const char *name = NULL; + if ((size_t)type < countof(names)) { + name = names[type]; } + + return name ? name : "U"; } /** %y: type */ @@ -544,7 +541,7 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF int error = 0; if (type == BFS_ERROR) { - if (errno_is_like(ELOOP)) { + if (errno == ELOOP) { str = "L"; } else if (errno_is_like(ENOENT)) { str = "N"; @@ -565,7 +562,7 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF } /** %Z: SELinux context */ -attr(maybe_unused) +_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) { @@ -711,7 +708,7 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha case '+': case ' ': must_be_numeric = true; - fallthru; + _fallthrough; case '-': if (strchr(fmt.str, c)) { bfs_expr_error(ctx, expr); diff --git a/src/printf.h b/src/printf.h index 2bff087..e8d862e 100644 --- a/src/printf.h +++ b/src/printf.h @@ -22,11 +22,11 @@ struct bfs_printf; /** * Parse a -printf format string. * - * @param ctx + * @ctx * The bfs context. - * @param expr + * @expr * The expression to fill in. - * @param format + * @format * The format string to parse. * @return * 0 on success, -1 on failure. @@ -36,11 +36,11 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha /** * Evaluate a parsed format string. * - * @param cfile + * @cfile * The CFILE to print to. - * @param format + * @format * The parsed printf format. - * @param ftwbuf + * @ftwbuf * The bftw() data for the current file. * @return * 0 on success, -1 on failure. diff --git a/src/pwcache.c b/src/pwcache.c index af8c237..fa19dad 100644 --- a/src/pwcache.c +++ b/src/pwcache.c @@ -1,16 +1,15 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "pwcache.h" + #include "alloc.h" #include "trie.h" + #include <errno.h> #include <grp.h> #include <pwd.h> #include <stdlib.h> -#include <string.h> -#include <unistd.h> /** Represents cache hits for negative results. */ static void *MISSING = &MISSING; diff --git a/src/pwcache.h b/src/pwcache.h index b6c0b67..d7c602d 100644 --- a/src/pwcache.h +++ b/src/pwcache.h @@ -27,9 +27,9 @@ struct bfs_users *bfs_users_new(void); /** * Get a user entry by name. * - * @param users + * @users * The user cache. - * @param name + * @name * The username to look up. * @return * The matching user, or NULL if not found (errno == 0) or an error @@ -40,9 +40,9 @@ const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name); /** * Get a user entry by ID. * - * @param users + * @users * The user cache. - * @param uid + * @uid * The ID to look up. * @return * The matching user, or NULL if not found (errno == 0) or an error @@ -53,7 +53,7 @@ const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid); /** * Flush a user cache. * - * @param users + * @users * The cache to flush. */ void bfs_users_flush(struct bfs_users *users); @@ -61,7 +61,7 @@ void bfs_users_flush(struct bfs_users *users); /** * Free a user cache. * - * @param users + * @users * The user cache to free. */ void bfs_users_free(struct bfs_users *users); @@ -82,9 +82,9 @@ struct bfs_groups *bfs_groups_new(void); /** * Get a group entry by name. * - * @param groups + * @groups * The group cache. - * @param name + * @name * The group name to look up. * @return * The matching group, or NULL if not found (errno == 0) or an error @@ -95,9 +95,9 @@ const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name); /** * Get a group entry by ID. * - * @param groups + * @groups * The group cache. - * @param uid + * @uid * The ID to look up. * @return * The matching group, or NULL if not found (errno == 0) or an error @@ -108,7 +108,7 @@ const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid); /** * Flush a group cache. * - * @param groups + * @groups * The cache to flush. */ void bfs_groups_flush(struct bfs_groups *groups); @@ -116,7 +116,7 @@ void bfs_groups_flush(struct bfs_groups *groups); /** * Free a group cache. * - * @param groups + * @groups * The group cache to free. */ void bfs_groups_free(struct bfs_groups *groups); diff --git a/src/sanity.h b/src/sanity.h index e168b8f..be77eef 100644 --- a/src/sanity.h +++ b/src/sanity.h @@ -8,21 +8,8 @@ #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__, ) @@ -33,7 +20,7 @@ #define SANITIZE_CALL__(macro, ptr, size, ...) \ macro(ptr, size) -#if SANITIZE_ADDRESS +#if __SANITIZE_ADDRESS__ # include <sanitizer/asan_interface.h> /** @@ -50,12 +37,30 @@ */ #define sanitize_free(...) SANITIZE_CALL(__asan_poison_memory_region, __VA_ARGS__) +/** + * Adjust the size of an allocated region, for things like dynamic arrays. + * + * @ptr + * The memory region. + * @old + * The previous usable size of the region. + * @new + * The new usable size of the region. + * @cap + * The total allocated capacity of the region. + */ +static inline void sanitize_resize(const void *ptr, size_t old, size_t new, size_t cap) { + const char *beg = ptr; + __sanitizer_annotate_contiguous_container(beg, beg + cap, beg + old, beg + new); +} + #else -# define sanitize_alloc sanitize_uninit -# define sanitize_free sanitize_uninit +# define sanitize_alloc(...) ((void)0) +# define sanitize_free(...) ((void)0) +# define sanitize_resize(ptr, old, new, cap) ((void)0) #endif -#if SANITIZE_MEMORY +#if __SANITIZE_MEMORY__ # include <sanitizer/msan_interface.h> /** @@ -73,19 +78,14 @@ #define sanitize_uninit(...) SANITIZE_CALL(__msan_allocated_memory, __VA_ARGS__) #else -# define sanitize_init(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) -# define sanitize_uninit(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__) +# define sanitize_init(...) ((void)0) +# define sanitize_uninit(...) ((void)0) #endif /** - * Squelch unused variable warnings when not sanitizing. - */ -#define sanitize_ignore(ptr, size) ((void)(ptr), (void)(size)) - -/** * Initialize a variable, unless sanitizers would detect uninitialized uses. */ -#if SANITIZE_MEMORY +#if __SANITIZE_MEMORY__ # define uninit(value) #else # define uninit(value) = value diff --git a/src/sighook.c b/src/sighook.c index ece8147..a87bed5 100644 --- a/src/sighook.c +++ b/src/sighook.c @@ -17,19 +17,33 @@ * drop to zero. Once it does, the old pointer can be safely freed. */ -#include "prelude.h" #include "sighook.h" + #include "alloc.h" #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "thread.h" + #include <errno.h> +#include <pthread.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> -#if _POSIX_SEMAPHORES > 0 +#if __linux__ +# include <sys/syscall.h> +#endif + +// NetBSD opens a file descriptor for each sem_init() +#if defined(_POSIX_SEMAPHORES) && !__NetBSD__ +# define BFS_POSIX_SEMAPHORES _POSIX_SEMAPHORES +#else +# define BFS_POSIX_SEMAPHORES (-1) +#endif + +#if BFS_POSIX_SEMAPHORES >= 0 # include <semaphore.h> #endif @@ -42,8 +56,8 @@ struct arc { /** The reference itself. */ void *ptr; -#if _POSIX_SEMAPHORES > 0 - /** A semaphore for arc_wake(). */ +#if BFS_POSIX_SEMAPHORES >= 0 + /** A semaphore for arc_wait(). */ sem_t sem; /** sem_init() result. */ int sem_status; @@ -52,11 +66,17 @@ struct arc { /** Initialize an arc. */ static void arc_init(struct arc *arc) { + bfs_verify(atomic_is_lock_free(&arc->refs)); + atomic_init(&arc->refs, 0); arc->ptr = NULL; -#if _POSIX_SEMAPHORES > 0 - arc->sem_status = sem_init(&arc->sem, false, 0); +#if BFS_POSIX_SEMAPHORES >= 0 + if (sysoption(SEMAPHORES) > 0) { + arc->sem_status = sem_init(&arc->sem, false, 0); + } else { + arc->sem_status = -1; + } #endif } @@ -91,7 +111,7 @@ static void arc_put(struct arc *arc) { size_t refs = fetch_sub(&arc->refs, 1, release); if (refs == 1) { -#if _POSIX_SEMAPHORES > 0 +#if BFS_POSIX_SEMAPHORES >= 0 if (arc->sem_status == 0 && sem_post(&arc->sem) != 0) { abort(); } @@ -101,7 +121,7 @@ static void arc_put(struct arc *arc) { /** Wait on the semaphore. */ static int arc_sem_wait(struct arc *arc) { -#if _POSIX_SEMAPHORES > 0 +#if BFS_POSIX_SEMAPHORES >= 0 if (arc->sem_status == 0) { while (sem_wait(&arc->sem) != 0) { bfs_everify(errno == EINTR, "sem_wait()"); @@ -142,9 +162,9 @@ done:; /** Destroy an arc. */ static void arc_destroy(struct arc *arc) { - bfs_assert(arc_refs(arc) <= 1); + bfs_assert(arc_refs(arc) == 0); -#if _POSIX_SEMAPHORES > 0 +#if BFS_POSIX_SEMAPHORES >= 0 if (arc->sem_status == 0) { bfs_everify(sem_destroy(&arc->sem) == 0, "sem_destroy()"); } @@ -164,12 +184,25 @@ struct rcu { /** Sentinel value for RCU, since arc uses NULL already. */ static void *RCU_NULL = &RCU_NULL; +/** Map NULL -> RCU_NULL. */ +static void *rcu_encode(void *ptr) { + return ptr ? ptr : RCU_NULL; +} + +/** Map RCU_NULL -> NULL. */ +static void *rcu_decode(void *ptr) { + bfs_assert(ptr != NULL); + return ptr == RCU_NULL ? NULL : ptr; +} + /** Initialize an RCU block. */ -static void rcu_init(struct rcu *rcu) { +static void rcu_init(struct rcu *rcu, void *ptr) { + bfs_verify(atomic_is_lock_free(&rcu->active)); + atomic_init(&rcu->active, 0); arc_init(&rcu->slots[0]); arc_init(&rcu->slots[1]); - arc_set(&rcu->slots[0], RCU_NULL); + arc_set(&rcu->slots[0], rcu_encode(ptr)); } /** Get the active slot. */ @@ -178,15 +211,20 @@ static struct arc *rcu_active(struct rcu *rcu) { return &rcu->slots[i]; } +/** Destroy an RCU block. */ +static void rcu_destroy(struct rcu *rcu) { + arc_wait(rcu_active(rcu)); + arc_destroy(&rcu->slots[1]); + arc_destroy(&rcu->slots[0]); +} + /** Read an RCU-protected pointer. */ static void *rcu_read(struct rcu *rcu, struct arc **slot) { while (true) { *slot = rcu_active(rcu); void *ptr = arc_get(*slot); - if (ptr == RCU_NULL) { - return NULL; - } else if (ptr) { - return ptr; + if (ptr) { + return rcu_decode(ptr); } // Otherwise, the other slot became active; retry } @@ -195,12 +233,7 @@ static void *rcu_read(struct rcu *rcu, struct arc **slot) { /** Get the RCU-protected pointer without acquiring a reference. */ static void *rcu_peek(struct rcu *rcu) { struct arc *arc = rcu_active(rcu); - void *ptr = arc->ptr; - if (ptr == RCU_NULL) { - return NULL; - } else { - return ptr; - } + return rcu_decode(arc->ptr); } /** Update an RCU-protected pointer, and return the old one. */ @@ -211,129 +244,104 @@ static void *rcu_update(struct rcu *rcu, void *ptr) { size_t j = i ^ 1; struct arc *next = &rcu->slots[j]; - arc_set(next, ptr ? ptr : RCU_NULL); + arc_set(next, rcu_encode(ptr)); store(&rcu->active, j, relaxed); - return arc_wait(prev); + return rcu_decode(arc_wait(prev)); } -struct sighook { - int sig; - sighook_fn *fn; - void *arg; - enum sigflags flags; +/** + * An RCU-protected linked list. + */ +struct rcu_list { + /** The first node in the list. */ + struct rcu head; + /** &last->next */ + struct rcu *tail; }; /** - * A table of signal hooks. + * An rcu_list node. */ -struct sigtable { - /** The number of filled slots. */ - size_t filled; - /** The length of the array. */ - size_t size; - /** An array of signal hooks. */ - struct arc hooks[]; +struct rcu_node { + /** The RCU pointer to this node. */ + struct rcu *self; + /** The next node in the list. */ + struct rcu next; }; -/** Add a hook to a table. */ -static int sigtable_add(struct sigtable *table, struct sighook *hook) { - if (!table || table->filled == table->size) { - return -1; - } - - for (size_t i = 0; i < table->size; ++i) { - struct arc *arc = &table->hooks[i]; - if (arc_refs(arc) == 0) { - arc_set(arc, hook); - ++table->filled; - return 0; - } - } - - return -1; +/** Initialize an rcu_list. */ +static void rcu_list_init(struct rcu_list *list) { + rcu_init(&list->head, NULL); + list->tail = &list->head; } -/** Delete a hook from a table. */ -static int sigtable_del(struct sigtable *table, struct sighook *hook) { - for (size_t i = 0; i < table->size; ++i) { - struct arc *arc = &table->hooks[i]; - if (arc->ptr == hook) { - arc_wait(arc); - --table->filled; - return 0; - } - } - - return -1; +/** Append to an rcu_list. */ +static void rcu_list_append(struct rcu_list *list, struct rcu_node *node) { + node->self = list->tail; + list->tail = &node->next; + rcu_init(&node->next, NULL); + rcu_update(node->self, node); } -/** Create a bigger copy of a signal table. */ -static struct sigtable *sigtable_grow(struct sigtable *prev) { - size_t old_size = prev ? prev->size : 0; - size_t new_size = old_size ? 2 * old_size : 1; - struct sigtable *table = ALLOC_FLEX(struct sigtable, hooks, new_size); - if (!table) { - return NULL; - } - - table->filled = 0; - table->size = new_size; - for (size_t i = 0; i < new_size; ++i) { - arc_init(&table->hooks[i]); - } - - for (size_t i = 0; i < old_size; ++i) { - struct sighook *hook = prev->hooks[i].ptr; - if (hook) { - bfs_verify(sigtable_add(table, hook) == 0); - } +/** Remove from an rcu_list. */ +static void rcu_list_remove(struct rcu_list *list, struct rcu_node *node) { + struct rcu_node *next = rcu_peek(&node->next); + rcu_update(node->self, next); + if (next) { + next->self = node->self; + } else { + list->tail = &list->head; } - - return table; + rcu_destroy(&node->next); } -/** Free a signal table. */ -static void sigtable_free(struct sigtable *table) { - if (!table) { - return; - } +/** + * Iterate over an rcu_list. + * + * It is save to `break` out of this loop, but `return` or `goto` will lead to + * a missed arc_put(). + */ +#define for_rcu(type, node, list) \ + for_rcu_(type, node, (list), node##_slot_, node##_prev_, node##_done_) - for (size_t i = 0; i < table->size; ++i) { - struct arc *arc = &table->hooks[i]; - arc_destroy(arc); - } - free(table); -} +#define for_rcu_(type, node, list, slot, prev, done) \ + for (struct arc *slot, *prev, **done = NULL; !done; arc_put(slot), done = &slot) \ + for (type *node = rcu_read(&list->head, &slot); \ + node; \ + prev = slot, \ + node = rcu_read(&((struct rcu_node *)node)->next, &slot), \ + arc_put(prev)) -/** Add a hook to a signal table, growing it if necessary. */ -static int rcu_sigtable_add(struct rcu *rcu, struct sighook *hook) { - struct sigtable *prev = rcu_peek(rcu); - if (sigtable_add(prev, hook) == 0) { - return 0; - } +struct sighook { + /** The RCU list node (must be the first field). */ + struct rcu_node node; - struct sigtable *next = sigtable_grow(prev); - if (!next) { - return -1; - } + /** The signal being hooked, or 0 for atsigexit(). */ + int sig; + /** Signal hook flags. */ + enum sigflags flags; + /** The function to call. */ + sighook_fn *fn; + /** An argument to pass to the function. */ + void *arg; + /** Flag for SH_ONESHOT. */ + atomic bool armed; +}; - bfs_verify(sigtable_add(next, hook) == 0); - rcu_update(rcu, next); - sigtable_free(prev); - return 0; -} +/** The lists of signal hooks. */ +static struct rcu_list sighooks[64]; -/** The global table of signal hooks. */ -static struct rcu rcu_sighooks; -/** The global table of atsigexit() hooks. */ -static struct rcu rcu_exithooks; +/** Get the hook list for a particular signal. */ +static struct rcu_list *siglist(int sig) { + return &sighooks[sig % countof(sighooks)]; +} /** Mutex for initialization and RCU writer exclusion. */ static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER; /** Check if a signal was generated by userspace. */ static bool is_user_generated(const siginfo_t *info) { - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03 + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_03 // // If si_code is SI_USER or SI_QUEUE, or any value less than or // equal to 0, then the signal was generated by a process ... @@ -351,7 +359,7 @@ static bool is_fault(const siginfo_t *info) { } } -// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html +// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/signal.h.html static const int FATAL_SIGNALS[] = { SIGABRT, SIGALRM, @@ -360,22 +368,31 @@ static const int FATAL_SIGNALS[] = { SIGHUP, SIGILL, SIGINT, +#ifdef SIGIO + SIGIO, +#endif SIGPIPE, - SIGQUIT, - SIGSEGV, - SIGTERM, - SIGUSR1, - SIGUSR2, #ifdef SIGPOLL SIGPOLL, #endif #ifdef SIGPROF SIGPROF, #endif +#ifdef SIGPWR + SIGPWR, +#endif + SIGQUIT, + SIGSEGV, +#ifdef SIGSTKFLT + SIGSTKFLT, +#endif #ifdef SIGSYS SIGSYS, #endif + SIGTERM, SIGTRAP, + SIGUSR1, + SIGUSR2, #ifdef SIGVTALRM SIGVTALRM, #endif @@ -392,7 +409,7 @@ static bool is_fatal(int sig) { } #ifdef SIGRTMIN - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03 + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_01 // // The default actions for the realtime signals in the range // SIGRTMIN to SIGRTMAX shall be to terminate the process @@ -406,7 +423,10 @@ static bool is_fatal(int sig) { } /** Reraise a fatal signal. */ -static noreturn void reraise(int sig) { +_noreturn +static void reraise(siginfo_t *info) { + int sig = info->si_signo; + // Restore the default signal action if (signal(sig, SIG_DFL) == SIG_ERR) { goto fail; @@ -420,52 +440,73 @@ static noreturn void reraise(int sig) { goto fail; } +#if __linux__ + // On Linux, try to re-raise the exact siginfo_t (since 3.9, a process can + // signal itself with any siginfo_t) + pid_t tid = syscall(SYS_gettid); + syscall(SYS_rt_tgsigqueueinfo, getpid(), tid, sig, info); +#endif + raise(sig); fail: abort(); } -/** Find any matching hooks and run them. */ -static enum sigflags run_hooks(struct rcu *rcu, int sig, siginfo_t *info) { - enum sigflags ret = 0; - struct arc *slot; - struct sigtable *table = rcu_read(rcu, &slot); - if (!table) { - goto done; +/** Check whether we should run a hook. */ +static bool should_run(int sig, struct sighook *hook) { + if (hook->sig != sig && hook->sig != 0) { + return false; } - for (size_t i = 0; i < table->size; ++i) { - struct arc *arc = &table->hooks[i]; - struct sighook *hook = arc_get(arc); - if (!hook) { - continue; + if (hook->flags & SH_ONESHOT) { + if (!exchange(&hook->armed, false, relaxed)) { + return false; } + } + + return true; +} - if (hook->sig == sig || hook->sig == 0) { +/** Find any matching hooks and run them. */ +static enum sigflags run_hooks(struct rcu_list *list, int sig, siginfo_t *info) { + enum sigflags ret = 0; + + for_rcu (struct sighook, hook, list) { + if (should_run(sig, hook)) { hook->fn(sig, info, hook->arg); ret |= hook->flags; } - arc_put(arc); } -done: - arc_put(slot); return ret; } /** Dispatches a signal to the registered handlers. */ static void sigdispatch(int sig, siginfo_t *info, void *context) { - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03 + // If we get a fault (e.g. a "real" SIGSEGV, not something like + // kill(..., SIGSEGV)), don't try to run signal hooks, since we could be + // in an arbitrarily corrupted state. // - // The behavior of a process is undefined after it returns normally - // from a signal-catching function for a SIGBUS, SIGFPE, SIGILL, or - // SIGSEGV signal that was not generated by kill(), sigqueue(), or - // raise(). + // POSIX says that returning normally from a signal handler for a fault + // is undefined. But in practice, it's better to uninstall the handler + // and return, which will re-run the faulting instruction and cause us + // to die "correctly" (e.g. with a core dump pointing at the faulting + // instruction, not reraise()). if (is_fault(info)) { - reraise(sig); + // On macOS, we cannot reliably distinguish between faults and + // asynchronous signals. For example, pkill -SEGV bfs will + // result in si_code == SEGV_ACCERR. So we always re-raise the + // signal, because just returning would cause us to ignore + // asynchronous SIG{BUS,ILL,SEGV}. +#if !__APPLE__ + if (signal(sig, SIG_DFL) != SIG_ERR) { + return; + } +#endif + reraise(info); } - // https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03 + // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_04 // // After returning from a signal-catching function, the value of // errno is unspecified if the signal-catching function or any @@ -475,35 +516,58 @@ static void sigdispatch(int sig, siginfo_t *info, void *context) { int error = errno; // Run the normal hooks - enum sigflags flags = run_hooks(&rcu_sighooks, sig, info); + struct rcu_list *list = siglist(sig); + enum sigflags flags = run_hooks(list, sig, info); // Run the atsigexit() hooks, if we're exiting if (!(flags & SH_CONTINUE) && is_fatal(sig)) { - run_hooks(&rcu_exithooks, sig, info); - reraise(sig); + list = siglist(0); + run_hooks(list, sig, info); + reraise(info); } errno = error; } +/** A saved signal handler, for sigreset() to restore. */ +struct sigsave { + struct rcu_node node; + int sig; + struct sigaction action; +}; + +/** The list of saved signal handlers. */ +static struct rcu_list saved; +/** `saved` initialization status (since rcu_list_init() isn't atomic). */ +static atomic bool initialized = false; + /** Make sure our signal handler is installed for a given signal. */ static int siginit(int sig) { +#ifdef SA_RESTART +# define BFS_SA_RESTART SA_RESTART +#else +# define BFS_SA_RESTART 0 +#endif + static struct sigaction action = { .sa_sigaction = sigdispatch, - .sa_flags = SA_RESTART | SA_SIGINFO, + .sa_flags = BFS_SA_RESTART | SA_SIGINFO, }; static sigset_t signals; - static bool initialized = false; - if (!initialized) { + if (!load(&initialized, relaxed)) { if (sigemptyset(&signals) != 0 || sigemptyset(&action.sa_mask) != 0) { return -1; } - rcu_init(&rcu_sighooks); - rcu_init(&rcu_exithooks); - initialized = true; + + for (size_t i = 0; i < countof(sighooks); ++i) { + rcu_list_init(&sighooks[i]); + } + + rcu_list_init(&saved); + store(&initialized, true, release); } int installed = sigismember(&signals, sig); @@ -513,47 +577,63 @@ static int siginit(int sig) { return 0; } - if (sigaction(sig, &action, NULL) != 0) { + sigset_t updated = signals; + if (sigaddset(&updated, sig) != 0) { return -1; } - if (sigaddset(&signals, sig) != 0) { + struct sigaction original; + if (sigaction(sig, NULL, &original) != 0) { + return -1; + } + + struct sigsave *save = ALLOC(struct sigsave); + if (!save) { + return -1; + } + + save->sig = sig; + save->action = original; + rcu_list_append(&saved, &save->node); + + if (sigaction(sig, &action, NULL) != 0) { + rcu_list_remove(&saved, &save->node); + free(save); return -1; } + signals = updated; return 0; } /** Shared sighook()/atsigexit() implementation. */ -static struct sighook *sighook_impl(struct rcu *rcu, int sig, sighook_fn *fn, void *arg, enum sigflags flags) { +static struct sighook *sighook_impl(int sig, sighook_fn *fn, void *arg, enum sigflags flags) { struct sighook *hook = ALLOC(struct sighook); if (!hook) { return NULL; } hook->sig = sig; + hook->flags = flags; hook->fn = fn; hook->arg = arg; - hook->flags = flags; - - if (rcu_sigtable_add(rcu, hook) != 0) { - free(hook); - return NULL; - } + atomic_init(&hook->armed, true); + struct rcu_list *list = siglist(sig); + rcu_list_append(list, &hook->node); return hook; } struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) { + bfs_assert(sig > 0); + mutex_lock(&sigmutex); struct sighook *ret = NULL; - if (siginit(sig) != 0) { - goto done; + if (siginit(sig) == 0) { + ret = sighook_impl(sig, fn, arg, flags); } - ret = sighook_impl(&rcu_sighooks, sig, fn, arg, flags); -done: mutex_unlock(&sigmutex); return ret; } @@ -561,24 +641,20 @@ done: struct sighook *atsigexit(sighook_fn *fn, void *arg) { mutex_lock(&sigmutex); - struct sighook *ret = NULL; - for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) { - if (siginit(FATAL_SIGNALS[i]) != 0) { - goto done; - } + // Ignore errors; atsigexit() is best-effort anyway and things + // like sanitizer runtimes or valgrind may reserve signals for + // their own use + siginit(FATAL_SIGNALS[i]); } #ifdef SIGRTMIN for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) { - if (siginit(i) != 0) { - goto done; - } + siginit(i); } #endif - ret = sighook_impl(&rcu_exithooks, 0, fn, arg, 0); -done: + struct sighook *ret = sighook_impl(0, fn, arg, 0); mutex_unlock(&sigmutex); return ret; } @@ -590,15 +666,27 @@ void sigunhook(struct sighook *hook) { mutex_lock(&sigmutex); - struct rcu *rcu = hook->sig ? &rcu_sighooks : &rcu_exithooks; - struct sigtable *table = rcu_peek(rcu); - bfs_verify(sigtable_del(table, hook) == 0); - - if (table->filled == 0) { - rcu_update(rcu, NULL); - sigtable_free(table); - } + struct rcu_list *list = siglist(hook->sig); + rcu_list_remove(list, &hook->node); mutex_unlock(&sigmutex); + free(hook); } + +int sigreset(void) { + if (!load(&initialized, acquire)) { + return 0; + } + + int ret = 0; + + for_rcu (struct sigsave, save, &saved) { + if (sigaction(save->sig, &save->action, NULL) != 0) { + ret = -1; + break; + } + } + + return ret; +} diff --git a/src/sighook.h b/src/sighook.h index 74d18c0..7149229 100644 --- a/src/sighook.h +++ b/src/sighook.h @@ -21,17 +21,19 @@ struct sighook; enum sigflags { /** Suppress the default action for this signal. */ SH_CONTINUE = 1 << 0, + /** Only run this hook once. */ + SH_ONESHOT = 1 << 1, }; /** * A signal hook callback. Hooks are executed from a signal handler, so must * only call async-signal-safe functions. * - * @param sig + * @sig * The signal number. - * @param info + * @info * Additional information about the signal. - * @param arg + * @arg * An arbitrary pointer passed to the hook. */ typedef void sighook_fn(int sig, siginfo_t *info, void *arg); @@ -39,13 +41,13 @@ typedef void sighook_fn(int sig, siginfo_t *info, void *arg); /** * Install a hook for a signal. * - * @param sig + * @sig * The signal to hook. - * @param fn + * @fn * The function to call. - * @param arg + * @arg * An argument passed to the function. - * @param flags + * @flags * Flags for the new hook. * @return * The installed hook, or NULL on failure. @@ -56,9 +58,9 @@ struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) * On a best-effort basis, invoke the given hook just before the program is * abnormally terminated by a signal. * - * @param fn + * @fn * The function to call. - * @param arg + * @arg * An argument passed to the function. * @return * The installed hook, or NULL on failure. @@ -70,4 +72,12 @@ struct sighook *atsigexit(sighook_fn *fn, void *arg); */ void sigunhook(struct sighook *hook); +/** + * Restore all signal handlers to their original dispositions (e.g. after fork()). + * + * @return + * 0 on success, -1 on failure. + */ +int sigreset(void); + #endif // BFS_SIGHOOK_H @@ -1,12 +1,14 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "stat.h" + #include "atomic.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "sanity.h" + #include <errno.h> #include <fcntl.h> #include <string.h> @@ -49,9 +51,11 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) { return "change time"; case BFS_STAT_MTIME: return "modification time"; + case BFS_STAT_MNT_ID: + return "mount ID"; } - bfs_bug("Unrecognized stat field"); + bfs_bug("Unrecognized stat field %d", (int)field); return "???"; } @@ -99,6 +103,10 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) { dest->rdev = src->st_rdev; dest->mask |= BFS_STAT_RDEV; + // No mount IDs in regular stat(), so use the dev_t as an approximation + dest->mnt_id = dest->dev; + dest->mask |= BFS_STAT_MNT_ID; + #if BFS_HAS_ST_FLAGS dest->attrs = src->st_flags; dest->mask |= BFS_STAT_ATTRS; @@ -167,6 +175,17 @@ int bfs_statx_flags(enum bfs_stat_flags flags) { return ret; } +unsigned int bfs_statx_mask(void) { + unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; +#ifdef STATX_MNT_ID + mask |= STATX_MNT_ID; +#endif +#ifdef STATX_MNT_ID_UNIQUE + mask |= STATX_MNT_ID_UNIQUE; +#endif + return mask; +} + int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { // Callers shouldn't have to check anything except the times const unsigned int guaranteed = STATX_BASIC_STATS & ~(STATX_ATIME | STATX_CTIME | STATX_MTIME); @@ -207,6 +226,18 @@ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { dest->attrs = src->stx_attributes; dest->mask |= BFS_STAT_ATTRS; + dest->mnt_id = dest->dev; +#ifdef STATX_MNT_ID + unsigned int mnt_mask = STATX_MNT_ID; +# ifdef STATX_MNT_ID_UNIQUE + mnt_mask |= STATX_MNT_ID_UNIQUE; +# endif + if (src->stx_mask & mnt_mask) { + dest->mnt_id = src->stx_mnt_id; + } +#endif + dest->mask |= BFS_STAT_MNT_ID; + if (src->stx_mask & STATX_ATIME) { dest->atime.tv_sec = src->stx_atime.tv_sec; dest->atime.tv_nsec = src->stx_atime.tv_nsec; @@ -238,7 +269,7 @@ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) { * bfs_stat() implementation backed by statx(). */ static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) { - unsigned int mask = STATX_BASIC_STATS | STATX_BTIME; + unsigned int mask = bfs_statx_mask(); struct statx xbuf; int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf); if (ret != 0) { @@ -12,7 +12,9 @@ #ifndef BFS_STAT_H #define BFS_STAT_H -#include "prelude.h" +#include "bfs.h" + +#include <stdint.h> #include <sys/stat.h> #include <sys/types.h> #include <time.h> @@ -25,7 +27,7 @@ # define BFS_USE_STATX (BFS_HAS_STATX || BFS_HAS_STATX_SYSCALL) #endif -#if BFS_USE_SYS_PARAM_H +#if __has_include(<sys/param.h>) # include <sys/param.h> #endif @@ -55,6 +57,7 @@ enum bfs_stat_field { BFS_STAT_BTIME = 1 << 11, BFS_STAT_CTIME = 1 << 12, BFS_STAT_MTIME = 1 << 13, + BFS_STAT_MNT_ID = 1 << 14, }; /** @@ -101,6 +104,8 @@ struct bfs_stat { blkcnt_t blocks; /** The device ID represented by this file. */ dev_t rdev; + /** The ID of the mount point containing this file. */ + uint64_t mnt_id; /** Attributes/flags set on the file. */ unsigned long long attrs; @@ -118,14 +123,14 @@ struct bfs_stat { /** * Facade over fstatat(). * - * @param at_fd + * @at_fd * The base file descriptor for the lookup. - * @param at_path + * @at_path * The path to stat, relative to at_fd. Pass NULL to fstat() at_fd * itself. - * @param flags + * @flags * Flags that affect the lookup. - * @param[out] buf + * @buf[out] * A place to store the stat buffer, if successful. * @return * 0 on success, -1 on error. @@ -149,6 +154,11 @@ void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src); int bfs_statx_flags(enum bfs_stat_flags flags); /** + * Get the default statx() mask. + */ +unsigned int bfs_statx_mask(void); + +/** * Convert struct statx to struct bfs_stat. */ int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src); diff --git a/src/thread.c b/src/thread.c index 3793896..b3604f8 100644 --- a/src/thread.c +++ b/src/thread.c @@ -1,13 +1,18 @@ // 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> +#if __has_include(<pthread_np.h>) +# include <pthread_np.h> +#endif + #define THREAD_FALLIBLE(expr) \ do { \ int err = expr; \ @@ -31,6 +36,14 @@ int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, THREAD_FALLIBLE(pthread_create(thread, attr, fn, arg)); } +void thread_setname(pthread_t thread, const char *name) { +#if BFS_HAS_PTHREAD_SETNAME_NP + pthread_setname_np(thread, name); +#elif BFS_HAS_PTHREAD_SET_NAME_NP + pthread_set_name_np(thread, name); +#endif +} + void thread_join(pthread_t thread, void **ret) { THREAD_INFALLIBLE(pthread_join(thread, ret)); } diff --git a/src/thread.h b/src/thread.h index db11bd8..3dd8422 100644 --- a/src/thread.h +++ b/src/thread.h @@ -8,17 +8,8 @@ #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); @@ -31,6 +22,11 @@ typedef void *thread_fn(void *arg); int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg); /** + * Set the name of a thread. + */ +void thread_setname(pthread_t thread, const char *name); + +/** * Wrapper for pthread_join(). */ void thread_join(pthread_t thread, void **ret); @@ -81,21 +81,23 @@ * and insert intermediate singleton "jump" nodes when necessary. */ -#include "prelude.h" #include "trie.h" + #include "alloc.h" +#include "bfs.h" #include "bit.h" #include "diag.h" #include "list.h" + #include <stdint.h> #include <string.h> -bfs_static_assert(CHAR_WIDTH == 8); +static_assert(CHAR_WIDTH == 8, "This trie implementation assumes 8-bit bytes."); #if __i386__ || __x86_64__ -# define trie_clones attr(target_clones("popcnt", "default")) +# define _trie_clones _target_clones("popcnt", "default") #else -# define trie_clones +# define _trie_clones #endif /** Number of bits for the sparse array bitmap, aka the range of a nibble. */ @@ -127,37 +129,37 @@ struct trie_node { * tag to distinguish internal nodes from leaves. This is safe as long * as all dynamic allocations are aligned to more than a single byte. */ - uintptr_t children[]; + uintptr_t children[]; // _counted_by(count_ones(bitmap)) }; -/** Check if an encoded pointer is to a leaf. */ -static bool trie_is_leaf(uintptr_t ptr) { +/** Check if an encoded pointer is to an internal node. */ +static bool trie_is_node(uintptr_t ptr) { return ptr & 1; } -/** Decode a pointer to a leaf. */ -static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { - bfs_assert(trie_is_leaf(ptr)); - return (struct trie_leaf *)(ptr ^ 1); +/** Decode a pointer to an internal node. */ +static struct trie_node *trie_decode_node(uintptr_t ptr) { + bfs_assert(trie_is_node(ptr)); + return (struct trie_node *)(ptr - 1); } -/** Encode a pointer to a leaf. */ -static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) { - uintptr_t ptr = (uintptr_t)leaf ^ 1; - bfs_assert(trie_is_leaf(ptr)); +/** Encode a pointer to an internal node. */ +static uintptr_t trie_encode_node(const struct trie_node *node) { + uintptr_t ptr = (uintptr_t)node + 1; + bfs_assert(trie_is_node(ptr)); return ptr; } -/** Decode a pointer to an internal node. */ -static struct trie_node *trie_decode_node(uintptr_t ptr) { - bfs_assert(!trie_is_leaf(ptr)); - return (struct trie_node *)ptr; +/** Decode a pointer to a leaf. */ +static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) { + bfs_assert(!trie_is_node(ptr)); + return (struct trie_leaf *)ptr; } -/** Encode a pointer to an internal node. */ -static uintptr_t trie_encode_node(const struct trie_node *node) { - uintptr_t ptr = (uintptr_t)node; - bfs_assert(!trie_is_leaf(ptr)); +/** Encode a pointer to a leaf. */ +static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) { + uintptr_t ptr = (uintptr_t)leaf; + bfs_assert(!trie_is_node(ptr)); return ptr; } @@ -169,20 +171,32 @@ void trie_init(struct trie *trie) { } /** Extract the nibble at a certain offset from a byte sequence. */ -static unsigned char trie_key_nibble(const void *key, size_t offset) { +static unsigned char trie_key_nibble(const void *key, size_t length, size_t offset) { const unsigned char *bytes = key; - size_t byte = offset >> 1; + size_t byte = offset / 2; + bfs_assert(byte < length); // A branchless version of // if (offset & 1) { - // return bytes[byte] >> 4; - // } else { // return bytes[byte] & 0xF; + // } else { + // return bytes[byte] >> 4; // } - unsigned int shift = (offset & 1) << 2; + unsigned int shift = 4 * ((offset + 1) % 2); return (bytes[byte] >> shift) & 0xF; } +/** Extract the nibble at a certain offset from a leaf. */ +static unsigned char trie_leaf_nibble(const struct trie_leaf *leaf, size_t offset) { + return trie_key_nibble(leaf->key, leaf->length, offset); +} + +/** Get the number of children of an internal node. */ +_trie_clones +static unsigned int trie_node_size(const struct trie_node *node) { + return count_ones((unsigned int)node->bitmap); +} + /** * Finds a leaf in the trie that matches the key at every branch. If the key * exists in the trie, the representative will match the searched key. But @@ -190,26 +204,24 @@ static unsigned char trie_key_nibble(const void *key, size_t offset) { * that case, the first mismatch between the key and the representative will be * the depth at which to make a new branch to insert the key. */ -trie_clones +_trie_clones static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) { uintptr_t ptr = trie->root; - if (!ptr) { - return NULL; - } - size_t offset = 0; - while (!trie_is_leaf(ptr)) { + size_t offset = 0, limit = 2 * length; + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); offset += node->offset; unsigned int index = 0; - if ((offset >> 1) < length) { - unsigned char nibble = trie_key_nibble(key, offset); + if (offset < limit) { + unsigned char nibble = trie_key_nibble(key, length, offset); unsigned int bit = 1U << nibble; - // bits = bitmap & bit ? bitmap & (bit - 1) : 0 - unsigned int mask = -!!(node->bitmap & bit); - unsigned int bits = node->bitmap & (bit - 1) & mask; - index = count_ones(bits); + unsigned int map = node->bitmap; + unsigned int bits = map & (bit - 1); + unsigned int mask = -!!(map & bit); + // index = (map & bit) ? count_ones(bits) : 0; + index = count_ones(bits) & mask; } ptr = node->children[index]; } @@ -221,7 +233,7 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) { return trie_find_mem(trie, key, strlen(key) + 1); } -trie_clones +_trie_clones static struct trie_leaf *trie_find_mem_impl(const struct trie *trie, const void *key, size_t length) { struct trie_leaf *rep = trie_representative(trie, key, length); if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) { @@ -235,7 +247,17 @@ struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t return trie_find_mem_impl(trie, key, length); } -trie_clones +void *trie_get_str(const struct trie *trie, const char *key) { + const struct trie_leaf *leaf = trie_find_str(trie, key); + return leaf ? leaf->value : NULL; +} + +void *trie_get_mem(const struct trie *trie, const void *key, size_t length) { + const struct trie_leaf *leaf = trie_find_mem(trie, key, length); + return leaf ? leaf->value : NULL; +} + +_trie_clones static struct trie_leaf *trie_find_postfix_impl(const struct trie *trie, const char *key) { size_t length = strlen(key); struct trie_leaf *rep = trie_representative(trie, key, length + 1); @@ -261,10 +283,10 @@ static struct trie_leaf *trie_terminal_leaf(const struct trie_node *node) { } uintptr_t ptr = node->children[0]; - if (trie_is_leaf(ptr)) { - return trie_decode_leaf(ptr); - } else { + if (trie_is_node(ptr)) { node = trie_decode_node(ptr); + } else { + return trie_decode_leaf(ptr); } } @@ -280,7 +302,7 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k } } -trie_clones +_trie_clones static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) { uintptr_t ptr = trie->root; if (!ptr) { @@ -291,21 +313,21 @@ static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const ch size_t skip = 0; size_t length = strlen(key) + 1; - size_t offset = 0; - while (!trie_is_leaf(ptr)) { + size_t offset = 0, limit = 2 * length; + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); offset += node->offset; - if ((offset >> 1) >= length) { + if (offset >= limit) { return best; } struct trie_leaf *leaf = trie_terminal_leaf(node); if (trie_check_prefix(leaf, skip, key, length)) { best = leaf; - skip = offset >> 1; + skip = offset / 2; } - unsigned char nibble = trie_key_nibble(key, offset); + unsigned char nibble = trie_key_nibble(key, length, offset); unsigned int bit = 1U << nibble; if (node->bitmap & bit) { unsigned int index = count_ones(node->bitmap & (bit - 1)); @@ -365,16 +387,10 @@ static struct trie_node *trie_node_realloc(struct trie *trie, struct trie_node * /** Free a node. */ static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) { - bfs_assert(size == (size_t)count_ones(node->bitmap)); + bfs_assert(size == trie_node_size(node)); varena_free(&trie->nodes, node, size); } -#if ENDIAN_NATIVE == ENDIAN_LITTLE -# define TRIE_BSWAP(n) (n) -#elif ENDIAN_NATIVE == ENDIAN_BIG -# define TRIE_BSWAP(n) bswap(n) -#endif - /** Find the offset of the first nibble that differs between two keys. */ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t length) { if (!rep) { @@ -388,32 +404,34 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t const char *rep_bytes = rep->key; const char *key_bytes = key; - size_t i = 0; - for (size_t chunk = sizeof(chunk); i + chunk <= length; i += chunk) { - size_t rep_chunk, key_chunk; - memcpy(&rep_chunk, rep_bytes + i, sizeof(rep_chunk)); - memcpy(&key_chunk, key_bytes + i, sizeof(key_chunk)); - - if (rep_chunk != key_chunk) { -#ifdef TRIE_BSWAP - size_t diff = TRIE_BSWAP(rep_chunk ^ key_chunk); - i *= 2; - i += trailing_zeros(diff) / 4; - return i; + size_t ret = 0, i = 0; + +#define CHUNK(n) CHUNK_(uint##n##_t, load8_beu##n) +#define CHUNK_(type, load8) \ + (length - i >= sizeof(type)) { \ + type rep_chunk = load8(rep_bytes + i); \ + type key_chunk = load8(key_bytes + i); \ + type diff = rep_chunk ^ key_chunk; \ + ret += leading_zeros(diff) / 4; \ + if (diff) { \ + return ret; \ + } \ + i += sizeof(type); \ + } + +#if SIZE_WIDTH >= 64 + while CHUNK(64); + if CHUNK(32); #else - break; + while CHUNK(32); #endif - } - } + if CHUNK(16); + if CHUNK(8); - for (; i < length; ++i) { - unsigned char diff = rep_bytes[i] ^ key_bytes[i]; - if (diff) { - return 2 * i + !(diff & 0xF); - } - } +#undef CHUNK_ +#undef CHUNK - return 2 * i; + return ret; } /** @@ -438,10 +456,10 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t * | Z * +--->... */ -trie_clones +_trie_clones static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, unsigned char nibble) { struct trie_node *node = trie_decode_node(*ptr); - unsigned int size = count_ones(node->bitmap); + unsigned int size = trie_node_size(node); // Double the capacity every power of two if (has_single_bit(size)) { @@ -492,10 +510,10 @@ static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, str * | Y * +--->key */ -static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, size_t *offset) { +static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, size_t *offset) { // We only ever need to jump to leaf nodes, since internal nodes are // guaranteed to be within OFFSET_MAX anyway - bfs_assert(trie_is_leaf(*ptr)); + struct trie_leaf *leaf = trie_decode_leaf(*ptr); struct trie_node *node = trie_node_alloc(trie, 1); if (!node) { @@ -505,7 +523,7 @@ static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, *offset += OFFSET_MAX; node->offset = OFFSET_MAX; - unsigned char nibble = trie_key_nibble(key, *offset); + unsigned char nibble = trie_leaf_nibble(leaf, *offset); node->bitmap = 1 << nibble; node->children[0] = *ptr; @@ -531,8 +549,8 @@ static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, * +--->leaf */ static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, struct trie_leaf *rep, size_t offset, size_t mismatch) { - unsigned char key_nibble = trie_key_nibble(leaf->key, mismatch); - unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch); + unsigned char key_nibble = trie_leaf_nibble(leaf, mismatch); + unsigned char rep_nibble = trie_leaf_nibble(rep, mismatch); bfs_assert(key_nibble != rep_nibble); struct trie_node *node = trie_node_alloc(trie, 2); @@ -544,7 +562,7 @@ static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct tr node->bitmap = (1 << key_nibble) | (1 << rep_nibble); size_t delta = mismatch - offset; - if (!trie_is_leaf(*ptr)) { + if (trie_is_node(*ptr)) { struct trie_node *child = trie_decode_node(*ptr); child->offset -= delta; } @@ -561,12 +579,18 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) { return trie_insert_mem(trie, key, strlen(key) + 1); } -trie_clones +_trie_clones static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) { struct trie_leaf *rep = trie_representative(trie, key, length); size_t mismatch = trie_mismatch(rep, key, length); - if (mismatch >= (length << 1)) { + size_t misbyte = mismatch / 2; + if (misbyte >= length) { + bfs_assert(misbyte == length); return rep; + } else if (rep && misbyte >= rep->length) { + bfs_bug("trie keys must be prefix-free"); + errno = EINVAL; + return NULL; } struct trie_leaf *leaf = trie_leaf_alloc(trie, key, length); @@ -581,14 +605,14 @@ static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key size_t offset = 0; uintptr_t *ptr = &trie->root; - while (!trie_is_leaf(*ptr)) { + while (trie_is_node(*ptr)) { struct trie_node *node = trie_decode_node(*ptr); if (offset + node->offset > mismatch) { break; } offset += node->offset; - unsigned char nibble = trie_key_nibble(key, offset); + unsigned char nibble = trie_leaf_nibble(leaf, offset); unsigned int bit = 1U << nibble; if (node->bitmap & bit) { bfs_assert(offset < mismatch); @@ -601,7 +625,7 @@ static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key } while (mismatch - offset > OFFSET_MAX) { - ptr = trie_jump(trie, ptr, key, &offset); + ptr = trie_jump(trie, ptr, &offset); if (!ptr) { trie_leaf_free(trie, leaf); return NULL; @@ -615,13 +639,33 @@ struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t len return trie_insert_mem_impl(trie, key, length); } +int trie_set_str(struct trie *trie, const char *key, const void *value) { + struct trie_leaf *leaf = trie_insert_str(trie, key); + if (leaf) { + leaf->value = (void *)value; + return 0; + } else { + return -1; + } +} + +int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value) { + struct trie_leaf *leaf = trie_insert_mem(trie, key, length); + if (leaf) { + leaf->value = (void *)value; + return 0; + } else { + return -1; + } +} + /** Free a chain of singleton nodes. */ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { - while (!trie_is_leaf(ptr)) { + while (trie_is_node(ptr)) { struct trie_node *node = trie_decode_node(ptr); // Make sure the bitmap is a power of two, i.e. it has just one child - bfs_assert(has_single_bit(node->bitmap)); + bfs_assert(has_single_bit((size_t)node->bitmap)); ptr = node->children[0]; trie_node_free(trie, node, 1); @@ -649,7 +693,7 @@ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { */ static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) { uintptr_t other = parent_node->children[child_index ^ 1]; - if (!trie_is_leaf(other)) { + if (trie_is_node(other)) { struct trie_node *other_node = trie_decode_node(other); if (other_node->offset + parent_node->offset <= OFFSET_MAX) { other_node->offset += parent_node->offset; @@ -659,22 +703,21 @@ static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_ } *parent = other; - trie_node_free(trie, parent_node, 1); + trie_node_free(trie, parent_node, 2); return 0; } -trie_clones +_trie_clones static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { uintptr_t *child = &trie->root; uintptr_t *parent = NULL; unsigned int child_bit = 0, child_index = 0; size_t offset = 0; - while (!trie_is_leaf(*child)) { + while (trie_is_node(*child)) { struct trie_node *node = trie_decode_node(*child); offset += node->offset; - bfs_assert((offset >> 1) < leaf->length); - unsigned char nibble = trie_key_nibble(leaf->key, offset); + unsigned char nibble = trie_leaf_nibble(leaf, offset); unsigned int bit = 1U << nibble; unsigned int bitmap = node->bitmap; bfs_assert(bitmap & bit); @@ -699,19 +742,19 @@ static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) { } struct trie_node *node = trie_decode_node(*parent); - child = node->children + child_index; - trie_free_singletons(trie, *child); + trie_free_singletons(trie, node->children[child_index]); - node->bitmap ^= child_bit; - unsigned int parent_size = count_ones(node->bitmap); - bfs_assert(parent_size > 0); - if (parent_size == 1 && trie_collapse_node(trie, parent, node, child_index) == 0) { + unsigned int parent_size = trie_node_size(node); + bfs_assert(parent_size > 1); + if (parent_size == 2 && trie_collapse_node(trie, parent, node, child_index) == 0) { return; } - if (child_index < parent_size) { - memmove(child, child + 1, (parent_size - child_index) * sizeof(*child)); + for (size_t i = child_index; i + 1 < parent_size; ++i) { + node->children[i] = node->children[i + 1]; } + node->bitmap &= ~child_bit; + --parent_size; if (has_single_bit(parent_size)) { node = trie_node_realloc(trie, node, 2 * parent_size, parent_size); @@ -6,6 +6,7 @@ #include "alloc.h" #include "list.h" + #include <stddef.h> #include <stdint.h> @@ -20,7 +21,7 @@ struct trie_leaf { /** The length of the key in bytes. */ size_t length; /** The key itself, stored inline. */ - char key[]; + char key[] _counted_by(length); }; /** @@ -45,9 +46,9 @@ void trie_init(struct trie *trie); /** * Find the leaf for a string key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * The found leaf, or NULL if the key is not present. @@ -57,11 +58,11 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); /** * Find the leaf for a fixed-size key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. - * @param length + * @length * The length of the key in bytes. * @return * The found leaf, or NULL if the key is not present. @@ -69,11 +70,37 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key); struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length); /** + * Get the value associated with a string key. + * + * @trie + * The trie to search. + * @key + * The key to look up. + * @return + * The found value, or NULL if the key is not present. + */ +void *trie_get_str(const struct trie *trie, const char *key); + +/** + * Get the value associated with a fixed-size key. + * + * @trie + * The trie to search. + * @key + * The key to look up. + * @length + * The length of the key in bytes. + * @return + * The found value, or NULL if the key is not present. + */ +void *trie_get_mem(const struct trie *trie, const void *key, size_t length); + +/** * Find the shortest leaf that starts with a given key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * A leaf that starts with the given key, or NULL. @@ -83,9 +110,9 @@ struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key); /** * Find the leaf that is the longest prefix of the given key. * - * @param trie + * @trie * The trie to search. - * @param key + * @key * The key to look up. * @return * The longest prefix match for the given key, or NULL. @@ -95,9 +122,9 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key); /** * Insert a string key into the trie. * - * @param trie + * @trie * The trie to modify. - * @param key + * @key * The key to insert. * @return * The inserted leaf, or NULL on failure. @@ -107,11 +134,11 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key); /** * Insert a fixed-size key into the trie. * - * @param trie + * @trie * The trie to modify. - * @param key + * @key * The key to insert. - * @param length + * @length * The length of the key in bytes. * @return * The inserted leaf, or NULL on failure. @@ -119,11 +146,41 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key); struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length); /** + * Set the value for a string key. + * + * @trie + * The trie to modify. + * @key + * The key to insert. + * @value + * The value to set. + * @return + * 0 on success, -1 on error. + */ +int trie_set_str(struct trie *trie, const char *key, const void *value); + +/** + * Set the value for a fixed-size key. + * + * @trie + * The trie to modify. + * @key + * The key to insert. + * @length + * The length of the key in bytes. + * @value + * The value to set. + * @return + * 0 on success, -1 on error. + */ +int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value); + +/** * Remove a leaf from a trie. * - * @param trie + * @trie * The trie to modify. - * @param leaf + * @leaf * The leaf to remove. */ void trie_remove(struct trie *trie, struct trie_leaf *leaf); @@ -2,6 +2,7 @@ // SPDX-License-Identifier: 0BSD #include "typo.h" + #include <limits.h> #include <stdint.h> #include <stdlib.h> @@ -7,9 +7,9 @@ /** * Find the "typo" distance between two strings. * - * @param actual + * @actual * The actual string typed by the user. - * @param expected + * @expected * The expected valid string. * @return The distance between the two strings. */ diff --git a/src/version.c b/src/version.c new file mode 100644 index 0000000..7479a9f --- /dev/null +++ b/src/version.c @@ -0,0 +1,32 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "bfs.h" + +const char bfs_version[] = { +#include "version.i" +}; + +const char bfs_confflags[] = { +#include "confflags.i" +}; + +const char bfs_cc[] = { +#include "cc.i" +}; + +const char bfs_cppflags[] = { +#include "cppflags.i" +}; + +const char bfs_cflags[] = { +#include "cflags.i" +}; + +const char bfs_ldflags[] = { +#include "ldflags.i" +}; + +const char bfs_ldlibs[] = { +#include "ldlibs.i" +}; diff --git a/src/xregex.c b/src/xregex.c index 484f099..796544e 100644 --- a/src/xregex.c +++ b/src/xregex.c @@ -1,13 +1,15 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "xregex.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "sanity.h" #include "thread.h" + #include <errno.h> #include <pthread.h> #include <stdlib.h> @@ -37,6 +39,12 @@ struct bfs_regex { static int bfs_onig_status; static OnigEncoding bfs_onig_enc; +static OnigSyntaxType bfs_onig_syntax_awk; +static OnigSyntaxType bfs_onig_syntax_gnu_awk; +static OnigSyntaxType bfs_onig_syntax_emacs; +static OnigSyntaxType bfs_onig_syntax_egrep; +static OnigSyntaxType bfs_onig_syntax_gnu_find; + /** pthread_once() callback. */ static void bfs_onig_once(void) { // Fall back to ASCII by default @@ -103,6 +111,35 @@ static void bfs_onig_once(void) { if (bfs_onig_status != ONIG_NORMAL) { bfs_onig_enc = NULL; } + + // Compute the GNU extensions + OnigSyntaxType *ere = ONIG_SYNTAX_POSIX_EXTENDED; + OnigSyntaxType *gnu = ONIG_SYNTAX_GNU_REGEX; + unsigned int gnu_op = gnu->op & ~ere->op; + unsigned int gnu_op2 = gnu->op2 & ~ere->op2; + unsigned int gnu_behavior = gnu->behavior & ~ere->behavior; + + onig_copy_syntax(&bfs_onig_syntax_awk, ONIG_SYNTAX_POSIX_EXTENDED); + bfs_onig_syntax_awk.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL; + bfs_onig_syntax_awk.behavior |= ONIG_SYN_BACKSLASH_ESCAPE_IN_CC; + + onig_copy_syntax(&bfs_onig_syntax_gnu_awk, &bfs_onig_syntax_awk); + bfs_onig_syntax_gnu_awk.op |= gnu_op; + bfs_onig_syntax_gnu_awk.op2 |= gnu_op2; + bfs_onig_syntax_gnu_awk.behavior |= gnu_behavior; + bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INDEP_REPEAT_OPS; + bfs_onig_syntax_gnu_awk.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS; + + // https://github.com/kkos/oniguruma/issues/296 + onig_copy_syntax(&bfs_onig_syntax_emacs, ONIG_SYNTAX_EMACS); + bfs_onig_syntax_emacs.op2 |= ONIG_SYN_OP2_QMARK_GROUP_EFFECT; + + onig_copy_syntax(&bfs_onig_syntax_egrep, ONIG_SYNTAX_POSIX_EXTENDED); + bfs_onig_syntax_egrep.behavior |= ONIG_SYN_ALLOW_INVALID_INTERVAL; + bfs_onig_syntax_egrep.behavior &= ~ONIG_SYN_CONTEXT_INVALID_REPEAT_OPS; + + onig_copy_syntax(&bfs_onig_syntax_gnu_find, &bfs_onig_syntax_emacs); + bfs_onig_syntax_gnu_find.options |= ONIG_OPTION_MULTILINE; } /** Initialize Oniguruma. */ @@ -143,12 +180,24 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ case BFS_REGEX_POSIX_EXTENDED: syntax = ONIG_SYNTAX_POSIX_EXTENDED; break; + case BFS_REGEX_AWK: + syntax = &bfs_onig_syntax_awk; + break; + case BFS_REGEX_GNU_AWK: + syntax = &bfs_onig_syntax_gnu_awk; + break; case BFS_REGEX_EMACS: - syntax = ONIG_SYNTAX_EMACS; + syntax = &bfs_onig_syntax_emacs; break; case BFS_REGEX_GREP: syntax = ONIG_SYNTAX_GREP; break; + case BFS_REGEX_EGREP: + syntax = &bfs_onig_syntax_egrep; + break; + case BFS_REGEX_GNU_FIND: + syntax = &bfs_onig_syntax_gnu_find; + break; } bfs_assert(syntax, "Invalid regex type"); diff --git a/src/xregex.h b/src/xregex.h index 998a2b0..c4504ee 100644 --- a/src/xregex.h +++ b/src/xregex.h @@ -15,8 +15,12 @@ struct bfs_regex; enum bfs_regex_type { BFS_REGEX_POSIX_BASIC, BFS_REGEX_POSIX_EXTENDED, + BFS_REGEX_AWK, + BFS_REGEX_GNU_AWK, BFS_REGEX_EMACS, BFS_REGEX_GREP, + BFS_REGEX_EGREP, + BFS_REGEX_GNU_FIND, }; /** @@ -38,13 +42,13 @@ enum bfs_regexec_flags { /** * Wrapper for regcomp() that supports additional regex types. * - * @param[out] preg + * @preg[out] * Will hold the compiled regex. - * @param pattern + * @pattern * The regular expression to compile. - * @param type + * @type * The regular expression syntax to use. - * @param flags + * @flags * Regex compilation flags. * @return * 0 on success, -1 on failure. @@ -54,11 +58,11 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ /** * Wrapper for regexec(). * - * @param regex + * @regex * The regular expression to execute. - * @param str + * @str * The string to match against. - * @param flags + * @flags * Regex execution flags. * @return * 1 for a match, 0 for no match, -1 on failure. @@ -73,7 +77,7 @@ void bfs_regfree(struct bfs_regex *regex); /** * Get a human-readable regex error message. * - * @param regex + * @regex * The compiled regex. * @return * A human-readable description of the error, which should be free()'d. diff --git a/src/xspawn.c b/src/xspawn.c index 33e5a4a..ee62c05 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -1,12 +1,15 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "xspawn.h" + #include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "list.h" +#include "sighook.h" + #include <errno.h> #include <fcntl.h> #include <signal.h> @@ -16,11 +19,11 @@ #include <sys/types.h> #include <unistd.h> -#if BFS_USE_PATHS_H +#if __has_include(<paths.h>) # include <paths.h> #endif -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 # include <spawn.h> #endif @@ -70,29 +73,42 @@ int bfs_spawn_init(struct bfs_spawn *ctx) { ctx->flags = 0; SLIST_INIT(ctx); -#if _POSIX_SPAWN > 0 - ctx->flags |= BFS_SPAWN_USE_POSIX; +#if BFS_POSIX_SPAWN >= 0 + if (sysoption(SPAWN) > 0) { + ctx->flags |= BFS_SPAWN_USE_POSIX; - errno = posix_spawn_file_actions_init(&ctx->actions); - if (errno != 0) { - return -1; - } + errno = posix_spawn_file_actions_init(&ctx->actions); + if (errno != 0) { + return -1; + } - errno = posix_spawnattr_init(&ctx->attr); - if (errno != 0) { - posix_spawn_file_actions_destroy(&ctx->actions); - return -1; + errno = posix_spawnattr_init(&ctx->attr); + if (errno != 0) { + posix_spawn_file_actions_destroy(&ctx->actions); + return -1; + } } #endif return 0; } -int bfs_spawn_destroy(struct bfs_spawn *ctx) { -#if _POSIX_SPAWN > 0 - posix_spawnattr_destroy(&ctx->attr); - posix_spawn_file_actions_destroy(&ctx->actions); +/** + * Clear the BFS_SPAWN_USE_POSIX flag and free the attributes. + */ +static void bfs_spawn_clear_posix(struct bfs_spawn *ctx) { + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + ctx->flags &= ~BFS_SPAWN_USE_POSIX; + +#if BFS_POSIX_SPAWN >= 0 + posix_spawnattr_destroy(&ctx->attr); + posix_spawn_file_actions_destroy(&ctx->actions); #endif + } +} + +int bfs_spawn_destroy(struct bfs_spawn *ctx) { + bfs_spawn_clear_posix(ctx); for_slist (struct bfs_spawn_action, action, ctx) { free(action); @@ -101,9 +117,9 @@ int bfs_spawn_destroy(struct bfs_spawn *ctx) { return 0; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** Set some posix_spawnattr flags. */ -attr(maybe_unused) +_maybe_unused static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { short prev; errno = posix_spawnattr_getflags(&ctx->attr, &prev); @@ -121,7 +137,7 @@ static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { return 0; } -#endif // _POSIX_SPAWN > 0 +#endif /** Allocate a spawn action. */ static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) { @@ -143,7 +159,7 @@ int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_addopen(&ctx->actions, fd, path, flags, mode); if (errno != 0) { @@ -167,7 +183,7 @@ int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_addclose(&ctx->actions, fd); if (errno != 0) { @@ -188,7 +204,7 @@ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = posix_spawn_file_actions_adddup2(&ctx->actions, oldfd, newfd); if (errno != 0) { @@ -216,19 +232,35 @@ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { */ #define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__) +/** + * NetBSD even resolves the executable before file actions with posix_spawn()! + */ +#define BFS_POSIX_SPAWN_AFTER_FCHDIR !__NetBSD__ + int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR); if (!action) { return -1; } +#if __APPLE__ + // macOS has a bug that causes EBADF when an fchdir() action refers to a + // file opened by the file actions + for_slist (struct bfs_spawn_action, prev, ctx) { + if (fd == prev->out_fd) { + bfs_spawn_clear_posix(ctx); + break; + } + } +#endif + #if BFS_HAS_POSIX_SPAWN_ADDFCHDIR # define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir #elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP # define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np #endif -#if _POSIX_SPAWN > 0 && defined(BFS_POSIX_SPAWN_FCHDIR) +#if BFS_POSIX_SPAWN >= 0 && defined(BFS_POSIX_SPAWN_ADDFCHDIR) if (ctx->flags & BFS_SPAWN_USE_POSIX) { errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd); if (errno != 0) { @@ -237,7 +269,7 @@ int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { } } #else - ctx->flags &= ~BFS_SPAWN_USE_POSIX; + bfs_spawn_clear_posix(ctx); #endif action->in_fd = fd; @@ -261,7 +293,7 @@ int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit goto fail; } #else - ctx->flags &= ~BFS_SPAWN_USE_POSIX; + bfs_spawn_clear_posix(ctx); #endif action->resource = resource; @@ -385,18 +417,40 @@ static bool bfs_resolve_relative(const struct bfs_resolver *res) { return false; } +/** Check if the actions include fchdir(). */ +static bool bfs_spawn_will_chdir(const struct bfs_spawn *ctx) { + if (ctx) { + for_slist (const struct bfs_spawn_action, action, ctx) { + if (action->op == BFS_SPAWN_FCHDIR) { + return true; + } + } + } + + return false; +} + +/** Check if we can call xfaccessat() before file actions. */ +static bool bfs_can_access_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (res->exe[0] == '/') { + return true; + } + + if (bfs_spawn_will_chdir(ctx)) { + return false; + } + + return true; +} + /** Check if we can resolve the executable before file actions. */ static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { if (!bfs_resolve_relative(res)) { return true; } - if (ctx) { - for_slist (const struct bfs_spawn_action, action, ctx) { - if (action->op == BFS_SPAWN_FCHDIR) { - return false; - } - } + if (bfs_spawn_will_chdir(ctx)) { + return false; } return true; @@ -426,6 +480,17 @@ static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const st }; if (bfs_can_skip_resolve(res, ctx)) { + if (bfs_can_access_early(res, ctx)) { + // Do this check eagerly, even though posix_spawn()/execv() also + // would, because: + // + // - faccessat() is faster than fork()/clone() + execv() + // - posix_spawn() is not guaranteed to report ENOENT + if (xfaccessat(AT_FDCWD, exe, X_OK) != 0) { + return -1; + } + } + res->done = true; return 0; } @@ -473,7 +538,7 @@ fail: return -1; } -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** bfs_spawn() implementation using posix_spawn(). */ static pid_t bfs_posix_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { @@ -504,13 +569,20 @@ static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs } #endif +#if !BFS_POSIX_SPAWN_AFTER_FCHDIR + if (res->exe[0] != '/' && bfs_spawn_will_chdir(ctx)) { + return false; + } +#endif + return true; } -#endif // _POSIX_SPAWN > 0 +#endif // BFS_POSIX_SPAWN >= 0 /** Actually exec() the new process. */ -static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { +_noreturn +static void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, const sigset_t *mask, int pipefd[2]) { xclose(pipefd[0]); for_slist (const struct bfs_spawn_action, action, ctx) { @@ -571,6 +643,18 @@ static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_s goto fail; } + // Reset signal handlers to their original values before we unblock + // signals, so that handlers don't run in both the parent and the child + if (sigreset() != 0) { + goto fail; + } + + // Restore the original signal mask for the child process + errno = pthread_sigmask(SIG_SETMASK, mask, NULL); + if (errno != 0) { + goto fail; + } + execve(res->exe, argv, envp); fail:; @@ -603,15 +687,19 @@ static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ct goto fail; } +#if BFS_HAS__FORK + pid_t pid = _Fork(); +#else pid_t pid = fork(); +#endif if (pid == 0) { // Child - bfs_spawn_exec(res, ctx, argv, envp, pipefd); + bfs_spawn_exec(res, ctx, argv, envp, &old_mask, pipefd); } // Restore the original signal mask - int ret = pthread_sigmask(SIG_SETMASK, &old_mask, NULL); - bfs_everify(ret == 0, "pthread_sigmask()"); + errno = pthread_sigmask(SIG_SETMASK, &old_mask, NULL); + bfs_everify(errno == 0, "pthread_sigmask()"); if (pid < 0) { // fork() failed @@ -639,7 +727,7 @@ fail: /** Call the right bfs_spawn() implementation. */ static pid_t bfs_spawn_impl(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 if (bfs_use_posix_spawn(res, ctx)) { return bfs_posix_spawn(res, ctx, argv, envp); } diff --git a/src/xspawn.h b/src/xspawn.h index 6a8f54a..3c74ccd 100644 --- a/src/xspawn.h +++ b/src/xspawn.h @@ -8,12 +8,17 @@ #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 +#ifdef _POSIX_SPAWN +# define BFS_POSIX_SPAWN _POSIX_SPAWN +#else +# define BFS_POSIX_SPAWN (-1) +#endif + +#if BFS_POSIX_SPAWN >= 0 # include <spawn.h> #endif @@ -38,7 +43,7 @@ struct bfs_spawn { struct bfs_spawn_action *head; struct bfs_spawn_action **tail; -#if _POSIX_SPAWN > 0 +#if BFS_POSIX_SPAWN >= 0 /** posix_spawn() context, for when we can use it. */ posix_spawn_file_actions_t actions; posix_spawnattr_t attr; @@ -104,13 +109,13 @@ int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit /** * Spawn a new process. * - * @param exe + * @exe * The executable to run. - * @param ctx + * @ctx * The context for the new process. - * @param argv + * @argv * The arguments for the new process. - * @param envp + * @envp * The environment variables for the new process (NULL for the current * environment). * @return @@ -122,7 +127,7 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char * Look up an executable in the current PATH, as BFS_SPAWN_USE_PATH or execvp() * would do. * - * @param exe + * @exe * The name of the binary to execute. Bare names without a '/' will be * searched on the provided PATH. * @return diff --git a/src/xtime.c b/src/xtime.c index 2808455..6b8a141 100644 --- a/src/xtime.c +++ b/src/xtime.c @@ -1,11 +1,14 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "xtime.h" + +#include "alloc.h" +#include "bfs.h" #include "bfstd.h" #include "diag.h" #include "sanity.h" + #include <errno.h> #include <limits.h> #include <sys/time.h> @@ -36,7 +39,7 @@ int xmktime(struct tm *tm, time_t *timep) { } // FreeBSD is missing an interceptor -#if BFS_HAS_TIMEGM && !(__FreeBSD__ && SANITIZE_MEMORY) +#if BFS_HAS_TIMEGM && !(__FreeBSD__ && __SANITIZE_MEMORY__) int xtimegm(struct tm *tm, time_t *timep) { time_t time = timegm(tm); @@ -206,6 +209,23 @@ static int xgetpart(const char **str, size_t n, int *result) { } int xgetdate(const char *str, struct timespec *result) { + // Handle @epochseconds + if (str[0] == '@') { + long long value; + if (xstrtoll(str + 1, NULL, 10, &value) != 0) { + goto error; + } + + time_t time = (time_t)value; + if ((long long)time != value) { + errno = ERANGE; + goto error; + } + + result->tv_sec = time; + goto done; + } + struct tm tm = { .tm_isdst = -1, }; @@ -324,6 +344,7 @@ end: } } +done: result->tv_nsec = 0; return 0; @@ -333,16 +354,150 @@ error: return -1; } -int xgettime(struct timespec *result) { -#if _POSIX_TIMERS > 0 - return clock_gettime(CLOCK_REALTIME, result); +/** One nanosecond. */ +static const long NS = 1000L * 1000 * 1000; + +void timespec_add(struct timespec *lhs, const struct timespec *rhs) { + lhs->tv_sec += rhs->tv_sec; + lhs->tv_nsec += rhs->tv_nsec; + if (lhs->tv_nsec >= NS) { + lhs->tv_nsec -= NS; + lhs->tv_sec += 1; + } +} + +void timespec_sub(struct timespec *lhs, const struct timespec *rhs) { + lhs->tv_sec -= rhs->tv_sec; + lhs->tv_nsec -= rhs->tv_nsec; + if (lhs->tv_nsec < 0) { + lhs->tv_nsec += NS; + lhs->tv_sec -= 1; + } +} + +int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs) { + if (lhs->tv_sec < rhs->tv_sec) { + return -1; + } else if (lhs->tv_sec > rhs->tv_sec) { + return 1; + } + + if (lhs->tv_nsec < rhs->tv_nsec) { + return -1; + } else if (lhs->tv_nsec > rhs->tv_nsec) { + return 1; + } + + return 0; +} + +void timespec_min(struct timespec *dest, const struct timespec *src) { + if (timespec_cmp(src, dest) < 0) { + *dest = *src; + } +} + +void timespec_max(struct timespec *dest, const struct timespec *src) { + if (timespec_cmp(src, dest) > 0) { + *dest = *src; + } +} + +double timespec_ns(const struct timespec *ts) { + return 1.0e9 * ts->tv_sec + ts->tv_nsec; +} + +#if defined(_POSIX_TIMERS) && BFS_HAS_TIMER_CREATE +# define BFS_POSIX_TIMERS _POSIX_TIMERS #else - struct timeval tv; - int ret = gettimeofday(&tv, NULL); - if (ret == 0) { - result->tv_sec = tv.tv_sec; - result->tv_nsec = tv.tv_usec * 1000L; +# define BFS_POSIX_TIMERS (-1) +#endif + +struct timer { +#if BFS_POSIX_TIMERS >= 0 + /** The POSIX timer. */ + timer_t timer; +#endif + /** Whether to use timer_create() or setitimer(). */ + bool legacy; +}; + +struct timer *xtimer_start(const struct timespec *interval) { + struct timer *timer = ALLOC(struct timer); + if (!timer) { + return NULL; + } + +#if BFS_POSIX_TIMERS >= 0 + if (sysoption(TIMERS)) { + clockid_t clock = CLOCK_REALTIME; + +#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0 + if (sysoption(MONOTONIC_CLOCK) > 0) { + clock = CLOCK_MONOTONIC; + } +#endif + + if (timer_create(clock, NULL, &timer->timer) != 0) { + goto fail; + } + + // https://github.com/llvm/llvm-project/issues/111847 + sanitize_init(&timer->timer); + + struct itimerspec spec = { + .it_value = *interval, + .it_interval = *interval, + }; + if (timer_settime(timer->timer, 0, &spec, NULL) != 0) { + timer_delete(timer->timer); + goto fail; + } + + timer->legacy = false; + return timer; } - return ret; #endif + +#if BFS_POSIX_TIMERS <= 0 + struct timeval tv = { + .tv_sec = interval->tv_sec, + .tv_usec = (interval->tv_nsec + 999) / 1000, + }; + struct itimerval ival = { + .it_value = tv, + .it_interval = tv, + }; + if (setitimer(ITIMER_REAL, &ival, NULL) != 0) { + goto fail; + } + + timer->legacy = true; + return timer; +#endif + +fail: + free(timer); + return NULL; +} + +void xtimer_stop(struct timer *timer) { + if (!timer) { + return; + } + + if (timer->legacy) { +#if BFS_POSIX_TIMERS <= 0 + struct itimerval ival = {0}; + int ret = setitimer(ITIMER_REAL, &ival, NULL); + bfs_everify(ret == 0, "setitimer()"); +#endif + } else { +#if BFS_POSIX_TIMERS >= 0 + int ret = timer_delete(timer->timer); + bfs_everify(ret == 0, "timer_delete()"); +#endif + } + + free(timer); } diff --git a/src/xtime.h b/src/xtime.h index fb60ae4..b76fef2 100644 --- a/src/xtime.h +++ b/src/xtime.h @@ -13,9 +13,9 @@ /** * mktime() wrapper that reports errors more reliably. * - * @param[in,out] tm - * The struct tm to convert. - * @param[out] timep + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -25,9 +25,9 @@ int xmktime(struct tm *tm, time_t *timep); /** * A portable timegm(), the inverse of gmtime(). * - * @param[in,out] tm - * The struct tm to convert. - * @param[out] timep + * @tm[in,out] + * The struct tm to convert and normalize. + * @timep[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -37,9 +37,9 @@ int xtimegm(struct tm *tm, time_t *timep); /** * Parse an ISO 8601-style timestamp. * - * @param[in] str + * @str * The string to parse. - * @param[out] result + * @result[out] * A pointer to the result. * @return * 0 on success, -1 on failure. @@ -47,13 +47,62 @@ int xtimegm(struct tm *tm, time_t *timep); int xgetdate(const char *str, struct timespec *result); /** - * Get the current time. + * Add to a timespec. + */ +void timespec_add(struct timespec *lhs, const struct timespec *rhs); + +/** + * Subtract from a timespec. + */ +void timespec_sub(struct timespec *lhs, const struct timespec *rhs); + +/** + * Compare two timespecs. * - * @param[out] result - * A pointer to the result. * @return - * 0 on success, -1 on failure. + * An integer with the sign of (*lhs - *rhs). + */ +int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs); + +/** + * Update a minimum timespec. + */ +void timespec_min(struct timespec *dest, const struct timespec *src); + +/** + * Update a maximum timespec. + */ +void timespec_max(struct timespec *dest, const struct timespec *src); + +/** + * Convert a timespec to floating point. + * + * @return + * The value in nanoseconds. + */ +double timespec_ns(const struct timespec *ts); + +/** + * A timer. + */ +struct timer; + +/** + * Start a timer. + * + * @interval + * The regular interval at which to send SIGALRM. + * @return + * The new timer on success, otherwise NULL. + */ +struct timer *xtimer_start(const struct timespec *interval); + +/** + * Stop a timer. + * + * @timer + * The timer to stop. */ -int xgettime(struct timespec *result); +void xtimer_stop(struct timer *timer); #endif // BFS_XTIME_H diff --git a/tests/alloc.c b/tests/alloc.c index 6c0defd..4aae515 100644 --- a/tests/alloc.c +++ b/tests/alloc.c @@ -1,52 +1,78 @@ // 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; +struct flexible { + alignas(64) int foo[8]; + int bar[]; +}; - // Check sizeof_flex() - struct flexible { - alignas(64) int foo[8]; - int bar[]; - }; - ret &= bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible)); - ret &= bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0); +/** Check varena_realloc() poisoning for a size combination. */ +static struct flexible *check_varena_realloc(struct varena *varena, struct flexible *flexy, size_t old_count, size_t new_count) { + flexy = varena_realloc(varena, flexy, old_count, new_count); + bfs_everify(flexy); - size_t too_many = SIZE_MAX / sizeof(int) + 1; - ret &= bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX)); + for (size_t i = 0; i < new_count; ++i) { + if (i < old_count) { + bfs_check(flexy->bar[i] == (int)i); + } else { + flexy->bar[i] = i; + } + } - // Corner case: sizeof(type) > align_ceil(alignof(type), offsetof(type, member)) - // Doesn't happen in typical ABIs - ret &= bfs_check(flex_size(8, 16, 4, 4, 1) == 16); + return flexy; +} - // Make sure we detect allocation size overflows -#if __GNUC__ && !__clang__ -# pragma GCC diagnostic ignored "-Walloc-size-larger-than=" -#endif +void check_alloc(void) { + // Check aligned allocation + void *ptr; + bfs_everify((ptr = zalloc(64, 129))); + bfs_check((uintptr_t)ptr % 64 == 0); + bfs_echeck((ptr = xrealloc(ptr, 64, 129, 65))); + bfs_check((uintptr_t)ptr % 64 == 0); + free(ptr); - ret &= bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); - ret &= bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + // Check sizeof_flex() + bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible)); + bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0); + + // volatile to suppress -Walloc-size-larger-than + volatile size_t too_many = SIZE_MAX / sizeof(int) + 1; + bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX)); + + // Make sure we detect allocation size overflows + bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); + bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW); // varena tests struct varena varena; VARENA_INIT(&varena, struct flexible, bar); for (size_t i = 0; i < 256; ++i) { - bfs_verify(varena_alloc(&varena, i)); + bfs_everify(varena_alloc(&varena, i)); struct arena *arena = &varena.arenas[varena.narenas - 1]; - ret &= bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i)); + bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i)); } + // Check varena_realloc() (un)poisoning + struct flexible *flexy = varena_alloc(&varena, 160); + bfs_everify(flexy); + + flexy = check_varena_realloc(&varena, flexy, 0, 160); + flexy = check_varena_realloc(&varena, flexy, 160, 192); + flexy = check_varena_realloc(&varena, flexy, 192, 160); + flexy = check_varena_realloc(&varena, flexy, 160, 320); + flexy = check_varena_realloc(&varena, flexy, 320, 96); + varena_destroy(&varena); - return ret; } diff --git a/tests/gnu/gid.out b/tests/bfs/Dmulti.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid.out +++ b/tests/bfs/Dmulti.out diff --git a/tests/bfs/Dmulti.sh b/tests/bfs/Dmulti.sh new file mode 100644 index 0000000..35d64b1 --- /dev/null +++ b/tests/bfs/Dmulti.sh @@ -0,0 +1 @@ +bfs_diff -Dopt,tree,unknown basic diff --git a/tests/bfs/LD_stat.out b/tests/bfs/LD_stat.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/bfs/LD_stat.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/bfs/LD_stat.sh b/tests/bfs/LD_stat.sh new file mode 100644 index 0000000..d407de3 --- /dev/null +++ b/tests/bfs/LD_stat.sh @@ -0,0 +1 @@ +bfs_diff -LD stat links diff --git a/tests/bfs/LDstat.out b/tests/bfs/LDstat.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/bfs/LDstat.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/bfs/LDstat.sh b/tests/bfs/LDstat.sh new file mode 100644 index 0000000..ec6df0b --- /dev/null +++ b/tests/bfs/LDstat.sh @@ -0,0 +1 @@ +bfs_diff -LDstat links diff --git a/tests/bfs/L_noerror.out b/tests/bfs/L_noerror.out new file mode 100644 index 0000000..a514555 --- /dev/null +++ b/tests/bfs/L_noerror.out @@ -0,0 +1,11 @@ +loops +loops/broken +loops/deeply +loops/deeply/nested +loops/deeply/nested/dir +loops/file +loops/notdir +loops/skip +loops/skip/dir +loops/skip/loop +loops/symlink diff --git a/tests/bfs/L_noerror.sh b/tests/bfs/L_noerror.sh new file mode 100644 index 0000000..7db2a4d --- /dev/null +++ b/tests/bfs/L_noerror.sh @@ -0,0 +1 @@ +bfs_diff -L loops -noerror diff --git a/tests/bfs/O_3.sh b/tests/bfs/O_3.sh new file mode 100644 index 0000000..f159852 --- /dev/null +++ b/tests/bfs/O_3.sh @@ -0,0 +1 @@ +! invoke_bfs -O 3 basic diff --git a/tests/bfs/Sbfs.out b/tests/bfs/Sbfs.out new file mode 100644 index 0000000..bb3cd8d --- /dev/null +++ b/tests/bfs/Sbfs.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/e +basic/g +basic/i +basic/j +basic/k +basic/l +basic/c/d +basic/e/f +basic/g/h +basic/j/foo +basic/k/foo +basic/l/foo +basic/k/foo/bar +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/Sbfs.sh b/tests/bfs/Sbfs.sh new file mode 100644 index 0000000..72d92c8 --- /dev/null +++ b/tests/bfs/Sbfs.sh @@ -0,0 +1,2 @@ +invoke_bfs -Sbfs -s basic >"$OUT" +diff_output diff --git a/tests/bfs/color_ca.out b/tests/bfs/color_ca.out new file mode 100644 index 0000000..bf74202 --- /dev/null +++ b/tests/bfs/color_ca.out @@ -0,0 +1,4 @@ +[01;34m.[0m +[01;34m./[0m[01;36mlink[0m +[01;34m./[0m[30;41mcapable[0m +[01;34m./[0mnormal diff --git a/tests/bfs/color_ca.sh b/tests/bfs/color_ca.sh new file mode 100644 index 0000000..3aaaaf1 --- /dev/null +++ b/tests/bfs/color_ca.sh @@ -0,0 +1,10 @@ +test "$UNAME" = "Linux" || skip +invoke_bfs . -quit -capable || skip + +cd "$TEST" + +"$XTOUCH" normal capable +bfs_sudo setcap all+ep capable || skip +ln -s capable link + +LS_COLORS="ca=30;41:" bfs_diff . -color diff --git a/tests/bfs/color_ca_incapable.out b/tests/bfs/color_ca_incapable.out new file mode 100644 index 0000000..a439814 --- /dev/null +++ b/tests/bfs/color_ca_incapable.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.gz +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mlower.tar.gz +[01;34mrainbow/[0mlu.tar.GZ +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mul.TAR.gz +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR +[01;34mrainbow/[0mupper.TAR.GZ diff --git a/tests/bfs/color_ca_incapable.sh b/tests/bfs/color_ca_incapable.sh new file mode 100644 index 0000000..f46a127 --- /dev/null +++ b/tests/bfs/color_ca_incapable.sh @@ -0,0 +1 @@ +LS_COLORS="ca=30;41:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh index 4adba69..4c14610 100644 --- a/tests/bfs/color_ext_case.sh +++ b/tests/bfs/color_ext_case.sh @@ -1,6 +1,6 @@ -# *.gz=01;31:*.GZ=01;32 -- case sensitive -# *.tAr=01;33:*.TaR=01;33 -- case-insensitive -# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive -# *.txt=35:*TXT=36 -- case-insensitive -export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36" +# *.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32 -- case sensitive +# *.tAr=01;33:*.TaR=01;33 -- case-insensitive +# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive +# *.txt=35:*TXT=36 -- case-insensitive +export LS_COLORS="*.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_flipflop.out b/tests/bfs/color_ext_case_flipflop.out new file mode 100644 index 0000000..f4cc53c --- /dev/null +++ b/tests/bfs/color_ext_case_flipflop.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;33mlower.tar.gz[0m +[01;34mrainbow/[0m[01;33mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;33mul.TAR.gz[0m +[01;34mrainbow/[0m[01;33mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.gz +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.GZ +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_flipflop.sh b/tests/bfs/color_ext_case_flipflop.sh new file mode 100644 index 0000000..4d6f615 --- /dev/null +++ b/tests/bfs/color_ext_case_flipflop.sh @@ -0,0 +1 @@ +LS_COLORS="*.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33:*.tar.gz=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_nul.out b/tests/bfs/color_ext_case_nul.out new file mode 100644 index 0000000..8ccd9a7 --- /dev/null +++ b/tests/bfs/color_ext_case_nul.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;31mlower.gz[0m +[01;34mrainbow/[0m[01;31mlower.tar.gz[0m +[01;34mrainbow/[0m[01;31mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;31mul.TAR.gz[0m +[01;34mrainbow/[0m[01;31mupper.GZ[0m +[01;34mrainbow/[0m[01;31mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_nul.sh b/tests/bfs/color_ext_case_nul.sh new file mode 100644 index 0000000..68fea1c --- /dev/null +++ b/tests/bfs/color_ext_case_nul.sh @@ -0,0 +1,5 @@ +# Regression test: embedded NUL bytes in an extension caused an assertion +# failure in the trie implementation + +export LS_COLORS='*.gz=01;31:*\0.GZ=01;32:' +bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_case_priority.out b/tests/bfs/color_ext_case_priority.out new file mode 100644 index 0000000..4a6c9a0 --- /dev/null +++ b/tests/bfs/color_ext_case_priority.out @@ -0,0 +1,27 @@ +[01;34m$'rainbow/\e[1m'[0m +[01;34m$'rainbow/\e[1m/'[0m$'\e[0m' +[01;34mrainbow[0m +[01;34mrainbow/[0m[01;31mlower.tar.gz[0m +[01;34mrainbow/[0m[01;32mexec.sh[0m +[01;34mrainbow/[0m[01;32mupper.TAR.GZ[0m +[01;34mrainbow/[0m[01;33mlower.gz[0m +[01;34mrainbow/[0m[01;33mlu.tar.GZ[0m +[01;34mrainbow/[0m[01;33mul.TAR.gz[0m +[01;34mrainbow/[0m[01;33mupper.GZ[0m +[01;34mrainbow/[0m[01;35msocket[0m +[01;34mrainbow/[0m[01;36mbroken[0m +[01;34mrainbow/[0m[01;36mchardev_link[0m +[01;34mrainbow/[0m[01;36mlink.txt[0m +[01;34mrainbow/[0m[30;42msticky_ow[0m +[01;34mrainbow/[0m[30;43msgid[0m +[01;34mrainbow/[0m[33mpipe[0m +[01;34mrainbow/[0m[34;42mow[0m +[01;34mrainbow/[0m[37;41msugid[0m +[01;34mrainbow/[0m[37;41msuid[0m +[01;34mrainbow/[0m[37;44msticky[0m +[01;34mrainbow/[0mfile.dat +[01;34mrainbow/[0mfile.txt +[01;34mrainbow/[0mlower.tar +[01;34mrainbow/[0mmh1 +[01;34mrainbow/[0mmh2 +[01;34mrainbow/[0mupper.TAR diff --git a/tests/bfs/color_ext_case_priority.sh b/tests/bfs/color_ext_case_priority.sh new file mode 100644 index 0000000..f178c56 --- /dev/null +++ b/tests/bfs/color_ext_case_priority.sh @@ -0,0 +1 @@ +LS_COLORS="*.gz=01;33:*.tar.gz=01;31:*.TAR.GZ=01;32:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh index ac4c7fb..9f818c9 100644 --- a/tests/bfs/color_ext_override.sh +++ b/tests/bfs/color_ext_override.sh @@ -1 +1 @@ -LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color +LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;30:*.gz=01;33:" bfs_diff rainbow -color diff --git a/tests/bfs/color_ls.out b/tests/bfs/color_ls.out index f69eb9c..cc64318 100644 --- a/tests/bfs/color_ls.out +++ b/tests/bfs/color_ls.out @@ -8,5 +8,5 @@ [01;34mfoo/bar/[0m[01;31mnowhere/nothing[0m [01;34mfoo/bar/[0mbaz [01;34mfoo/bar/[0mbaz -[01;34mfoo/bar/baz[0m[01;31m//qux[0m -[01;34mfoo/bar/baz[0m[01;31m//qux[0m +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m +[01;34mfoo/bar/[0mbaz[01;31m//qux[0m diff --git a/tests/bfs/color_notdir_slash_error.out b/tests/bfs/color_notdir_slash_error.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/bfs/color_notdir_slash_error.out diff --git a/tests/bfs/color_notdir_slash_error.sh b/tests/bfs/color_notdir_slash_error.sh new file mode 100644 index 0000000..ca26d50 --- /dev/null +++ b/tests/bfs/color_notdir_slash_error.sh @@ -0,0 +1,2 @@ +# Regression test: infinite loop printing the error message for .../notdir/nowhere +! bfs_diff -color links/notdir/nowhere diff --git a/tests/bfs/execdir_path_relative_slash.out b/tests/bfs/execdir_path_relative_slash.out new file mode 100644 index 0000000..62b31f6 --- /dev/null +++ b/tests/bfs/execdir_path_relative_slash.out @@ -0,0 +1,19 @@ +./a +./b +./bar +./bar +./basic +./baz +./c +./d +./e +./f +./foo +./foo +./foo +./g +./h +./i +./j +./k +./l diff --git a/tests/bfs/execdir_path_relative_slash.sh b/tests/bfs/execdir_path_relative_slash.sh new file mode 100644 index 0000000..fb5a924 --- /dev/null +++ b/tests/bfs/execdir_path_relative_slash.sh @@ -0,0 +1 @@ +PATH="foo:$PATH" bfs_diff basic -execdir /bin/sh -c 'printf "%s\\n" "$@"' sh {} + diff --git a/tests/bfs/files0_from_root.sh b/tests/bfs/files0_from_root.sh new file mode 100644 index 0000000..6ba5f00 --- /dev/null +++ b/tests/bfs/files0_from_root.sh @@ -0,0 +1,2 @@ +printf 'basic\0' >"$TEST/input" +! invoke_bfs basic -files0-from "$TEST/input" diff --git a/tests/bfs/noerror.out b/tests/bfs/noerror.out new file mode 100644 index 0000000..c4f8ce4 --- /dev/null +++ b/tests/bfs/noerror.out @@ -0,0 +1,4 @@ +inaccessible +inaccessible/dir +inaccessible/file +inaccessible/link diff --git a/tests/bfs/noerror.sh b/tests/bfs/noerror.sh new file mode 100644 index 0000000..e334f8f --- /dev/null +++ b/tests/bfs/noerror.sh @@ -0,0 +1 @@ +bfs_diff inaccessible -noerror diff --git a/tests/bfs/noerror_nowarn.sh b/tests/bfs/noerror_nowarn.sh new file mode 100644 index 0000000..26e7e68 --- /dev/null +++ b/tests/bfs/noerror_nowarn.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs inaccessible -noerror -nowarn 2>&1 >/dev/null) +[ -z "$stderr" ] diff --git a/tests/bfs/noerror_warn.sh b/tests/bfs/noerror_warn.sh new file mode 100644 index 0000000..ec85f4c --- /dev/null +++ b/tests/bfs/noerror_warn.sh @@ -0,0 +1,2 @@ +stderr=$(invoke_bfs inaccessible -noerror -warn 2>&1 >/dev/null) +[ -n "$stderr" ] diff --git a/tests/bfs/nohidden.out b/tests/bfs/nohidden.out index d3ec901..84e6bd2 100644 --- a/tests/bfs/nohidden.out +++ b/tests/bfs/nohidden.out @@ -1,4 +1,8 @@ + +/n weirdnames +weirdnames/ +weirdnames/ weirdnames/ weirdnames/ /j weirdnames/! @@ -11,6 +15,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -19,3 +25,5 @@ weirdnames/[ weirdnames/[/k weirdnames/\ weirdnames/\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bfs/nohidden_depth.out b/tests/bfs/nohidden_depth.out index d3ec901..84e6bd2 100644 --- a/tests/bfs/nohidden_depth.out +++ b/tests/bfs/nohidden_depth.out @@ -1,4 +1,8 @@ + +/n weirdnames +weirdnames/ +weirdnames/ weirdnames/ weirdnames/ /j weirdnames/! @@ -11,6 +15,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -19,3 +25,5 @@ weirdnames/[ weirdnames/[/k weirdnames/\ weirdnames/\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bfs/perm_leading_plus_symbolic.out b/tests/bfs/perm_leading_plus_symbolic.out index e69de29..09bc88f 100644 --- a/tests/bfs/perm_leading_plus_symbolic.out +++ b/tests/bfs/perm_leading_plus_symbolic.out @@ -0,0 +1,3 @@ +perms +perms/drwxr-xr-x +perms/frwxr-xr-x diff --git a/tests/bfs/warn_without_noerror.sh b/tests/bfs/warn_without_noerror.sh new file mode 100644 index 0000000..5167309 --- /dev/null +++ b/tests/bfs/warn_without_noerror.sh @@ -0,0 +1,2 @@ +# bfs shouldn't print "warning: Suppressed errors" without -noerror +! invoke_bfs inaccessible -warn 2>&1 >/dev/null | grep warning >&2 diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh index 02c8173..4683764 100644 --- a/tests/bfs/xtype_depth.sh +++ b/tests/bfs/xtype_depth.sh @@ -1,2 +1,2 @@ # Make sure -xtype is considered side-effecting for facts_when_impure -! invoke_bfs loops -xtype l -depth 100 +! invoke_bfs inaccessible/link -xtype l -depth 100 diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh index 8d75d7d..c1d94f3 100644 --- a/tests/bfs/xtype_reorder.sh +++ b/tests/bfs/xtype_reorder.sh @@ -1,4 +1,3 @@ # Make sure -xtype is not reordered in front of anything -- if -xtype runs # before -links 100, it will report an ELOOP error -bfs_diff loops -links 100 -xtype l -invoke_bfs loops -links 100 -xtype l +bfs_diff inaccessible/link -links 100 -xtype l diff --git a/tests/bfstd.c b/tests/bfstd.c index f0f61ce..6e15e2b 100644 --- a/tests/bfstd.c +++ b/tests/bfstd.c @@ -1,93 +1,212 @@ // 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 <limits.h> +#include <stdint.h> #include <stdlib.h> #include <string.h> -/** Check the result of xdirname()/xbasename(). */ -static bool check_base_dir(const char *path, const char *dir, const char *base) { - bool ret = true; +/** asciilen() test cases. */ +static void check_asciilen(void) { + bfs_check(asciilen("") == 0); + bfs_check(asciilen("@") == 1); + bfs_check(asciilen("@@") == 2); + bfs_check(asciilen("\xFF@") == 0); + bfs_check(asciilen("@\xFF") == 1); + bfs_check(asciilen("@@@@@@@@") == 8); + bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16); + bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24); + bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15); + bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16); + bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17); + bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); + bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); + bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); +} +/** Check the result of xdirname()/xbasename(). */ +static void check_base_dir(const char *path, const char *dir, const char *base) { char *xdir = xdirname(path); bfs_everify(xdir, "xdirname()"); - ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); + bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir); free(xdir); char *xbase = xbasename(path); bfs_everify(xbase, "xbasename()"); - ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); + bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base); free(xbase); +} - return ret; +/** xdirname()/xbasename() test cases. */ +static void check_basedirs(void) { + // From man 3p basename + check_base_dir("usr", ".", "usr"); + check_base_dir("usr/", ".", "usr"); + check_base_dir("", ".", "."); + check_base_dir("/", "/", "/"); + // check_base_dir("//", "/" or "//", "/" or "//"); + check_base_dir("///", "/", "/"); + check_base_dir("/usr/", "/", "usr"); + check_base_dir("/usr/lib", "/usr", "lib"); + check_base_dir("//usr//lib//", "//usr", "lib"); + check_base_dir("/home//dwc//test", "/home//dwc", "test"); } /** Check the result of wordesc(). */ -static bool check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { +static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) { char buf[256]; char *end = buf + sizeof(buf); char *esc = wordesc(buf, end, str, flags); - return bfs_check(esc != end) - && bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + if (bfs_check(esc != end)) { + bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp); + } } -bool check_bfstd(void) { - bool ret = true; - - ret &= bfs_check(asciilen("") == 0); - ret &= bfs_check(asciilen("@") == 1); - ret &= bfs_check(asciilen("@@") == 2); - ret &= bfs_check(asciilen("\xFF@") == 0); - ret &= bfs_check(asciilen("@\xFF") == 1); - ret &= bfs_check(asciilen("@@@@@@@@") == 8); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16); - ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17); - ret &= bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7); - ret &= bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8); - ret &= bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9); - - // From man 3p basename - ret &= check_base_dir("usr", ".", "usr"); - ret &= check_base_dir("usr/", ".", "usr"); - ret &= check_base_dir("", ".", "."); - ret &= check_base_dir("/", "/", "/"); - // check_base_dir("//", "/" or "//", "/" or "//"); - ret &= check_base_dir("///", "/", "/"); - ret &= check_base_dir("/usr/", "/", "usr"); - ret &= check_base_dir("/usr/lib", "/usr", "lib"); - ret &= check_base_dir("//usr//lib//", "//usr", "lib"); - ret &= check_base_dir("/home//dwc//test", "/home//dwc", "test"); - - ret &= check_wordesc("", "\"\"", WESC_SHELL); - ret &= check_wordesc("word", "word", WESC_SHELL); - ret &= check_wordesc("two words", "\"two words\"", WESC_SHELL); - ret &= check_wordesc("word's", "\"word's\"", WESC_SHELL); - ret &= check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL); - ret &= check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL); - ret &= check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("~user", "\"~user\"", WESC_SHELL); +/** wordesc() test cases. */ +static void check_wordescs(void) { + check_wordesc("", "\"\"", WESC_SHELL); + check_wordesc("word", "word", WESC_SHELL); + check_wordesc("two words", "\"two words\"", WESC_SHELL); + check_wordesc("word's", "\"word's\"", WESC_SHELL); + check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL); + check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL); + check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY); + check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY); + check_wordesc("~user", "\"~user\"", WESC_SHELL); const char *charmap = nl_langinfo(CODESET); if (strcmp(charmap, "UTF-8") == 0) { - ret &= check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); - ret &= check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); + check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY); + check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY); + check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL); } +} + +/** xstrto*() test cases. */ +static void check_strtox(void) { + short s; + unsigned short us; + int i; + unsigned int ui; + long l; + unsigned long ul; + long long ll; + unsigned long long ull; + char *end; + +#define check_strtouerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtous(str, end, base, &us) != 0 && errno == err); \ + bfs_echeck(xstrtoui(str, end, base, &ui) != 0 && errno == err); \ + bfs_echeck(xstrtoul(str, end, base, &ul) != 0 && errno == err); \ + bfs_echeck(xstrtoull(str, end, base, &ull) != 0 && errno == err); \ + } while (0) + + check_strtouerr(ERANGE, "-1", NULL, 0); + check_strtouerr(ERANGE, "-0x1", NULL, 0); - ret &= bfs_check(xstrwidth("Hello world") == 11); - ret &= bfs_check(xstrwidth("Hello\1world") == 10); + check_strtouerr(EINVAL, "-", NULL, 0); + check_strtouerr(EINVAL, "-q", NULL, 0); + check_strtouerr(EINVAL, "-1q", NULL, 0); + check_strtouerr(EINVAL, "-0x", NULL, 0); + +#define check_strtoerr(err, str, end, base) \ + do { \ + bfs_echeck(xstrtos(str, end, base, &s) != 0 && errno == err); \ + bfs_echeck(xstrtoi(str, end, base, &i) != 0 && errno == err); \ + bfs_echeck(xstrtol(str, end, base, &l) != 0 && errno == err); \ + bfs_echeck(xstrtoll(str, end, base, &ll) != 0 && errno == err); \ + check_strtouerr(err, str, end, base); \ + } while (0) + + check_strtoerr(EINVAL, "", NULL, 0); + check_strtoerr(EINVAL, "", &end, 0); + check_strtoerr(EINVAL, " 1 ", &end, 0); + check_strtoerr(EINVAL, " -1", NULL, 0); + check_strtoerr(EINVAL, " 123", NULL, 0); + check_strtoerr(EINVAL, "123 ", NULL, 0); + check_strtoerr(EINVAL, "0789", NULL, 0); + check_strtoerr(EINVAL, "789A", NULL, 0); + check_strtoerr(EINVAL, "0x", NULL, 0); + check_strtoerr(EINVAL, "0x789A", NULL, 10); + check_strtoerr(EINVAL, "0x-1", NULL, 0); + +#define check_strtotype(type, min, max, fmt, fn, str, base, v, n) \ + do { \ + if ((n) >= min && (n) <= max) { \ + bfs_echeck(fn(str, NULL, base, &v) == 0); \ + bfs_check(v == (type)(n), "%s('%s') == " fmt " (!= " fmt ")", #fn, str, v, (type)(n)); \ + } else { \ + bfs_echeck(fn(str, NULL, base, &v) != 0 && errno == ERANGE); \ + } \ + } while (0) + +#define check_strtoint(str, base, n) \ + do { \ + check_strtotype( signed short, SHRT_MIN, SHRT_MAX, "%d", xstrtos, str, base, s, n); \ + check_strtotype( signed int, INT_MIN, INT_MAX, "%d", xstrtoi, str, base, i, n); \ + check_strtotype( signed long, LONG_MIN, LONG_MAX, "%ld", xstrtol, str, base, l, n); \ + check_strtotype( signed long long, LLONG_MIN, LLONG_MAX, "%lld", xstrtoll, str, base, ll, n); \ + check_strtotype(unsigned short, 0, USHRT_MAX, "%u", xstrtous, str, base, us, n); \ + check_strtotype(unsigned int, 0, UINT_MAX, "%u", xstrtoui, str, base, ui, n); \ + check_strtotype(unsigned long, 0, ULONG_MAX, "%lu", xstrtoul, str, base, ul, n); \ + check_strtotype(unsigned long long, 0, ULLONG_MAX, "%llu", xstrtoull, str, base, ull, n); \ + } while (0) + + check_strtoint("123", 0, 123); + check_strtoint("+123", 0, 123); + check_strtoint("-123", 0, -123); + + check_strtoint("0123", 0, 0123); + check_strtoint("0x789A", 0, 0x789A); + + check_strtoint("0123", 10, 123); + check_strtoint("0789", 10, 789); + + check_strtoint("123", 16, 0x123); + + check_strtoint("0x7FFF", 0, 0x7FFF); + check_strtoint("-0x8000", 0, -0x8000); + + check_strtoint("0x7FFFFFFF", 0, 0x7FFFFFFFL); + check_strtoint("-0x80000000", 0, -0x7FFFFFFFL - 1); + + check_strtoint("0x7FFFFFFFFFFFFFFF", 0, 0x7FFFFFFFFFFFFFFFLL); + check_strtoint("-0x8000000000000000", 0, -0x7FFFFFFFFFFFFFFFLL - 1); + +#define check_strtoend(str, estr, base, n) \ + do { \ + bfs_echeck(xstrtoll(str, &end, base, &ll) == 0); \ + bfs_check(ll == (n), "xstrtoll('%s') == %lld (!= %lld)", str, ll, (long long)(n)); \ + bfs_check(strcmp(end, estr) == 0, "xstrtoll('%s'): end == '%s' (!= '%s')", str, end, estr); \ + } while (0) + + check_strtoend("123 ", " ", 0, 123); + check_strtoend("0789", "89", 0, 07); + check_strtoend("789A", "A", 0, 789); + check_strtoend("0xDEFG", "G", 0, 0xDEF); +} + +/** xstrwidth() test cases. */ +static void check_strwidth(void) { + bfs_check(xstrwidth("Hello world") == 11); + bfs_check(xstrwidth("Hello\1world") == 10); +} - return ret; +void check_bfstd(void) { + check_asciilen(); + check_basedirs(); + check_wordescs(); + check_strtox(); + check_strwidth(); } diff --git a/tests/bit.c b/tests/bit.c index 674d1b2..09d470b 100644 --- a/tests/bit.c +++ b/tests/bit.c @@ -1,141 +1,160 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "tests.h" + +#include "bfs.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); +// Polyfill C23's one-argument static_assert() +#if __STDC_VERSION__ < C23 +# undef static_assert +# define static_assert(...) _Static_assert(__VA_ARGS__, #__VA_ARGS__) +#endif + +static_assert(UMAX_WIDTH(0x1) == 1); +static_assert(UMAX_WIDTH(0x3) == 2); +static_assert(UMAX_WIDTH(0x7) == 3); +static_assert(UMAX_WIDTH(0xF) == 4); +static_assert(UMAX_WIDTH(0xFF) == 8); +static_assert(UMAX_WIDTH(0xFFF) == 12); +static_assert(UMAX_WIDTH(0xFFFF) == 16); #define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1) #define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1) #define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1) -bfs_static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); -bfs_static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); -bfs_static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH)); +static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH)); +static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH)); +static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH)); -bfs_static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH)); -bfs_static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH)); -bfs_static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH)); +static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH)); +static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH)); +static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH)); -bfs_static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); -bfs_static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); -bfs_static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH)); +static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH)); +static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH)); +static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH)); -bfs_static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH)); -bfs_static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH)); -bfs_static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH)); +static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH)); +static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH)); +static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH)); -bfs_static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); -bfs_static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); -bfs_static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH)); +static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH)); +static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH)); +static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH)); -bfs_static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH)); -bfs_static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH)); -bfs_static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH)); +static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH)); +static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH)); +static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH)); -bfs_static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); -bfs_static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); -bfs_static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH)); +static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH)); +static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH)); +static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH)); -bfs_static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH)); -bfs_static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH)); -bfs_static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH)); +static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH)); +static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH)); +static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH)); #define check_eq(a, b) \ bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b)) -bool check_bit(void) { - bool ret = true; - - const char *str = "\x1\x2\x3\x4"; +void check_bit(void) { + const char *str = "\x1\x2\x3\x4\x5\x6\x7\x8"; uint32_t word; memcpy(&word, str, sizeof(word)); #if ENDIAN_NATIVE == ENDIAN_LITTLE - ret &= check_eq(word, 0x04030201); + check_eq(word, 0x04030201); #elif ENDIAN_NATIVE == ENDIAN_BIG - ret &= check_eq(word, 0x01020304); + check_eq(word, 0x01020304); #else # warning "Skipping byte order tests on mixed/unknown-endian machine" #endif - ret &= check_eq(bswap((uint8_t)0x12), 0x12); - ret &= check_eq(bswap((uint16_t)0x1234), 0x3412); - ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412); - ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412); - - ret &= check_eq(count_ones(0x0), 0); - ret &= check_eq(count_ones(0x1), 1); - ret &= check_eq(count_ones(0x2), 1); - ret &= check_eq(count_ones(0x3), 2); - ret &= check_eq(count_ones(0x137F), 10); - - ret &= check_eq(count_zeros(0), INT_WIDTH); - ret &= check_eq(count_zeros(0L), LONG_WIDTH); - ret &= check_eq(count_zeros(0LL), LLONG_WIDTH); - ret &= check_eq(count_zeros((uint8_t)0), 8); - ret &= check_eq(count_zeros((uint16_t)0), 16); - ret &= check_eq(count_zeros((uint32_t)0), 32); - ret &= check_eq(count_zeros((uint64_t)0), 64); - - ret &= check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A); - ret &= check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123); - ret &= check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345); - ret &= check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678); - - ret &= check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A); - ret &= check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341); - ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123); - ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678); - - for (int i = 0; i < 16; ++i) { + check_eq(bswap((uint8_t)0x12), 0x12); + check_eq(bswap((uint16_t)0x1234), 0x3412); + check_eq(bswap((uint32_t)0x12345678), 0x78563412); + check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412); + + // Make sure we can bswap() every unsigned type + (void)bswap((unsigned char)0); + (void)bswap((unsigned short)0); + (void)bswap(0U); + (void)bswap(0UL); + (void)bswap(0ULL); + + check_eq(load8_beu8(str), 0x01); + check_eq(load8_leu8(str), 0x01); + check_eq(load8_beu16(str), 0x0102); + check_eq(load8_leu16(str), 0x0201); + check_eq(load8_beu32(str), 0x01020304); + check_eq(load8_leu32(str), 0x04030201); + check_eq(load8_beu64(str), 0x0102030405060708ULL); + check_eq(load8_leu64(str), 0x0807060504030201ULL); + + check_eq(count_ones(0x0U), 0); + check_eq(count_ones(0x1U), 1); + check_eq(count_ones(0x2U), 1); + check_eq(count_ones(0x3U), 2); + check_eq(count_ones(0x137FU), 10); + + check_eq(count_zeros(0U), UINT_WIDTH); + check_eq(count_zeros(0UL), ULONG_WIDTH); + check_eq(count_zeros(0ULL), ULLONG_WIDTH); + check_eq(count_zeros((uint8_t)0), 8); + check_eq(count_zeros((uint16_t)0), 16); + check_eq(count_zeros((uint32_t)0), 32); + check_eq(count_zeros((uint64_t)0), 64); + + check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A); + check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123); + check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345); + check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678); + + check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A); + check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341); + check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123); + check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678); + + for (unsigned int i = 0; i < 16; ++i) { uint16_t n = (uint16_t)1 << i; - for (int j = i; j < 16; ++j) { + for (unsigned int j = i; j < 16; ++j) { uint16_t m = (uint16_t)1 << j; uint16_t nm = n | m; - ret &= check_eq(count_ones(nm), 1 + (n != m)); - ret &= check_eq(count_zeros(nm), 15 - (n != m)); - ret &= check_eq(leading_zeros(nm), 15 - j); - ret &= check_eq(trailing_zeros(nm), i); - ret &= check_eq(first_leading_one(nm), j + 1); - ret &= check_eq(first_trailing_one(nm), i + 1); - ret &= check_eq(bit_width(nm), j + 1); - ret &= check_eq(bit_floor(nm), m); + check_eq(count_ones(nm), 1 + (n != m)); + check_eq(count_zeros(nm), 15 - (n != m)); + check_eq(leading_zeros(nm), 15 - j); + check_eq(trailing_zeros(nm), i); + check_eq(first_leading_one(nm), 16 - j); + check_eq(first_trailing_one(nm), i + 1); + check_eq(bit_width(nm), j + 1); + check_eq(bit_floor(nm), m); if (n == m) { - ret &= check_eq(bit_ceil(nm), m); - ret &= bfs_check(has_single_bit(nm)); + check_eq(bit_ceil(nm), m); + bfs_check(has_single_bit(nm)); } else { if (j < 15) { - ret &= check_eq(bit_ceil(nm), (m << 1)); + check_eq(bit_ceil(nm), (m << 1)); } - ret &= bfs_check(!has_single_bit(nm)); + bfs_check(!has_single_bit(nm)); } } } - ret &= check_eq(leading_zeros((uint16_t)0), 16); - ret &= check_eq(trailing_zeros((uint16_t)0), 16); - ret &= check_eq(first_leading_one(0), 0); - ret &= check_eq(first_trailing_one(0), 0); - ret &= check_eq(bit_width(0), 0); - ret &= check_eq(bit_floor(0), 0); - ret &= check_eq(bit_ceil(0), 1); - - ret &= bfs_check(!has_single_bit(0)); - ret &= bfs_check(!has_single_bit(UINT32_MAX)); - ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1))); - - return ret; + check_eq(leading_zeros((uint16_t)0), 16); + check_eq(trailing_zeros((uint16_t)0), 16); + check_eq(first_leading_one(0U), 0); + check_eq(first_trailing_one(0U), 0); + check_eq(bit_width(0U), 0); + check_eq(bit_floor(0U), 0); + check_eq(bit_ceil(0U), 1); + + bfs_check(!has_single_bit(0U)); + bfs_check(!has_single_bit(UINT32_MAX)); + bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1))); } diff --git a/tests/bsd/Hf.out b/tests/bsd/Hf.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/bsd/Hf.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/bsd/Hf.sh b/tests/bsd/Hf.sh new file mode 100644 index 0000000..333280c --- /dev/null +++ b/tests/bsd/Hf.sh @@ -0,0 +1 @@ +bfs_diff -Hf links/deeply/nested/dir diff --git a/tests/bsd/X.out b/tests/bsd/X.out index afa84f7..dbe2408 100644 --- a/tests/bsd/X.out +++ b/tests/bsd/X.out @@ -9,6 +9,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -17,3 +19,5 @@ weirdnames/... weirdnames/.../h weirdnames/[ weirdnames/[/k +weirdnames/{ +weirdnames/{/l diff --git a/tests/bsd/perm_000_plus.out b/tests/bsd/perm_000_plus.out index d7494b8..e279684 100644 --- a/tests/bsd/perm_000_plus.out +++ b/tests/bsd/perm_000_plus.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out index 9a5b95a..1b6d885 100644 --- a/tests/bsd/perm_222_plus.out +++ b/tests/bsd/perm_222_plus.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out index 7e5ae98..eef88ca 100644 --- a/tests/bsd/perm_644_plus.out +++ b/tests/bsd/perm_644_plus.out @@ -1,7 +1,26 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/bsd/printx.out b/tests/bsd/printx.out index 04bf9a9..034b2da 100644 --- a/tests/bsd/printx.out +++ b/tests/bsd/printx.out @@ -1,3 +1,5 @@ + +/n weirdnames weirdnames/! weirdnames/!- @@ -9,6 +11,8 @@ weirdnames/(-/c weirdnames/(/b weirdnames/) weirdnames/)/g +weirdnames/* +weirdnames/*/m weirdnames/, weirdnames/,/f weirdnames/- @@ -17,7 +21,11 @@ weirdnames/... weirdnames/.../h weirdnames/[ weirdnames/[/k +weirdnames/\ +weirdnames/\ weirdnames/\ weirdnames/\ /j weirdnames/\\ weirdnames/\\/i +weirdnames/{ +weirdnames/{/l diff --git a/tests/bsd/s.out b/tests/bsd/s.out index 6b790c2..5c85ac8 100644 --- a/tests/bsd/s.out +++ b/tests/bsd/s.out @@ -1,12 +1,16 @@ weirdnames +weirdnames/ + weirdnames/ weirdnames/! weirdnames/!- weirdnames/( weirdnames/(- weirdnames/) +weirdnames/* weirdnames/, weirdnames/- weirdnames/... weirdnames/[ weirdnames/\ +weirdnames/{ diff --git a/tests/color.sh b/tests/color.sh index 4f4312e..9e2e0f6 100644 --- a/tests/color.sh +++ b/tests/color.sh @@ -46,9 +46,7 @@ show_bar() { # Name the pipe deterministically based on the ttyname, so that concurrent # tests.sh runs on the same terminal (e.g. make -jN check) cooperate - local pipe - pipe=$(printf '%s' "$TTY" | tr '/' '-') - pipe="${TMPDIR:-/tmp}/bfs$pipe.bar" + local pipe="${TMPDIR:-/tmp}/bfs${TTY//\//-}.bar" if mkfifo "$pipe" 2>/dev/null; then # We won the race, create the background process to manage the bar @@ -78,7 +76,7 @@ hide_bar() { # The background process that muxes multiple status bars for one TTY bar_proc() { # Read from the pipe, write to the TTY - exec <"$1" >"$TTY" + exec <"$1" >/dev/tty # Delete the pipe when done defer rm "$1" @@ -133,7 +131,7 @@ bar_proc() { # Resize the status bar resize_bar() { # Bash gets $LINES from stderr, so if it's redirected use tput instead - TTY_HEIGHT="${LINES:-$(tput lines 2>"$TTY")}" + TTY_HEIGHT="${LINES:-$(tput lines 2>/dev/tty)}" if ((BAR_HEIGHT == 0)); then return diff --git a/tests/common/HLP.out b/tests/common/HLP.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/common/HLP.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/common/HLP.sh b/tests/common/HLP.sh new file mode 100644 index 0000000..4b6d631 --- /dev/null +++ b/tests/common/HLP.sh @@ -0,0 +1 @@ +bfs_diff -HLP links/deeply/nested/dir diff --git a/tests/common/L_mount.out b/tests/common/L_mount.out deleted file mode 100644 index 788579d..0000000 --- a/tests/common/L_mount.out +++ /dev/null @@ -1,5 +0,0 @@ -. -./foo -./foo/bar -./foo/qux -./mnt diff --git a/tests/common/amin.out b/tests/common/amin.out new file mode 100644 index 0000000..af57325 --- /dev/null +++ b/tests/common/amin.out @@ -0,0 +1,6 @@ +-amin 1: ./one_minute_ago +-amin +1: ./one_hour_ago +-amin +1: ./two_minutes_ago +-amin -1: ./in_one_hour +-amin -1: ./in_one_minute +-amin -1: ./thirty_seconds_ago diff --git a/tests/common/amin.sh b/tests/common/amin.sh new file mode 100644 index 0000000..92c3531 --- /dev/null +++ b/tests/common/amin.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -at "@$((now - 121))" two_minutes_ago +"$XTOUCH" -at "@$((now - 61))" one_minute_ago +"$XTOUCH" -at "@$((now - 30))" thirty_seconds_ago +"$XTOUCH" -at "@$((now + 60))" in_one_minute +"$XTOUCH" -at "@$((now + 60 * 60))" in_one_hour + +bfs_diff . -mindepth 1 \ + \( -amin -1 -exec printf -- '-amin -1: %s\n' {} \; -o -true \) \ + \( -amin 1 -exec printf -- '-amin 1: %s\n' {} \; -o -true \) \ + \( -amin +1 -exec printf -- '-amin +1: %s\n' {} \; -o -true \) diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out index da45e23..49f773d 100644 --- a/tests/common/empty_error.out +++ b/tests/common/empty_error.out @@ -1,3 +1 @@ -./bar -./baz -./qux +inaccessible/file diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh index 7c8049c..3438cca 100644 --- a/tests/common/empty_error.sh +++ b/tests/common/empty_error.sh @@ -1,7 +1 @@ -cd "$TEST" - -"$XTOUCH" -p foo/ bar/ baz qux -chmod -r foo baz -defer chmod +r foo baz - -! bfs_diff . -empty +! bfs_diff inaccessible -empty diff --git a/tests/gnu/gid_minus.out b/tests/common/gid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_minus.out +++ b/tests/common/gid.out diff --git a/tests/gnu/gid.sh b/tests/common/gid.sh index 2707b4a..2707b4a 100644 --- a/tests/gnu/gid.sh +++ b/tests/common/gid.sh diff --git a/tests/common/gid_invalid_id.sh b/tests/common/gid_invalid_id.sh new file mode 100644 index 0000000..74f0055 --- /dev/null +++ b/tests/common/gid_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -gid 1eW6f5RM9Qi diff --git a/tests/common/gid_invalid_name.sh b/tests/common/gid_invalid_name.sh new file mode 100644 index 0000000..0e2e5f5 --- /dev/null +++ b/tests/common/gid_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -gid eW6f5RM9Qi diff --git a/tests/gnu/gid_minus_plus.out b/tests/common/gid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_minus_plus.out +++ b/tests/common/gid_minus.out diff --git a/tests/gnu/gid_minus.sh b/tests/common/gid_minus.sh index e3822f0..e3822f0 100644 --- a/tests/gnu/gid_minus.sh +++ b/tests/common/gid_minus.sh diff --git a/tests/gnu/gid_plus.out b/tests/common/gid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_plus.out +++ b/tests/common/gid_minus_plus.out diff --git a/tests/gnu/gid_minus_plus.sh b/tests/common/gid_minus_plus.sh index 4ff0877..4ff0877 100644 --- a/tests/gnu/gid_minus_plus.sh +++ b/tests/common/gid_minus_plus.sh diff --git a/tests/gnu/gid_plus_plus.out b/tests/common/gid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/gid_plus_plus.out +++ b/tests/common/gid_plus.out diff --git a/tests/gnu/gid_plus.sh b/tests/common/gid_plus.sh index ccba0e6..ccba0e6 100644 --- a/tests/gnu/gid_plus.sh +++ b/tests/common/gid_plus.sh diff --git a/tests/gnu/uid.out b/tests/common/gid_plus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid.out +++ b/tests/common/gid_plus_plus.out diff --git a/tests/gnu/gid_plus_plus.sh b/tests/common/gid_plus_plus.sh index ec7ae86..ec7ae86 100644 --- a/tests/gnu/gid_plus_plus.sh +++ b/tests/common/gid_plus_plus.sh diff --git a/tests/common/iname.sh b/tests/common/iname.sh deleted file mode 100644 index c25a646..0000000 --- a/tests/common/iname.sh +++ /dev/null @@ -1,2 +0,0 @@ -invoke_bfs -quit -iname PATTERN || skip -bfs_diff basic -iname '*F*' diff --git a/tests/common/mmin.out b/tests/common/mmin.out new file mode 100644 index 0000000..4c79a16 --- /dev/null +++ b/tests/common/mmin.out @@ -0,0 +1,6 @@ +-mmin 1: ./one_minute_ago +-mmin +1: ./one_hour_ago +-mmin +1: ./two_minutes_ago +-mmin -1: ./in_one_hour +-mmin -1: ./in_one_minute +-mmin -1: ./thirty_seconds_ago diff --git a/tests/common/mmin.sh b/tests/common/mmin.sh new file mode 100644 index 0000000..4e1d19c --- /dev/null +++ b/tests/common/mmin.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -mt "@$((now - 121))" two_minutes_ago +"$XTOUCH" -mt "@$((now - 61))" one_minute_ago +"$XTOUCH" -mt "@$((now - 30))" thirty_seconds_ago +"$XTOUCH" -mt "@$((now + 60))" in_one_minute +"$XTOUCH" -mt "@$((now + 60 * 60))" in_one_hour + +bfs_diff . -mindepth 1 \ + \( -mmin -1 -exec printf -- '-mmin -1: %s\n' {} \; -o -true \) \ + \( -mmin 1 -exec printf -- '-mmin 1: %s\n' {} \; -o -true \) \ + \( -mmin +1 -exec printf -- '-mmin +1: %s\n' {} \; -o -true \) diff --git a/tests/common/name_slash.sh b/tests/common/name_slash.sh deleted file mode 100644 index 8d89623..0000000 --- a/tests/common/name_slash.sh +++ /dev/null @@ -1 +0,0 @@ -bfs_diff / -maxdepth 0 -name / diff --git a/tests/common/name_slashes.sh b/tests/common/name_slashes.sh deleted file mode 100644 index 78d0a84..0000000 --- a/tests/common/name_slashes.sh +++ /dev/null @@ -1 +0,0 @@ -bfs_diff /// -maxdepth 0 -name / diff --git a/tests/gnu/uid_minus.out b/tests/common/uid.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_minus.out +++ b/tests/common/uid.out diff --git a/tests/gnu/uid.sh b/tests/common/uid.sh index fb3cd93..fb3cd93 100644 --- a/tests/gnu/uid.sh +++ b/tests/common/uid.sh diff --git a/tests/common/uid_invalid_id.sh b/tests/common/uid_invalid_id.sh new file mode 100644 index 0000000..f5b952d --- /dev/null +++ b/tests/common/uid_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -uid 1eW6f5RM9Qi diff --git a/tests/common/uid_invalid_name.sh b/tests/common/uid_invalid_name.sh new file mode 100644 index 0000000..a2c359f --- /dev/null +++ b/tests/common/uid_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -uid eW6f5RM9Qi diff --git a/tests/gnu/uid_minus_plus.out b/tests/common/uid_minus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_minus_plus.out +++ b/tests/common/uid_minus.out diff --git a/tests/gnu/uid_minus.sh b/tests/common/uid_minus.sh index 6d371f2..6d371f2 100644 --- a/tests/gnu/uid_minus.sh +++ b/tests/common/uid_minus.sh diff --git a/tests/gnu/uid_plus.out b/tests/common/uid_minus_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_plus.out +++ b/tests/common/uid_minus_plus.out diff --git a/tests/gnu/uid_minus_plus.sh b/tests/common/uid_minus_plus.sh index e7a0496..e7a0496 100644 --- a/tests/gnu/uid_minus_plus.sh +++ b/tests/common/uid_minus_plus.sh diff --git a/tests/gnu/uid_plus_plus.out b/tests/common/uid_plus.out index a7ccfe4..a7ccfe4 100644 --- a/tests/gnu/uid_plus_plus.out +++ b/tests/common/uid_plus.out diff --git a/tests/gnu/uid_plus.sh b/tests/common/uid_plus.sh index 22b2c8e..22b2c8e 100644 --- a/tests/gnu/uid_plus.sh +++ b/tests/common/uid_plus.sh diff --git a/tests/common/uid_plus_plus.out b/tests/common/uid_plus_plus.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/common/uid_plus_plus.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/gnu/uid_plus_plus.sh b/tests/common/uid_plus_plus.sh index e021888..e021888 100644 --- a/tests/gnu/uid_plus_plus.sh +++ b/tests/common/uid_plus_plus.sh diff --git a/tests/getopts.sh b/tests/getopts.sh index 5214e9f..a16511f 100644 --- a/tests/getopts.sh +++ b/tests/getopts.sh @@ -5,11 +5,7 @@ ## Argument parsing -if command -v nproc &>/dev/null; then - JOBS=$(nproc) -else - JOBS=1 -fi +JOBS=$(_nproc) MAKE= PATTERNS=() SUDO=() @@ -23,7 +19,6 @@ VERBOSE_TESTS=0 # Print usage information usage() { - local pad=$(printf "%*s" ${#0} "") color cat <<EOF Usage: ${GRN}$0${RST} [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] diff --git a/tests/gnu/L_printf_types.out b/tests/gnu/L_printf_types.out new file mode 100644 index 0000000..734b15f --- /dev/null +++ b/tests/gnu/L_printf_types.out @@ -0,0 +1,17 @@ +(links) () d d +(links/broken) (nowhere) l N +(links/deeply) () d d +(links/deeply/nested) () d d +(links/deeply/nested/broken) (nowhere) l N +(links/deeply/nested/dir) () d d +(links/deeply/nested/file) () f f +(links/deeply/nested/link) () f f +(links/file) () f f +(links/hardlink) () f f +(links/notdir) (symlink/file) l N +(links/skip) () d d +(links/skip/broken) (nowhere) l N +(links/skip/dir) () d d +(links/skip/file) () f f +(links/skip/link) () f f +(links/symlink) () f f diff --git a/tests/gnu/L_printf_types.sh b/tests/gnu/L_printf_types.sh new file mode 100644 index 0000000..caa9083 --- /dev/null +++ b/tests/gnu/L_printf_types.sh @@ -0,0 +1 @@ +bfs_diff -L links -printf '(%p) (%l) %y %Y\n' diff --git a/tests/gnu/execdir_self.out b/tests/gnu/execdir_self.out new file mode 100644 index 0000000..3ad0640 --- /dev/null +++ b/tests/gnu/execdir_self.out @@ -0,0 +1 @@ +./bar.sh diff --git a/tests/gnu/execdir_self.sh b/tests/gnu/execdir_self.sh new file mode 100644 index 0000000..1fc5d04 --- /dev/null +++ b/tests/gnu/execdir_self.sh @@ -0,0 +1,9 @@ +cd "$TEST" +mkdir foo +cat >foo/bar.sh <<EOF +#!/bin/sh +printf '%s\n' "\$@" +EOF +chmod +x foo/bar.sh + +bfs_diff . -name bar.sh -execdir {} {} \; diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out index 49c1b21..e256554 100644 --- a/tests/gnu/executable.out +++ b/tests/gnu/executable.out @@ -1,4 +1,19 @@ perms -perms/rwx -perms/rx -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--x------ +perms/f--x--x--x +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-x------ +perms/fr-xr-xr-x +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh index 85eee8f..7b42772 100644 --- a/tests/gnu/files0_from_empty.sh +++ b/tests/gnu/files0_from_empty.sh @@ -1 +1 @@ -! printf "\0" | invoke_bfs -files0-from - +! printf '\0' | invoke_bfs -files0-from - diff --git a/tests/gnu/files0_from_file.out b/tests/gnu/files0_from_file.out index 1d87e6b..0f6b00d 100644 --- a/tests/gnu/files0_from_file.out +++ b/tests/gnu/files0_from_file.out @@ -1,3 +1,7 @@ + + + + /j /j @@ -16,6 +20,9 @@ ) )/g )/g +* +*/m +*/m , ,/f ,/f @@ -25,9 +32,14 @@ ... .../h .../h +/n +/n [ [/k [/k \ \/i \/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_file_file.out b/tests/gnu/files0_from_file_file.out new file mode 100644 index 0000000..fb683c7 --- /dev/null +++ b/tests/gnu/files0_from_file_file.out @@ -0,0 +1,2 @@ +basic/g +basic/g/h diff --git a/tests/gnu/files0_from_file_file.sh b/tests/gnu/files0_from_file_file.sh new file mode 100644 index 0000000..1119952 --- /dev/null +++ b/tests/gnu/files0_from_file_file.sh @@ -0,0 +1,3 @@ +printf 'basic/c\0' >"$TEST/in1" +printf 'basic/g\0' >"$TEST/in2" +bfs_diff -files0-from "$TEST/in1" -files0-from "$TEST/in2" diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh deleted file mode 100644 index 8e145ce..0000000 --- a/tests/gnu/files0_from_ok.sh +++ /dev/null @@ -1 +0,0 @@ -! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin.out b/tests/gnu/files0_from_stdin.out index 1d87e6b..0f6b00d 100644 --- a/tests/gnu/files0_from_stdin.out +++ b/tests/gnu/files0_from_stdin.out @@ -1,3 +1,7 @@ + + + + /j /j @@ -16,6 +20,9 @@ ) )/g )/g +* +*/m +*/m , ,/f ,/f @@ -25,9 +32,14 @@ ... .../h .../h +/n +/n [ [/k [/k \ \/i \/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_ok.sh b/tests/gnu/files0_from_stdin_ok.sh new file mode 100644 index 0000000..0283c8d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -files0-from - -ok echo {} \; diff --git a/tests/gnu/files0_from_stdin_ok_file.out b/tests/gnu/files0_from_stdin_ok_file.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_ok_file.sh b/tests/gnu/files0_from_stdin_ok_file.sh new file mode 100644 index 0000000..028df0c --- /dev/null +++ b/tests/gnu/files0_from_stdin_ok_file.sh @@ -0,0 +1,4 @@ +FILE="$TMP/$TEST.in" +cd weirdnames +invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0" +yes | bfs_diff -files0-from - -ok printf '%s\n' {} \; -files0-from "$FILE" diff --git a/tests/gnu/files0_from_stdin_stdin.out b/tests/gnu/files0_from_stdin_stdin.out new file mode 100644 index 0000000..0f6b00d --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.out @@ -0,0 +1,45 @@ + + + + + + /j + /j +! +!- +!-/e +!-/e +!/d +!/d +( +(- +(-/c +(-/c +(/b +(/b +) +)/g +)/g +* +*/m +*/m +, +,/f +,/f +- +-/a +-/a +... +.../h +.../h +/n +/n +[ +[/k +[/k +\ +\/i +\/i +{ +{/l +{/l diff --git a/tests/gnu/files0_from_stdin_stdin.sh b/tests/gnu/files0_from_stdin_stdin.sh new file mode 100644 index 0000000..8f6368f --- /dev/null +++ b/tests/gnu/files0_from_stdin_stdin.sh @@ -0,0 +1,2 @@ +cd weirdnames +invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -files0-from - diff --git a/tests/gnu/fls_overflow.sh b/tests/gnu/fls_overflow.sh new file mode 100644 index 0000000..067bc86 --- /dev/null +++ b/tests/gnu/fls_overflow.sh @@ -0,0 +1,4 @@ +# Regression test: times that overflow localtime() should still print +cd "$TEST" +"$XTOUCH" -t "@1111111111111111111" overflow || skip +invoke_bfs . -fls "$OUT" diff --git a/tests/gnu/follow_comma.out b/tests/gnu/follow_comma.out index 920b3d3..5e4b806 100644 --- a/tests/gnu/follow_comma.out +++ b/tests/gnu/follow_comma.out @@ -1,4 +1,7 @@ + . +./ +./ ./ ./ /j ./! @@ -11,6 +14,8 @@ ./(/b ./) ./)/g +./* +./*/m ./, ./,/f ./- @@ -21,3 +26,6 @@ ./[/k ./\ ./\/i +./{ +./{/l +/n diff --git a/tests/gnu/follow_files0_from.out b/tests/gnu/follow_files0_from.out new file mode 100644 index 0000000..c77d546 --- /dev/null +++ b/tests/gnu/follow_files0_from.out @@ -0,0 +1,42 @@ +links +links/broken +links/broken +links/deeply +links/deeply +links/deeply/nested +links/deeply/nested +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/file +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/deeply/nested/link +links/file +links/file +links/hardlink +links/hardlink +links/notdir +links/notdir +links/skip +links/skip +links/skip/broken +links/skip/broken +links/skip/dir +links/skip/dir +links/skip/file +links/skip/file +links/skip/link +links/skip/link +links/symlink +links/symlink diff --git a/tests/gnu/follow_files0_from.sh b/tests/gnu/follow_files0_from.sh new file mode 100644 index 0000000..8c20f6d --- /dev/null +++ b/tests/gnu/follow_files0_from.sh @@ -0,0 +1 @@ +invoke_bfs links -print0 | bfs_diff -follow -files0-from - diff --git a/tests/gnu/fprint_noerror.sh b/tests/gnu/fprint_unreached_error.sh index f13a62b..f13a62b 100644 --- a/tests/gnu/fprint_noerror.sh +++ b/tests/gnu/fprint_unreached_error.sh diff --git a/tests/gnu/fstype_btrfs_subvol.out b/tests/gnu/fstype_btrfs_subvol.out new file mode 100644 index 0000000..8871fb9 --- /dev/null +++ b/tests/gnu/fstype_btrfs_subvol.out @@ -0,0 +1,4 @@ +mnt +mnt/file +mnt/subvol +mnt/subvol/file diff --git a/tests/gnu/fstype_btrfs_subvol.sh b/tests/gnu/fstype_btrfs_subvol.sh new file mode 100644 index 0000000..71df45c --- /dev/null +++ b/tests/gnu/fstype_btrfs_subvol.sh @@ -0,0 +1,25 @@ +# Test that -fstype works in btrfs subvolumes + +command -v btrfs &>/dev/null || skip + +cd "$TEST" + +# Make a btrfs filesystem image +truncate -s128M img +mkfs.btrfs img >&2 + +# Mount it +mkdir mnt +bfs_sudo mount img mnt || skip +defer bfs_sudo umount mnt + +# Make it owned by us +bfs_sudo chown "$(id -u):$(id -g)" mnt + +# Create a subvolume inside it +btrfs subvolume create mnt/subvol >&2 + +# Make a file in and outside the subvolume +"$XTOUCH" mnt/file mnt/subvol/file + +bfs_diff mnt -fstype btrfs -print -o -printf '%p %F\n' diff --git a/tests/gnu/ignore_readdir_race_loop.out b/tests/gnu/ignore_readdir_race_loop.out new file mode 100644 index 0000000..a514555 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_loop.out @@ -0,0 +1,11 @@ +loops +loops/broken +loops/deeply +loops/deeply/nested +loops/deeply/nested/dir +loops/file +loops/notdir +loops/skip +loops/skip/dir +loops/skip/loop +loops/symlink diff --git a/tests/gnu/ignore_readdir_race_loop.sh b/tests/gnu/ignore_readdir_race_loop.sh new file mode 100644 index 0000000..3329169 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_loop.sh @@ -0,0 +1,2 @@ +# Make sure -ignore_readdir_race doesn't suppress ELOOP from an actual filesystem loop +! bfs_diff -L loops -ignore_readdir_race diff --git a/tests/gnu/ignore_readdir_race_rmdir.out b/tests/gnu/ignore_readdir_race_rmdir.out new file mode 100644 index 0000000..ede8749 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_rmdir.out @@ -0,0 +1,2 @@ +./bar +./foo diff --git a/tests/gnu/ignore_readdir_race_rmdir.sh b/tests/gnu/ignore_readdir_race_rmdir.sh new file mode 100644 index 0000000..87f36a9 --- /dev/null +++ b/tests/gnu/ignore_readdir_race_rmdir.sh @@ -0,0 +1,5 @@ +cd "$TEST" +"$XTOUCH" -p foo/ bar/ + +# Check that -ignore_readdir_race suppresses errors from opendir() +bfs_diff . -ignore_readdir_race -mindepth 1 -print -name foo -exec rmdir {} \; diff --git a/tests/gnu/not_comma.out b/tests/gnu/not_comma.out new file mode 100644 index 0000000..b90468e --- /dev/null +++ b/tests/gnu/not_comma.out @@ -0,0 +1,34 @@ +basic +basic +basic/a +basic/a +basic/b +basic/b +basic/c +basic/c +basic/c/d +basic/c/d +basic/e +basic/e +basic/e/f +basic/g +basic/g +basic/g/h +basic/g/h +basic/i +basic/i +basic/j +basic/j +basic/j/foo +basic/k +basic/k +basic/k/foo +basic/k/foo/bar +basic/k/foo/bar +basic/l +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar +basic/l/foo/bar/baz +basic/l/foo/bar/baz diff --git a/tests/gnu/not_comma.sh b/tests/gnu/not_comma.sh new file mode 100644 index 0000000..04c0195 --- /dev/null +++ b/tests/gnu/not_comma.sh @@ -0,0 +1,2 @@ +# Regression test: assertion failure in sink_not_comma() +bfs_diff basic -not \( -print , -name '*f*' \) -print diff --git a/tests/gnu/ok_files0_from_stdin.sh b/tests/gnu/ok_files0_from_stdin.sh new file mode 100644 index 0000000..2c4de7b --- /dev/null +++ b/tests/gnu/ok_files0_from_stdin.sh @@ -0,0 +1 @@ +! printf 'basic\0' | invoke_bfs -ok echo {} \; -files0-from - diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh index 87c7298..a5dc0d0 100644 --- a/tests/gnu/ok_flush.sh +++ b/tests/gnu/ok_flush.sh @@ -1,4 +1,4 @@ # I/O streams should be flushed before -ok prompts -yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT" +yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT" sort_output diff_output diff --git a/tests/gnu/perm_000_slash.out b/tests/gnu/perm_000_slash.out index d7494b8..e279684 100644 --- a/tests/gnu/perm_000_slash.out +++ b/tests/gnu/perm_000_slash.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out index 9a5b95a..1b6d885 100644 --- a/tests/gnu/perm_222_slash.out +++ b/tests/gnu/perm_222_slash.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out index 7e5ae98..eef88ca 100644 --- a/tests/gnu/perm_644_slash.out +++ b/tests/gnu/perm_644_slash.out @@ -1,7 +1,26 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out index 7e5ae98..fcbf49e 100644 --- a/tests/gnu/perm_leading_plus_symbolic_slash.out +++ b/tests/gnu/perm_leading_plus_symbolic_slash.out @@ -1,7 +1,28 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out index 7e5ae98..5a21321 100644 --- a/tests/gnu/perm_symbolic_slash.out +++ b/tests/gnu/perm_symbolic_slash.out @@ -1,7 +1,24 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr--r--r-- +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out index 386feba..56d1f52 100644 --- a/tests/gnu/readable.out +++ b/tests/gnu/readable.out @@ -1,5 +1,19 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/regextype_awk.out b/tests/gnu/regextype_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_awk.sh b/tests/gnu/regextype_awk.sh new file mode 100644 index 0000000..3718473 --- /dev/null +++ b/tests/gnu/regextype_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype awk -quit || skip + +bfs_diff weirdnames -regextype awk -regex '.*/[\[\*]/.*' diff --git a/tests/gnu/regextype_egrep.out b/tests/gnu/regextype_egrep.out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/gnu/regextype_egrep.out diff --git a/tests/gnu/regextype_egrep.sh b/tests/gnu/regextype_egrep.sh new file mode 100644 index 0000000..281d9c0 --- /dev/null +++ b/tests/gnu/regextype_egrep.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype egrep -quit || skip + +bfs_diff weirdnames -regextype egrep -regex '*.*/{l' diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh index 3cc388c..164d17a 100644 --- a/tests/gnu/regextype_emacs.sh +++ b/tests/gnu/regextype_emacs.sh @@ -1,3 +1,3 @@ invoke_bfs -regextype emacs -quit || skip -bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)' +bfs_diff basic -regextype emacs -regex '.*/\(?:f+o?o?\|bar\)' diff --git a/tests/gnu/regextype_findutils_default.out b/tests/gnu/regextype_findutils_default.out new file mode 100644 index 0000000..709a7ba --- /dev/null +++ b/tests/gnu/regextype_findutils_default.out @@ -0,0 +1,3 @@ +/n +weirdnames/ +weirdnames/*/m diff --git a/tests/gnu/regextype_findutils_default.sh b/tests/gnu/regextype_findutils_default.sh new file mode 100644 index 0000000..c870312 --- /dev/null +++ b/tests/gnu/regextype_findutils_default.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype findutils-default -quit || skip + +bfs_diff weirdnames -regextype findutils-default -regex '.*/./\(m\|n\)' diff --git a/tests/gnu/regextype_gnu_awk.out b/tests/gnu/regextype_gnu_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_gnu_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_gnu_awk.sh b/tests/gnu/regextype_gnu_awk.sh new file mode 100644 index 0000000..6b66496 --- /dev/null +++ b/tests/gnu/regextype_gnu_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype gnu-awk -quit || skip + +bfs_diff weirdnames -regextype gnu-awk -regex '.*/[\[\*]/(\<.\>)' diff --git a/tests/gnu/regextype_posix_awk.out b/tests/gnu/regextype_posix_awk.out new file mode 100644 index 0000000..0f32fc4 --- /dev/null +++ b/tests/gnu/regextype_posix_awk.out @@ -0,0 +1,2 @@ +weirdnames/*/m +weirdnames/[/k diff --git a/tests/gnu/regextype_posix_awk.sh b/tests/gnu/regextype_posix_awk.sh new file mode 100644 index 0000000..86377d7 --- /dev/null +++ b/tests/gnu/regextype_posix_awk.sh @@ -0,0 +1,3 @@ +invoke_bfs -regextype posix-awk -quit || skip + +bfs_diff weirdnames -regextype posix-awk -regex '.*/[\[\*]/.*' diff --git a/tests/gnu/regextype_posix_minimal_basic.out b/tests/gnu/regextype_posix_minimal_basic.out new file mode 100644 index 0000000..0f0971e --- /dev/null +++ b/tests/gnu/regextype_posix_minimal_basic.out @@ -0,0 +1 @@ +./( diff --git a/tests/gnu/regextype_posix_minimal_basic.sh b/tests/gnu/regextype_posix_minimal_basic.sh new file mode 100644 index 0000000..ee324f3 --- /dev/null +++ b/tests/gnu/regextype_posix_minimal_basic.sh @@ -0,0 +1,2 @@ +cd weirdnames +bfs_diff -regextype posix-minimal-basic -regex '\./\((\)' diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh index 5e5d4e9..fe0a778 100644 --- a/tests/gnu/used.sh +++ b/tests/gnu/used.sh @@ -1,37 +1,18 @@ -iso8601() { - printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@" -} - cd "$TEST" -now=$(date '+%Y-%m-%dT%H:%M:%S') - -# Parse the current date -[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail -# Treat leading zeros as decimal, not octal -YYYY=$((10#${BASH_REMATCH[1]})) -MM=$((10#${BASH_REMATCH[2]})) -DD=$((10#${BASH_REMATCH[3]})) -hh=$((10#${BASH_REMATCH[4]})) -mm=$((10#${BASH_REMATCH[5]})) -ss=$((10#${BASH_REMATCH[6]})) +now=$(epoch_time) # -used is always false if atime < ctime -yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss) -"$XTOUCH" -at "$yesterday" yesterday +"$XTOUCH" -at "@$((now - 60 * 60 * 24))" yesterday # -used rounds up -tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$tomorrow" tomorrow +"$XTOUCH" -at "@$((now + 60 * 60))" tomorrow -dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$dayafter" dayafter +"$XTOUCH" -at "@$((now + 60 * 60 * 25))" dayafter -nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss) -"$XTOUCH" -at "$nextweek" nextweek +"$XTOUCH" -at "@$((now + 60 * 60 * (24 * 6 + 1)))" nextweek -nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss) -"$XTOUCH" -at "$nextyear" nextyear +"$XTOUCH" -at "@$((now + 60 * 60 * 24 * 365))" nextyear bfs_diff -mindepth 1 \ -a -used 1 -printf '-used 1: %p\n' \ diff --git a/tests/gnu/writable.out b/tests/gnu/writable.out index 9a5b95a..1b6d885 100644 --- a/tests/gnu/writable.out +++ b/tests/gnu/writable.out @@ -1,5 +1,20 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/gnu/xtype_l_loops.out b/tests/gnu/xtype_l_loops.out new file mode 100644 index 0000000..fdaccab --- /dev/null +++ b/tests/gnu/xtype_l_loops.out @@ -0,0 +1,3 @@ +loops/broken +loops/loop +loops/notdir diff --git a/tests/gnu/xtype_l_loops.sh b/tests/gnu/xtype_l_loops.sh new file mode 100644 index 0000000..6893134 --- /dev/null +++ b/tests/gnu/xtype_l_loops.sh @@ -0,0 +1 @@ +bfs_diff loops -xtype l diff --git a/tests/ioq.c b/tests/ioq.c index 99c98a2..1a0da97 100644 --- a/tests/ioq.c +++ b/tests/ioq.c @@ -1,13 +1,12 @@ // 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 "ioq.h" + #include <fcntl.h> #include <stdlib.h> @@ -50,6 +49,7 @@ static void check_ioq_push_block(void) { int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL); bfs_everify(ret == 0, "ioq_opendir()"); } + ioq_submit(ioq); bfs_verify(ioq_capacity(ioq) == 0); // Now cancel the queue, pushing an additional IOQ_STOP message @@ -71,7 +71,6 @@ static void check_ioq_push_block(void) { ioq_destroy(ioq); } -bool check_ioq(void) { +void check_ioq(void) { check_ioq_push_block(); - return true; } diff --git a/tests/list.c b/tests/list.c new file mode 100644 index 0000000..5d0403f --- /dev/null +++ b/tests/list.c @@ -0,0 +1,99 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "tests.h" + +#include "bfs.h" +#include "diag.h" +#include "list.h" + +#include <stddef.h> + +struct item { + int n; + struct item *next; +}; + +struct list { + struct item *head; + struct item **tail; +}; + +static bool check_list_items(struct list *list, int *array, size_t size) { + struct item **cur = &list->head; + for (size_t i = 0; i < size; ++i) { + if (!bfs_check(*cur != NULL)) { + return false; + } + int n = (*cur)->n; + if (!bfs_check(n == array[i], "%d != %d", n, array[i])) { + return false; + } + cur = &(*cur)->next; + } + + if (!bfs_check(*cur == NULL)) { + return false; + } + if (!bfs_check(list->tail == cur)) { + return false; + } + + return true; +} + +#define ARRAY(...) (int[]){ __VA_ARGS__ }, countof((int[]){ __VA_ARGS__ }) +#define EMPTY() NULL, 0 + +void check_list(void) { + struct list l1; + SLIST_INIT(&l1); + bfs_verify(check_list_items(&l1, EMPTY())); + + struct list l2; + SLIST_INIT(&l2); + bfs_verify(check_list_items(&l2, EMPTY())); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, EMPTY())); + + struct item i10 = { .n = 10 }; + SLIST_APPEND(&l1, &i10); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + SLIST_SPLICE(&l1, &l1.head, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10))); + + struct item i20 = { .n = 20 }; + SLIST_PREPEND(&l2, &i20); + bfs_verify(check_list_items(&l2, ARRAY(20))); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 20))); + bfs_verify(check_list_items(&l2, EMPTY())); + + struct item i15 = { .n = 15 }; + SLIST_APPEND(&l2, &i15); + SLIST_SPLICE(&l1, &i10.next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + bfs_verify(check_list_items(&l2, EMPTY())); + + SLIST_EXTEND(&l1, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + SLIST_SPLICE(&l1, &i10.next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + SLIST_SPLICE(&l1, &l1.head, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20))); + + struct item i11 = { .n = 11 }; + struct item i12 = { .n = 12 }; + SLIST_APPEND(&l2, &i11); + SLIST_APPEND(&l2, &i12); + SLIST_SPLICE(&l1, &l1.head->next, &l2); + bfs_verify(check_list_items(&l1, ARRAY(10, 11, 12, 15, 20))); +} diff --git a/tests/main.c b/tests/main.c index aef0583..9240e1c 100644 --- a/tests/main.c +++ b/tests/main.c @@ -5,18 +5,45 @@ * Entry point for unit tests. */ -#include "prelude.h" #include "tests.h" + +#include "alloc.h" #include "bfstd.h" #include "color.h" +#include "list.h" + #include <locale.h> #include <stdio.h> #include <stdlib.h> +#include <stdint.h> #include <string.h> +#include <sys/wait.h> #include <time.h> +#include <unistd.h> + +/** Result of the current test. */ +static bool pass; + +bool bfs_check_impl(bool result) { + pass &= result; + return result; +} + +/** + * A running test. + */ +struct test_proc { + /** Linked list links. */ + struct test_proc *prev, *next; + + /** The PID of this test. */ + pid_t pid; + /** The name of this test. */ + const char *name; +}; /** - * Test context. + * Global test context. */ struct test_ctx { /** Number of command line arguments. */ @@ -24,6 +51,17 @@ struct test_ctx { /** The arguments themselves. */ char **argv; + /** Maximum jobs (-j). */ + int jobs; + /** Current jobs. */ + int running; + /** Completed jobs. */ + int done; + /** List of running tests. */ + struct { + struct test_proc *head, *tail; + } procs; + /** Parsed colors. */ struct colors *colors; /** Colorized output stream. */ @@ -34,10 +72,15 @@ struct test_ctx { }; /** Initialize the test context. */ -static int test_init(struct test_ctx *ctx, int argc, char **argv) { +static int test_init(struct test_ctx *ctx, int jobs, int argc, char **argv) { ctx->argc = argc; ctx->argv = argv; + ctx->jobs = jobs; + ctx->running = 0; + ctx->done = 0; + LIST_INIT(&ctx->procs); + ctx->colors = parse_colors(); ctx->cout = cfwrap(stdout, ctx->colors, false); if (!ctx->cout) { @@ -49,26 +92,15 @@ static int test_init(struct test_ctx *ctx, int argc, char **argv) { return 0; } -/** Finalize the test context. */ -static int test_fini(struct test_ctx *ctx) { - if (ctx->cout) { - cfclose(ctx->cout); - } - - free_colors(ctx->colors); - - return ctx->ret; -} - /** Check if a test case is enabled for this run. */ static bool should_run(const struct test_ctx *ctx, const char *test) { // Run all tests by default - if (ctx->argc < 2) { + if (ctx->argc == 0) { return true; } // With args, run only specified tests - for (int i = 1; i < ctx->argc; ++i) { + for (int i = 0; i < ctx->argc; ++i) { if (strcmp(test, ctx->argv[i]) == 0) { return true; } @@ -77,16 +109,104 @@ static bool should_run(const struct test_ctx *ctx, const char *test) { return false; } -/** Run a test if it's enabled. */ -static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) { - if (should_run(ctx, test)) { - if (fn()) { - cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test); +/** Wait for a test to finish. */ +static void wait_test(struct test_ctx *ctx) { + int wstatus; + pid_t pid = xwaitpid(0, &wstatus, 0); + bfs_everify(pid > 0, "xwaitpid()"); + + struct test_proc *proc = NULL; + for_list (struct test_proc, i, &ctx->procs) { + if (i->pid == pid) { + proc = i; + break; + } + } + + bfs_verify(proc, "No test_proc for PID %ju", (intmax_t)pid); + + bool passed = false; + + if (WIFEXITED(wstatus)) { + int status = WEXITSTATUS(wstatus); + if (status == EXIT_SUCCESS) { + cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", proc->name); + passed = true; + } else if (status == EXIT_FAILURE) { + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", proc->name); } else { - cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test); - ctx->ret = EXIT_FAILURE; + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (Exit %d)\n", proc->name, status); + } + } else { + const char *str = NULL; + if (WIFSIGNALED(wstatus)) { + str = strsignal(WTERMSIG(wstatus)); } + if (!str) { + str = "Unknown"; + } + cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (%s)\n", proc->name, str); + } + + if (!passed) { + ctx->ret = EXIT_FAILURE; } + + --ctx->running; + ++ctx->done; + LIST_REMOVE(&ctx->procs, proc); + free(proc); +} + +/** Unit test function type. */ +typedef void test_fn(void); + +/** Run a test if it's enabled. */ +static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) { + if (!should_run(ctx, test)) { + return; + } + + while (ctx->running >= ctx->jobs) { + wait_test(ctx); + } + + struct test_proc *proc = ALLOC(struct test_proc); + bfs_everify(proc, "alloc()"); + + LIST_ITEM_INIT(proc); + proc->name = test; + + fflush(NULL); + proc->pid = fork(); + bfs_everify(proc->pid >= 0, "fork()"); + + if (proc->pid > 0) { + // Parent + ++ctx->running; + LIST_APPEND(&ctx->procs, proc); + return; + } + + // Child + pass = true; + fn(); + exit(pass ? EXIT_SUCCESS : EXIT_FAILURE); +} + +/** Finalize the test context. */ +static int test_fini(struct test_ctx *ctx) { + while (ctx->running > 0) { + wait_test(ctx); + } + + if (ctx->cout) { + cfclose(ctx->cout); + } + + free_colors(ctx->colors); + + return ctx->ret; } int main(int argc, char *argv[]) { @@ -102,8 +222,37 @@ int main(int argc, char *argv[]) { } tzset(); + unsigned int jobs = 0; + + const char *cmd = argc > 0 ? argv[0] : "units"; + int c; + while (c = getopt(argc, argv, ":j:"), c != -1) { + switch (c) { + case 'j': + if (xstrtoui(optarg, NULL, 10, &jobs) != 0) { + fprintf(stderr, "%s: Bad job count '%s': %s\n", cmd, optarg, errstr()); + return EXIT_FAILURE; + } + break; + case ':': + fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt); + return EXIT_FAILURE; + case '?': + fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt); + return EXIT_FAILURE; + } + } + + if (!jobs) { + jobs = nproc(); + } + + if (optind > argc) { + optind = argc; + } + struct test_ctx ctx; - if (test_init(&ctx, argc, argv) != 0) { + if (test_init(&ctx, jobs, argc - optind, argv + optind) != 0) { goto done; } @@ -111,6 +260,7 @@ int main(int argc, char *argv[]) { run_test(&ctx, "bfstd", check_bfstd); run_test(&ctx, "bit", check_bit); run_test(&ctx, "ioq", check_ioq); + run_test(&ctx, "list", check_list); run_test(&ctx, "sighook", check_sighook); run_test(&ctx, "trie", check_trie); run_test(&ctx, "xspawn", check_xspawn); diff --git a/tests/mksock.c b/tests/mksock.c index 5786cb6..f46df96 100644 --- a/tests/mksock.c +++ b/tests/mksock.c @@ -7,6 +7,7 @@ */ #include "bfstd.h" + #include <errno.h> #include <stdio.h> #include <stdlib.h> diff --git a/tests/posix/HL.out b/tests/posix/HL.out new file mode 100644 index 0000000..ec9e861 --- /dev/null +++ b/tests/posix/HL.out @@ -0,0 +1,17 @@ +links +links/broken +links/deeply +links/deeply/nested +links/deeply/nested/broken +links/deeply/nested/dir +links/deeply/nested/file +links/deeply/nested/link +links/file +links/hardlink +links/notdir +links/skip +links/skip/broken +links/skip/dir +links/skip/file +links/skip/link +links/symlink diff --git a/tests/posix/HL.sh b/tests/posix/HL.sh new file mode 100644 index 0000000..1858982 --- /dev/null +++ b/tests/posix/HL.sh @@ -0,0 +1 @@ +bfs_diff -HL links diff --git a/tests/posix/LH.out b/tests/posix/LH.out new file mode 100644 index 0000000..ff635ff --- /dev/null +++ b/tests/posix/LH.out @@ -0,0 +1 @@ +links/deeply/nested/dir diff --git a/tests/posix/LH.sh b/tests/posix/LH.sh new file mode 100644 index 0000000..ef1d980 --- /dev/null +++ b/tests/posix/LH.sh @@ -0,0 +1 @@ +bfs_diff -LH links/deeply/nested/dir diff --git a/tests/posix/L_mount.out b/tests/posix/L_mount.out new file mode 100644 index 0000000..7ed5f0d --- /dev/null +++ b/tests/posix/L_mount.out @@ -0,0 +1,2 @@ +. +./foo diff --git a/tests/common/L_mount.sh b/tests/posix/L_mount.sh index fd8042a..fd8042a 100644 --- a/tests/common/L_mount.sh +++ b/tests/posix/L_mount.sh diff --git a/tests/posix/atime.out b/tests/posix/atime.out new file mode 100644 index 0000000..5ed206b --- /dev/null +++ b/tests/posix/atime.out @@ -0,0 +1,6 @@ +-atime 1: ./yesterday +-atime +1: ./last_week +-atime +1: ./two_days_ago +-atime -1: ./now +-atime -1: ./one_hour_ago +-atime -1: ./tomorrow diff --git a/tests/posix/atime.sh b/tests/posix/atime.sh new file mode 100644 index 0000000..25dfd7e --- /dev/null +++ b/tests/posix/atime.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -at "@$((now - 60 * 60 * 24 * 7))" last_week +"$XTOUCH" -at "@$((now - 60 * 60 * 49))" two_days_ago +"$XTOUCH" -at "@$((now - 60 * 60 * 25))" yesterday +"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -at "@$((now))" now +"$XTOUCH" -at "@$((now + 60 * 60 * 24))" tomorrow + +bfs_diff . \! -name . \ + \( -atime -1 -exec printf -- '-atime -1: %s\n' {} \; -o -prune \) \ + \( -atime 1 -exec printf -- '-atime 1: %s\n' {} \; -o -prune \) \ + \( -atime +1 -exec printf -- '-atime +1: %s\n' {} \; -o -prune \) diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out index 7ed5f0d..c4f8ce4 100644 --- a/tests/posix/depth_error.out +++ b/tests/posix/depth_error.out @@ -1,2 +1,4 @@ -. -./foo +inaccessible +inaccessible/dir +inaccessible/file +inaccessible/link diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh index db414ba..9b29385 100644 --- a/tests/posix/depth_error.sh +++ b/tests/posix/depth_error.sh @@ -1,7 +1 @@ -cd "$TEST" -"$XTOUCH" -p foo/bar - -chmod a-r foo -defer chmod +r foo - -! bfs_diff . -depth +! bfs_diff inaccessible -depth diff --git a/tests/posix/exec_return.out b/tests/posix/exec_return.out new file mode 100644 index 0000000..600c93a --- /dev/null +++ b/tests/posix/exec_return.out @@ -0,0 +1,18 @@ +basic +basic/a +basic/b +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/exec_return.sh b/tests/posix/exec_return.sh new file mode 100644 index 0000000..cfa0f5d --- /dev/null +++ b/tests/posix/exec_return.sh @@ -0,0 +1 @@ +bfs_diff basic -exec test {} = basic/c \; -o -print diff --git a/tests/posix/exec_sigmask.out b/tests/posix/exec_sigmask.out new file mode 100644 index 0000000..bb646f3 --- /dev/null +++ b/tests/posix/exec_sigmask.out @@ -0,0 +1 @@ +SigBlk: 0000000000000000 diff --git a/tests/posix/exec_sigmask.sh b/tests/posix/exec_sigmask.sh new file mode 100644 index 0000000..2907458 --- /dev/null +++ b/tests/posix/exec_sigmask.sh @@ -0,0 +1,16 @@ +# Regression test: restore the signal mask after fork() + +cd "$TEST" +mkfifo p1 p2 + +{ + # Get the PID of `sh` + read -r pid <p1 + # Send SIGTERM -- this will hang forever if signals are blocked + kill $pid +} & + +# Write the `sh` PID to p1, then hang reading p2 until we're killed +! invoke_bfs p1 -exec bash -c 'echo $$ >p1 && read -r _ <p2' bash {} + || fail + +_wait diff --git a/tests/posix/exec_substring_plus.out b/tests/posix/exec_substring_plus.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/exec_substring_plus.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/exec_substring_plus.sh b/tests/posix/exec_substring_plus.sh new file mode 100644 index 0000000..90309b0 --- /dev/null +++ b/tests/posix/exec_substring_plus.sh @@ -0,0 +1,14 @@ +# https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html +# +# Only a <plus-sign> that immediately follows an argument containing only +# the two characters "{}" shall punctuate the end of the primary expression. +# Other uses of the <plus-sign> shall not be treated as special. +# ... +# If a utility_name or argument string contains the two characters "{}", but +# not just the two characters "{}", it is implementation-defined whether +# find replaces those two characters or uses the string without change. + +invoke_bfs basic -exec printf '%s %s %s %s\n' {} {}+ +{} + \; | sed 's/ .*//' >"$OUT" +sort_output +diff_output + diff --git a/tests/posix/group_invalid_id.sh b/tests/posix/group_invalid_id.sh new file mode 100644 index 0000000..1a89747 --- /dev/null +++ b/tests/posix/group_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -group 1eW6f5RM9Qi diff --git a/tests/posix/group_invalid_name.sh b/tests/posix/group_invalid_name.sh new file mode 100644 index 0000000..a08dc72 --- /dev/null +++ b/tests/posix/group_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -group eW6f5RM9Qi diff --git a/tests/posix/group_o_group.out b/tests/posix/group_o_group.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/group_o_group.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/group_o_group.sh b/tests/posix/group_o_group.sh new file mode 100644 index 0000000..60aefc0 --- /dev/null +++ b/tests/posix/group_o_group.sh @@ -0,0 +1,3 @@ +# Regression test for +# https://github.com/tavianator/bfs/issues/155 +bfs_diff basic -group 0 -o -group "$(id -g)" diff --git a/tests/common/iname.out b/tests/posix/iname.out index a9e5d42..a9e5d42 100644 --- a/tests/common/iname.out +++ b/tests/posix/iname.out diff --git a/tests/posix/iname.sh b/tests/posix/iname.sh new file mode 100644 index 0000000..a9297ac --- /dev/null +++ b/tests/posix/iname.sh @@ -0,0 +1 @@ +bfs_diff basic -iname '*F*' diff --git a/tests/common/mount.out b/tests/posix/mount.out index 6253434..b0ad937 100644 --- a/tests/common/mount.out +++ b/tests/posix/mount.out @@ -1,4 +1,3 @@ . ./foo ./foo/bar -./mnt diff --git a/tests/common/mount.sh b/tests/posix/mount.sh index c9abde5..c9abde5 100644 --- a/tests/common/mount.sh +++ b/tests/posix/mount.sh diff --git a/tests/posix/mtime.out b/tests/posix/mtime.out new file mode 100644 index 0000000..91f0114 --- /dev/null +++ b/tests/posix/mtime.out @@ -0,0 +1,6 @@ +-mtime 1: ./yesterday +-mtime +1: ./last_week +-mtime +1: ./two_days_ago +-mtime -1: ./now +-mtime -1: ./one_hour_ago +-mtime -1: ./tomorrow diff --git a/tests/posix/mtime.sh b/tests/posix/mtime.sh new file mode 100644 index 0000000..8367631 --- /dev/null +++ b/tests/posix/mtime.sh @@ -0,0 +1,15 @@ +cd "$TEST" + +now=$(epoch_time) + +"$XTOUCH" -mt "@$((now - 60 * 60 * 24 * 7))" last_week +"$XTOUCH" -mt "@$((now - 60 * 60 * 49))" two_days_ago +"$XTOUCH" -mt "@$((now - 60 * 60 * 25))" yesterday +"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago +"$XTOUCH" -mt "@$((now))" now +"$XTOUCH" -mt "@$((now + 60 * 60 * 24))" tomorrow + +bfs_diff . \! -name . \ + \( -mtime -1 -exec printf -- '-mtime -1: %s\n' {} \; -o -prune \) \ + \( -mtime 1 -exec printf -- '-mtime 1: %s\n' {} \; -o -prune \) \ + \( -mtime +1 -exec printf -- '-mtime +1: %s\n' {} \; -o -prune \) diff --git a/tests/common/name_slash.out b/tests/posix/name_slash.out index b498fd4..b498fd4 100644 --- a/tests/common/name_slash.out +++ b/tests/posix/name_slash.out diff --git a/tests/posix/name_slash.sh b/tests/posix/name_slash.sh new file mode 100644 index 0000000..b42b145 --- /dev/null +++ b/tests/posix/name_slash.sh @@ -0,0 +1 @@ +bfs_diff / -prune -name / diff --git a/tests/common/name_slashes.out b/tests/posix/name_slashes.out index 187b81f..187b81f 100644 --- a/tests/common/name_slashes.out +++ b/tests/posix/name_slashes.out diff --git a/tests/posix/name_slashes.sh b/tests/posix/name_slashes.sh new file mode 100644 index 0000000..45a39d3 --- /dev/null +++ b/tests/posix/name_slashes.sh @@ -0,0 +1 @@ +bfs_diff /// -prune -name / diff --git a/tests/posix/perm_000.out b/tests/posix/perm_000.out index 5fd30bc..9df7f46 100644 --- a/tests/posix/perm_000.out +++ b/tests/posix/perm_000.out @@ -1 +1 @@ -perms/0 +perms/f--------- diff --git a/tests/posix/perm_000_minus.out b/tests/posix/perm_000_minus.out index d7494b8..e279684 100644 --- a/tests/posix/perm_000_minus.out +++ b/tests/posix/perm_000_minus.out @@ -1,8 +1,29 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x------ +perms/dr-xr-xr-x +perms/drwx------ +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f--------- +perms/f--x------ +perms/f--x--x--x +perms/f-w------- +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx------ +perms/f-wx--x--x +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/fr-------- +perms/fr--r--r-- +perms/fr-x------ +perms/fr-xr-xr-x +perms/frw------- +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr----- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out index 1690e43..bdc5590 100644 --- a/tests/posix/perm_222.out +++ b/tests/posix/perm_222.out @@ -1 +1 @@ -perms/w +perms/f-w--w--w- diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out index 1690e43..342b285 100644 --- a/tests/posix/perm_222_minus.out +++ b/tests/posix/perm_222_minus.out @@ -1 +1,5 @@ -perms/w +perms/drwxrwxrwx +perms/f-w--w--w- +perms/f-wx-wx-wx +perms/frw-rw-rw- +perms/frwxrwxrwx diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out index 4e64e49..9f77ce6 100644 --- a/tests/posix/perm_644.out +++ b/tests/posix/perm_644.out @@ -1 +1 @@ -perms/rw +perms/frw-r--r-- diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out index 2e2576b..84f69f5 100644 --- a/tests/posix/perm_644_minus.out +++ b/tests/posix/perm_644_minus.out @@ -1,3 +1,10 @@ perms -perms/rw -perms/rwx +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_symbolic_minus.out b/tests/posix/perm_leading_plus_symbolic_minus.out index e69de29..38d0e1c 100644 --- a/tests/posix/perm_leading_plus_symbolic_minus.out +++ b/tests/posix/perm_leading_plus_symbolic_minus.out @@ -0,0 +1,7 @@ +perms +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_umask.out b/tests/posix/perm_leading_plus_umask.out new file mode 100644 index 0000000..6ed4b7f --- /dev/null +++ b/tests/posix/perm_leading_plus_umask.out @@ -0,0 +1,10 @@ +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/f-w--w---- +perms/f-w--w--w- +perms/f-wx-wx--x +perms/f-wx-wx-wx +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/perm_leading_plus_umask.sh b/tests/posix/perm_leading_plus_umask.sh new file mode 100644 index 0000000..948b4ad --- /dev/null +++ b/tests/posix/perm_leading_plus_umask.sh @@ -0,0 +1,3 @@ +# Test for https://www.austingroupbugs.net/view.php?id=1392 +umask 002 +bfs_diff perms -perm -+w diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out index 2e2576b..84f69f5 100644 --- a/tests/posix/perm_symbolic_minus.out +++ b/tests/posix/perm_symbolic_minus.out @@ -1,3 +1,10 @@ perms -perms/rw -perms/rwx +perms/drwxr-xr-x +perms/drwxrwxr-x +perms/drwxrwxrwx +perms/frw-r--r-- +perms/frw-rw-r-- +perms/frw-rw-rw- +perms/frwxr-xr-x +perms/frwxrwxr-x +perms/frwxrwxrwx diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out index 4e64e49..9f77ce6 100644 --- a/tests/posix/permcopy.out +++ b/tests/posix/permcopy.out @@ -1 +1 @@ -perms/rw +perms/frw-r--r-- diff --git a/tests/gnu/print0.out b/tests/posix/print0.out Binary files differindex 1347444..1347444 100644 --- a/tests/gnu/print0.out +++ b/tests/posix/print0.out diff --git a/tests/gnu/print0.sh b/tests/posix/print0.sh index b916172..b916172 100644 --- a/tests/gnu/print0.sh +++ b/tests/posix/print0.sh diff --git a/tests/posix/prune_error.out b/tests/posix/prune_error.out new file mode 100644 index 0000000..436c48e --- /dev/null +++ b/tests/posix/prune_error.out @@ -0,0 +1 @@ +inaccessible diff --git a/tests/posix/prune_error.sh b/tests/posix/prune_error.sh new file mode 100644 index 0000000..07a2523 --- /dev/null +++ b/tests/posix/prune_error.sh @@ -0,0 +1 @@ +! bfs_diff -L inaccessible -path '*/*' -prune -o -print diff --git a/tests/posix/root_order.out b/tests/posix/root_order.out new file mode 100644 index 0000000..ea94276 --- /dev/null +++ b/tests/posix/root_order.out @@ -0,0 +1,4 @@ +basic/a +basic/b +basic/c/d +basic/e/f diff --git a/tests/posix/root_order.sh b/tests/posix/root_order.sh new file mode 100644 index 0000000..86adf20 --- /dev/null +++ b/tests/posix/root_order.sh @@ -0,0 +1,6 @@ +# Root paths must be processed in order +# https://www.austingroupbugs.net/view.php?id=1859 + +# -size forces a stat(), which we don't want to be async +invoke_bfs basic/{a,b,c/d,e/f} -size -1000 >"$OUT" +diff_output diff --git a/tests/posix/user_invalid_id.sh b/tests/posix/user_invalid_id.sh new file mode 100644 index 0000000..c378f7e --- /dev/null +++ b/tests/posix/user_invalid_id.sh @@ -0,0 +1 @@ +! invoke_bfs -user 1eW6f5RM9Qi diff --git a/tests/posix/user_invalid_name.sh b/tests/posix/user_invalid_name.sh new file mode 100644 index 0000000..bbf3031 --- /dev/null +++ b/tests/posix/user_invalid_name.sh @@ -0,0 +1 @@ +! invoke_bfs -user eW6f5RM9Qi diff --git a/tests/posix/user_o_user.out b/tests/posix/user_o_user.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/posix/user_o_user.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/posix/user_o_user.sh b/tests/posix/user_o_user.sh new file mode 100644 index 0000000..7c143ae --- /dev/null +++ b/tests/posix/user_o_user.sh @@ -0,0 +1,3 @@ +# Regression test for +# https://github.com/tavianator/bfs/issues/155 +bfs_diff basic -user 0 -o -user "$(id -u)" diff --git a/tests/ptyx.c b/tests/ptyx.c new file mode 100644 index 0000000..59292df --- /dev/null +++ b/tests/ptyx.c @@ -0,0 +1,252 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +/** + * Execute a command in a pseudo-terminal. + * + * $ ptyx [-w WIDTH] [-h HEIGHT] [--] COMMAND [ARGS...] + */ + +#include "bfs.h" +#include "bfstd.h" + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> + +#if __has_include(<stropts.h>) +# include <stropts.h> +#endif + +#if __sun +/** + * Push a STREAMS module, if it's not already there. + * + * See https://www.illumos.org/issues/9042. + */ +static int i_push(int fd, const char *name) { + int ret = ioctl(fd, I_FIND, name); + if (ret < 0) { + return ret; + } else if (ret == 0) { + return ioctl(fd, I_PUSH, name); + } else { + return 0; + } +} +#endif + +int main(int argc, char *argv[]) { + const char *cmd = argc > 0 ? argv[0] : "ptyx"; + +/** Report an error message and exit. */ +#define die(...) die_(__VA_ARGS__, ) + +#define die_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format "%s", cmd, __VA_ARGS__ "\n"); \ + exit(EXIT_FAILURE); \ + } while (0) + +/** Report an error code and exit. */ +#define edie(...) edie_(__VA_ARGS__, ) + +#define edie_(format, ...) \ + do { \ + fprintf(stderr, "%s: " format ": %s\n", cmd, __VA_ARGS__ errstr()); \ + exit(EXIT_FAILURE); \ + } while (0) + + unsigned short width = 0; + unsigned short height = 0; + + // Parse the command line + int c; + while (c = getopt(argc, argv, "+:w:h:"), c != -1) { + switch (c) { + case 'w': + if (xstrtous(optarg, NULL, 10, &width) != 0) { + edie("Bad width '%s'", optarg); + } + break; + case 'h': + if (xstrtous(optarg, NULL, 10, &height) != 0) { + edie("Bad height '%s'", optarg); + } + break; + case ':': + die("Missing argument to -%c", optopt); + case '?': + die("Unrecognized option -%c", optopt); + } + } + + if (optind >= argc) { + die("Missing command"); + } + char **args = argv + optind; + + // Create a new pty, and set it up + int ptm = posix_openpt(O_RDWR | O_NOCTTY); + if (ptm < 0) { + edie("posix_openpt()"); + } + if (grantpt(ptm) != 0) { + edie("grantpt()"); + } + if (unlockpt(ptm) != 0) { + edie("unlockpt()"); + } + + // Get the subsidiary device path + char *name = ptsname(ptm); + if (!name) { + edie("ptsname()"); + } + + // Open the subsidiary device + int pts = open(name, O_RDWR | O_NOCTTY); + if (pts < 0) { + edie("%s", name); + } + +#if __sun + // On Solaris/illumos, a pty doesn't behave like a terminal until we + // push some STREAMS modules (see ptm(4D), ptem(4M), ldterm(4M)). + if (i_push(pts, "ptem") != 0) { + die("ioctl(I_PUSH, ptem)"); + } + if (i_push(pts, "ldterm") != 0) { + die("ioctl(I_PUSH, ldterm)"); + } +#endif + + // A new pty starts at 0x0, which is not very useful. Instead, grab the + // default size from the current controlling terminal, if possible. + if (!width || !height) { + int tty = open_cterm(O_RDONLY | O_CLOEXEC); + if (tty >= 0) { + struct winsize ws; + if (xtcgetwinsize(tty, &ws) != 0) { + edie("tcgetwinsize()"); + } + if (!width) { + width = ws.ws_col; + } + if (!height) { + height = ws.ws_row; + } + xclose(tty); + } + } + if (!width) { + width = 80; + } + if (!height) { + height = 24; + } + + // Update the pty size + struct winsize ws; + if (xtcgetwinsize(pts, &ws) != 0) { + edie("tcgetwinsize()"); + } + ws.ws_col = width; + ws.ws_row = height; + if (xtcsetwinsize(pts, &ws) != 0) { + edie("tcsetwinsize()"); + } + + // Set custom terminal attributes + struct termios attrs; + if (tcgetattr(pts, &attrs) != 0) { + edie("tcgetattr()"); + } + attrs.c_oflag &= ~OPOST; // Don't convert \n to \r\n + if (tcsetattr(pts, TCSANOW, &attrs) != 0) { + edie("tcsetattr()"); + } + + pid_t pid = fork(); + if (pid < 0) { + edie("fork()"); + } else if (pid == 0) { + // Child + close(ptm); + + // Make ourselves a session leader so we can have our own + // controlling terminal + if (setsid() < 0) { + edie("setsid()"); + } + +#ifdef TIOCSCTTY + // Set the pty as the controlling terminal + if (ioctl(pts, TIOCSCTTY, 0) != 0) { + edie("ioctl(TIOCSCTTY)"); + } +#endif + + // Redirect std{in,out,err} to the pty + if (dup2(pts, STDIN_FILENO) < 0 + || dup2(pts, STDOUT_FILENO) < 0 + || dup2(pts, STDERR_FILENO) < 0) { + edie("dup2()"); + } + if (pts > STDERR_FILENO) { + xclose(pts); + } + + // Run the requested command + execvp(args[0], args); + edie("execvp(): %s", args[0]); + } + + // Parent + xclose(pts); + + // Read output from the pty and copy it to stdout + char buf[1024]; + while (true) { + ssize_t len = read(ptm, buf, sizeof(buf)); + if (len > 0) { + if (xwrite(STDOUT_FILENO, buf, len) < 0) { + edie("write()"); + } + } else if (len == 0) { + break; + } else if (errno == EINTR) { + continue; + } else if (errno == EIO) { + // Linux reports EIO rather than EOF when pts is closed + break; + } else { + die("read()"); + } + } + + xclose(ptm); + + int wstatus; + if (xwaitpid(pid, &wstatus, 0) < 0) { + edie("waitpid()"); + } + + if (WIFEXITED(wstatus)) { + return WEXITSTATUS(wstatus); + } else if (WIFSIGNALED(wstatus)) { + int sig = WTERMSIG(wstatus); + fprintf(stderr, "%s: %s: %s\n", cmd, args[0], strsignal(sig)); + return 128 + sig; + } else { + return 128; + } +} diff --git a/tests/run.sh b/tests/run.sh index 8c1402d..3ed2a9c 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -94,19 +94,17 @@ reap_test() { # Wait for a background test to finish wait_test() { - local pid + local pid line ret - while true; do - wait -n -ppid + while :; do + line=$((LINENO + 1)) + _wait -n -ppid ret=$? if [ "${pid:-}" ]; then break - elif ((ret > 128)); then - # Interrupted by signal - continue else - debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR + debug "${BASH_SOURCE[0]}" $line "${RED}error $ret${RST}" >&$DUPERR exit 1 fi done @@ -120,7 +118,9 @@ wait_ready() { # We'd like to parse the output of jobs -n, but we can't run it in a # subshell or we won't get the right output jobs -n >"$TMP/jobs" - while read -r job status ret foo; do + + local job status ret rest + while read -r job status ret rest; do case "$status" in Done) reap_test 0 @@ -156,7 +156,7 @@ comake() { exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]} } -# Print the current test progess +# Print the current test progress progress() { if [ "${BAR:-}" ]; then print_bar "$(printf "$@")" @@ -359,20 +359,12 @@ invoke_bfs() { fi } -if command -v unbuffer &>/dev/null; then - UNBUFFER=unbuffer -elif command -v expect_unbuffer &>/dev/null; then - UNBUFFER=expect_unbuffer -fi - # Run bfs with a pseudo-terminal attached bfs_pty() { - test -n "${UNBUFFER:-}" || skip - bfs_verbose "$@" local ret=0 - "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$? + "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$? if ((ret > 125)); then exit $ret @@ -410,6 +402,17 @@ make_xattrs() { esac } +# Get the Unix epoch time in seconds +epoch_time() { + if [ "${EPOCHSECONDS:-}" ]; then + # Added in bash 5 + printf '%d' "$EPOCHSECONDS" + else + # https://stackoverflow.com/a/12746260/502399 + awk 'BEGIN { srand(); print srand(); }' + fi +} + ## Snapshot testing # Return value when a difference is detected diff --git a/tests/sighook.c b/tests/sighook.c index c94526e..82e0ae5 100644 --- a/tests/sighook.c +++ b/tests/sighook.c @@ -1,15 +1,23 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "tests.h" -#include "sighook.h" + #include "atomic.h" +#include "bfstd.h" +#include "sighook.h" #include "thread.h" +#include "xtime.h" + +#include <errno.h> #include <pthread.h> #include <signal.h> -#include <sys/time.h> +#include <stddef.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> +/** Counts SIGALRM deliveries. */ static atomic size_t count = 0; /** SIGALRM handler. */ @@ -17,81 +25,204 @@ static void alrm_hook(int sig, siginfo_t *info, void *arg) { fetch_add(&count, 1, relaxed); } -/** Swap out an old hook for a new hook. */ -static int swap_hooks(struct sighook **hook) { - struct sighook *next = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); - if (!bfs_echeck(next, "sighook(SIGALRM)")) { - return -1; - } +/** SH_ONESHOT counter. */ +static atomic size_t shots = 0; - sigunhook(*hook); - *hook = next; - return 0; +/** SH_ONESHOT hook. */ +static void alrm_oneshot(int sig, siginfo_t *info, void *arg) { + fetch_add(&shots, 1, relaxed); } -/** Background thread that rapidly (un)registers signal hooks. */ +/** Keeps the background thread alive. */ +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; +static bool done = false; + +/** Background thread that receives signals. */ static void *hook_thread(void *ptr) { - struct sighook *hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); - if (!bfs_echeck(hook, "sighook(SIGALRM)")) { - return NULL; + mutex_lock(&mutex); + while (!done) { + cond_wait(&cond, &mutex); } + mutex_unlock(&mutex); + return NULL; +} - while (load(&count, relaxed) < 1000) { - if (swap_hooks(&hook) != 0) { - sigunhook(hook); - return NULL; - } +/** Block a signal in this thread. */ +static int block_signal(int sig, sigset_t *old) { + sigset_t set; + if (sigemptyset(&set) != 0) { + return -1; + } + if (sigaddset(&set, sig) != 0) { + return -1; } - sigunhook(hook); - return &count; + errno = pthread_sigmask(SIG_BLOCK, &set, old); + if (errno != 0) { + return -1; + } + + return 0; } -bool check_sighook(void) { - bool ret = true; +/** Tests for sighook(). */ +static void check_hooks(void) { + struct sighook *hook = NULL; + struct sighook *oneshot = NULL; - struct sighook *hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); - ret &= bfs_echeck(hook, "sighook(SIGALRM)"); - if (!ret) { - goto done; + hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(hook, "sighook(SIGALRM)")) { + return; } - struct itimerval ival = { - .it_value = { - .tv_usec = 100, - }, - .it_interval = { - .tv_usec = 100, - }, - }; - ret &= bfs_echeck(setitimer(ITIMER_REAL, &ival, NULL) == 0); - if (!ret) { + // Create a background thread to receive SIGALRM + pthread_t thread; + if (!bfs_echeck(thread_create(&thread, NULL, hook_thread, NULL) == 0)) { goto unhook; } - pthread_t thread; - ret &= bfs_echeck(thread_create(&thread, NULL, hook_thread, NULL) == 0); - if (!ret) { - goto untime; + // Block SIGALRM in this thread so the handler runs concurrently with + // sighook()/sigunhook() + sigset_t mask; + if (!bfs_echeck(block_signal(SIGALRM, &mask) == 0)) { + goto unthread; } - while (ret && load(&count, relaxed) < 1000) { - ret &= swap_hooks(&hook) == 0; + // Check that we can unregister and re-register a hook + sigunhook(hook); + hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(hook, "sighook(SIGALRM)")) { + goto unblock; } - void *ptr; - thread_join(thread, &ptr); - ret &= bfs_check(ptr); + // Test SH_ONESHOT + oneshot = sighook(SIGALRM, alrm_oneshot, NULL, SH_ONESHOT); + if (!bfs_echeck(oneshot, "sighook(SH_ONESHOT)")) { + goto unblock; + } -untime: - ival.it_value.tv_usec = 0; - ret &= bfs_echeck(setitimer(ITIMER_REAL, &ival, NULL) == 0); - if (!ret) { - goto unhook; + // Create a timer that sends SIGALRM every 100 microseconds + const struct timespec ival = { .tv_nsec = 100 * 1000 }; + struct timer *timer = xtimer_start(&ival); + if (!bfs_echeck(timer, "xtimer_start()")) { + goto unblock; + } + + // Rapidly register/unregister SIGALRM hooks + size_t alarms; + while (alarms = load(&count, relaxed), alarms < 1000) { + size_t nshots = load(&shots, relaxed); + bfs_check(nshots <= 1); + if (alarms > 1) { + bfs_check(nshots == 1); + } + if (alarms >= 500) { + sigunhook(oneshot); + oneshot = NULL; + } + + struct sighook *next = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE); + if (!bfs_echeck(next, "sighook(SIGALRM)")) { + break; + } + + sigunhook(hook); + hook = next; } + // Stop the timer + xtimer_stop(timer); +unblock: + // Restore the old signal mask + errno = pthread_sigmask(SIG_SETMASK, &mask, NULL); + bfs_echeck(errno == 0, "pthread_sigmask()"); +unthread: + // Quit the background thread + mutex_lock(&mutex); + done = true; + mutex_unlock(&mutex); + cond_signal(&cond); + thread_join(thread, NULL); unhook: + // Unregister the SIGALRM hooks + sigunhook(oneshot); sigunhook(hook); -done: - return ret; +} + +/** atsigexit() hook. */ +static void exit_hook(int sig, siginfo_t *info, void *arg) { + // Write the signal that's killing us to the pipe + int *pipes = arg; + if (xwrite(pipes[1], &sig, sizeof(sig)) != sizeof(sig)) { + abort(); + } +} + +/** Tests for atsigexit(). */ +static void check_sigexit(int sig) { + // To wait for the child to call atsigexit() + int ready[2]; + bfs_everify(pipe(ready) == 0); + + // Written in the atsigexit() handler + int killed[2]; + bfs_everify(pipe(killed) == 0); + + pid_t pid; + bfs_everify((pid = fork()) >= 0); + + if (pid > 0) { + // Parent + xclose(ready[1]); + xclose(killed[1]); + + // Wait for the child to call atsigexit() + char c; + bfs_everify(xread(ready[0], &c, 1) == 1); + + // Kill the child with the signal + bfs_everify(kill(pid, sig) == 0); + + // Check that the child died to the right signal + int wstatus; + if (bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)) { + bfs_check(WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == sig); + } + + // Check that the signal hook wrote the signal number to the pipe + int hsig; + if (bfs_echeck(xread(killed[0], &hsig, sizeof(hsig)) == sizeof(hsig))) { + bfs_check(hsig == sig); + } + } else { + // Child + xclose(ready[0]); + xclose(killed[0]); + + // exit_hook() will write to killed[1] + bfs_everify(atsigexit(exit_hook, killed) != NULL); + + // Tell the parent we're ready + bfs_everify(xwrite(ready[1], "A", 1) == 1); + + // Wait until we're killed + const struct timespec dur = { .tv_nsec = 1 }; + while (true) { + nanosleep(&dur, NULL); + } + } +} + +void check_sighook(void) { + check_hooks(); + + check_sigexit(SIGINT); + check_sigexit(SIGQUIT); + check_sigexit(SIGPIPE); + + // macOS cannot distinguish between sync and async SIG{BUS,ILL,SEGV} +#if !__APPLE__ + check_sigexit(SIGSEGV); +#endif } diff --git a/tests/stddirs.sh b/tests/stddirs.sh index e08e6bf..1569fee 100644 --- a/tests/stddirs.sh +++ b/tests/stddirs.sh @@ -14,13 +14,31 @@ make_basic() { # Creates a file+directory structure with various permissions for tests make_perms() { - "$XTOUCH" -p -M000 "$1/0" - "$XTOUCH" -p -M444 "$1/r" - "$XTOUCH" -p -M222 "$1/w" - "$XTOUCH" -p -M644 "$1/rw" - "$XTOUCH" -p -M555 "$1/rx" - "$XTOUCH" -p -M311 "$1/wx" - "$XTOUCH" -p -M755 "$1/rwx" + "$XTOUCH" -p -M000 "$1/f---------" + "$XTOUCH" -p -M111 "$1/f--x--x--x" + "$XTOUCH" -p -M222 "$1/f-w--w--w-" + "$XTOUCH" -p -M333 "$1/f-wx-wx-wx" + "$XTOUCH" -p -M444 "$1/fr--r--r--" + "$XTOUCH" -p -M555 "$1/fr-xr-xr-x" "$1/dr-xr-xr-x/" + "$XTOUCH" -p -M666 "$1/frw-rw-rw-" + "$XTOUCH" -p -M777 "$1/frwxrwxrwx" "$1/drwxrwxrwx/" + + "$XTOUCH" -p -M220 "$1/f-w--w----" + "$XTOUCH" -p -M331 "$1/f-wx-wx--x" + "$XTOUCH" -p -M664 "$1/frw-rw-r--" + "$XTOUCH" -p -M775 "$1/frwxrwxr-x" "$1/drwxrwxr-x/" + + "$XTOUCH" -p -M311 "$1/f-wx--x--x" + "$XTOUCH" -p -M644 "$1/frw-r--r--" + "$XTOUCH" -p -M755 "$1/frwxr-xr-x" "$1/drwxr-xr-x/" + + "$XTOUCH" -p -M100 "$1/f--x------" + "$XTOUCH" -p -M200 "$1/f-w-------" + "$XTOUCH" -p -M300 "$1/f-wx------" + "$XTOUCH" -p -M400 "$1/fr--------" + "$XTOUCH" -p -M500 "$1/fr-x------" "$1/dr-x------/" + "$XTOUCH" -p -M600 "$1/frw-------" + "$XTOUCH" -p -M700 "$1/frwxr-----" "$1/drwx------/" } # Creates a file+directory structure with various symbolic and hard links @@ -48,6 +66,12 @@ make_loops() { ln -s deeply/nested/loop/nested "$1/skip" } +# Creates a file+directory structure with inaccessible files +make_inaccessible() { + "$XTOUCH" -p -M000 "$1/file" "$1/dir/" + ln -s dir/file "$1/link" +} + # Creates a file+directory structure with varying timestamps make_times() { "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a" @@ -71,6 +95,9 @@ make_weirdnames() { "$XTOUCH" -p "$1/\\/i" "$XTOUCH" -p "$1/ /j" "$XTOUCH" -p "$1/[/k" + "$XTOUCH" -p "$1/{/l" + "$XTOUCH" -p "$1/*/m" + "$XTOUCH" -p "$1/"$'\n/n' } # Creates a very deep directory structure for testing PATH_MAX handling @@ -124,7 +151,7 @@ make_stddirs() { if ((CLEAN)); then defer clean_stddirs else - printf "Test files saved to ${BLD}%s${RST}\n" "$TMP" + color printf "Test files saved to ${BLD}%s${RST}\n" "$TMP" fi chown "$(id -u):$(id -g)" "$TMP" @@ -133,6 +160,7 @@ make_stddirs() { make_perms "$TMP/perms" make_links "$TMP/links" make_loops "$TMP/loops" + make_inaccessible "$TMP/inaccessible" make_times "$TMP/times" make_weirdnames "$TMP/weirdnames" make_deep "$TMP/deep" @@ -148,5 +176,6 @@ clean_stddirs() { fi done + chmod -R +rwX "$TMP" rm -rf "$TMP" } diff --git a/tests/tests.h b/tests/tests.h index de95a49..d395c7c 100644 --- a/tests/tests.h +++ b/tests/tests.h @@ -8,67 +8,67 @@ #ifndef BFS_TESTS_H #define BFS_TESTS_H -#include "prelude.h" #include "bfstd.h" #include "diag.h" -/** Unit test function type. */ -typedef bool test_fn(void); - /** Memory allocation tests. */ -bool check_alloc(void); +void check_alloc(void); /** Standard library wrapper tests. */ -bool check_bfstd(void); +void check_bfstd(void); /** Bit manipulation tests. */ -bool check_bit(void); +void check_bit(void); /** I/O queue tests. */ -bool check_ioq(void); +void check_ioq(void); + +/** Linked list tests. */ +void check_list(void); /** Signal hook tests. */ -bool check_sighook(void); +void check_sighook(void); /** Trie tests. */ -bool check_trie(void); +void check_trie(void); /** Process spawning tests. */ -bool check_xspawn(void); +void check_xspawn(void); /** Time tests. */ -bool check_xtime(void); +void check_xtime(void); -/** Don't ignore the bfs_check() return value. */ -attr(nodiscard) -static inline bool bfs_check(bool ret) { - return ret; -} +/** Record a single check and return the result. */ +bool bfs_check_impl(bool result); /** * Check a condition, logging a message on failure but continuing. */ #define bfs_check(...) \ - bfs_check(bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", "")) + bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", ) #define bfs_check_(str, cond, format, ...) \ - ((cond) ? true : (bfs_diag( \ - sizeof(format) > 1 \ - ? "%.0s" format "%s%s" \ - : "Check failed: `%s`%s", \ - str, __VA_ARGS__), false)) + bfs_check_impl((cond) || (bfs_check__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false)) + +#define bfs_check__(format, ...) \ + bfs_diagf(sizeof(format) > 1 \ + ? BFS_DIAG_FORMAT_("%s" format "%s") \ + : BFS_DIAG_FORMAT_("Check failed: `%s`"), \ + BFS_DIAG_ARGS_(__VA_ARGS__)) /** * Check a condition, logging the current error string on failure. */ #define bfs_echeck(...) \ - bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", errstr()) + bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", ) #define bfs_echeck_(str, cond, format, ...) \ - ((cond) ? true : (bfs_diag( \ - sizeof(format) > 1 \ - ? "%.0s" format "%s: %s" \ - : "Check failed: `%s`: %s", \ - str, __VA_ARGS__), false)) + bfs_check_impl((cond) || (bfs_echeck__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false)) + +#define bfs_echeck__(format, ...) \ + bfs_diagf(sizeof(format) > 1 \ + ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \ + : BFS_DIAG_FORMAT_("Check failed: `%s`: %s"), \ + BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), )) #endif // BFS_TESTS_H diff --git a/tests/trie.c b/tests/trie.c index 4667322..59bde40 100644 --- a/tests/trie.c +++ b/tests/trie.c @@ -1,14 +1,16 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "tests.h" -#include "trie.h" + +#include "bfs.h" #include "diag.h" +#include "trie.h" + #include <stdlib.h> #include <string.h> -const char *keys[] = { +static const char *keys[] = { "foo", "bar", "baz", @@ -18,9 +20,11 @@ const char *keys[] = { "quuuux", "pre", - "pref", "prefi", + "pref", "prefix", + "p", + "pRefix", "AAAA", "AADD", @@ -35,18 +39,20 @@ const char *keys[] = { ">>>>>>", ">>><<<", ">>>", -}; -const size_t nkeys = countof(keys); + "AAAAAAA", + "AAAAAAAB", + "AAAAAAAa", +}; -bool check_trie(void) { - bool ret = true; +static const size_t nkeys = countof(keys); +void check_trie(void) { struct trie trie; trie_init(&trie); for (size_t i = 0; i < nkeys; ++i) { - ret &= bfs_check(!trie_find_str(&trie, keys[i])); + bfs_check(!trie_find_str(&trie, keys[i])); const char *prefix = NULL; for (size_t j = 0; j < i; ++j) { @@ -60,37 +66,38 @@ bool check_trie(void) { struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]); if (prefix) { bfs_verify(leaf); - ret &= bfs_check(strcmp(prefix, leaf->key) == 0); + bfs_check(strcmp(prefix, leaf->key) == 0); } else { - ret &= bfs_check(!leaf); + bfs_check(!leaf); } leaf = trie_insert_str(&trie, keys[i]); bfs_verify(leaf); - ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); - ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); + bfs_check(strcmp(keys[i], leaf->key) == 0); + bfs_check(leaf->length == strlen(keys[i]) + 1); } { size_t i = 0; for_trie (leaf, &trie) { - ret &= bfs_check(leaf == trie_find_str(&trie, keys[i])); - ret &= bfs_check(!leaf->prev || leaf->prev->next == leaf); - ret &= bfs_check(!leaf->next || leaf->next->prev == leaf); + bfs_check(leaf == trie_find_str(&trie, keys[i])); + bfs_check(leaf == trie_insert_str(&trie, keys[i])); + bfs_check(!leaf->prev || leaf->prev->next == leaf); + bfs_check(!leaf->next || leaf->next->prev == leaf); ++i; } - ret &= bfs_check(i == nkeys); + bfs_check(i == nkeys); } for (size_t i = 0; i < nkeys; ++i) { struct trie_leaf *leaf = trie_find_str(&trie, keys[i]); bfs_verify(leaf); - ret &= bfs_check(strcmp(keys[i], leaf->key) == 0); - ret &= bfs_check(leaf->length == strlen(keys[i]) + 1); + bfs_check(strcmp(keys[i], leaf->key) == 0); + bfs_check(leaf->length == strlen(keys[i]) + 1); trie_remove(&trie, leaf); leaf = trie_find_str(&trie, keys[i]); - ret &= bfs_check(!leaf); + bfs_check(!leaf); const char *postfix = NULL; for (size_t j = i + 1; j < nkeys; ++j) { @@ -104,14 +111,14 @@ bool check_trie(void) { leaf = trie_find_postfix(&trie, keys[i]); if (postfix) { bfs_verify(leaf); - ret &= bfs_check(strcmp(postfix, leaf->key) == 0); + bfs_check(strcmp(postfix, leaf->key) == 0); } else { - ret &= bfs_check(!leaf); + bfs_check(!leaf); } } for_trie (leaf, &trie) { - ret &= bfs_check(false, "trie should be empty"); + bfs_check(false, "trie should be empty"); } // This tests the "jump" node handling on 32-bit platforms @@ -120,18 +127,17 @@ bool check_trie(void) { bfs_verify(longstr); memset(longstr, 0xAC, longsize); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); memset(longstr + longsize / 2, 0xAB, longsize / 2); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); memset(longstr, 0xAA, longsize / 2); - ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize)); - ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize)); + bfs_check(!trie_find_mem(&trie, longstr, longsize)); + bfs_check(trie_insert_mem(&trie, longstr, longsize)); free(longstr); trie_destroy(&trie); - return ret; } diff --git a/tests/util.sh b/tests/util.sh index 76b72b9..1718a1a 100644 --- a/tests/util.sh +++ b/tests/util.sh @@ -16,6 +16,7 @@ ROOT=$(_realpath "$(dirname -- "$TESTS")") TESTS="$ROOT/tests" BIN="$ROOT/bin" MKSOCK="$BIN/tests/mksock" +PTYX="$BIN/tests/ptyx" XTOUCH="$BIN/tests/xtouch" UNAME=$(uname) @@ -70,7 +71,7 @@ stdenv() { fi } -# Drop root priviliges or bail +# Drop root privileges or bail drop_root() { if command -v capsh &>/dev/null; then if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then @@ -189,3 +190,28 @@ pop_defers() { return $ret } + +## Parallelism + +# Get the number of processors +_nproc() { + { + nproc \ + || sysctl -n hw.ncpu \ + || getconf _NPROCESSORS_ONLN \ + || echo 1 + } 2>/dev/null +} + +# Run wait, looping if interrupted +_wait() { + local ret=130 + + # "If wait is interrupted by a signal, the return status will be greater than 128" + while ((ret > 128)); do + ret=0 + wait "$@" || ret=$? + done + + return $ret +} diff --git a/tests/xspawn.c b/tests/xspawn.c index b1d6dc1..6864192 100644 --- a/tests/xspawn.c +++ b/tests/xspawn.c @@ -1,12 +1,13 @@ // 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> @@ -49,102 +50,123 @@ fail: return NULL; } -/** Check that we resolve executables in $PATH correctly. */ -static bool check_use_path(bool use_posix) { - bool ret = true; +/** Add an entry to $PATH. */ +static int add_path(const char *entry, char **old_path) { + int ret = -1; + const char *new_path = NULL; - struct bfs_spawn spawn; - ret &= bfs_echeck(bfs_spawn_init(&spawn) == 0); - if (!ret) { - goto out; - } + *old_path = getenv("PATH"); + if (*old_path) { + *old_path = strdup(*old_path); + if (!*old_path) { + goto done; + } - spawn.flags |= BFS_SPAWN_USE_PATH; - if (!use_posix) { - spawn.flags &= ~BFS_SPAWN_USE_POSIX; + new_path = dstrprintf("%s:%s", entry, *old_path); + if (!new_path) { + goto done; + } + } else { + new_path = entry; } - ret &= bfs_echeck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0); - ret &= bfs_echeck(bfs_spawn_adddup2(&spawn, 10, 11) == 0); - ret &= bfs_echeck(bfs_spawn_addclose(&spawn, 10) == 0); - ret &= bfs_echeck(bfs_spawn_addfchdir(&spawn, 11) == 0); - ret &= bfs_echeck(bfs_spawn_addclose(&spawn, 11) == 0); - if (!ret) { - goto destroy; + ret = setenv("PATH", new_path, true); + +done: + if (new_path && new_path != entry) { + dstrfree((dchar *)new_path); } - // Check that $PATH is resolved in the parent's environment - char **envp; - ret &= bfs_echeck(envp = envdup()); - if (!ret) { - goto destroy; + if (ret != 0) { + free(*old_path); + *old_path = NULL; } - // Check that $PATH is resolved after the file actions - char *old_path = getenv("PATH"); - dchar *new_path = NULL; + return ret; +} + +/** Undo add_path(). */ +static int reset_path(char *old_path) { + int ret; + if (old_path) { - ret &= bfs_echeck(old_path = strdup(old_path)); - if (!ret) { - goto env; - } - new_path = dstrprintf("tests:%s", old_path); + ret = setenv("PATH", old_path, true); + free(old_path); } else { - new_path = dstrdup("tests"); - } - ret &= bfs_check(new_path); - if (!ret) { - goto path; + ret = unsetenv("PATH"); } - ret &= bfs_echeck(setenv("PATH", new_path, true) == 0); - if (!ret) { - goto path; - } + return ret; +} - char *argv[] = {"xspawnee", old_path, NULL}; - pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp); - ret &= bfs_echeck(pid >= 0, "bfs_spawn()"); - if (!ret) { - goto unset; +/** Spawn the test binary and check for success. */ +static void check_spawnee(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { + pid_t pid = bfs_spawn(exe, ctx, argv, envp); + if (!bfs_echeck(pid >= 0, "bfs_spawn('%s')", exe)) { + return; } int wstatus; - ret &= bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid) + bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid) && bfs_check(WIFEXITED(wstatus)); - if (ret) { + if (exited) { int wexit = WEXITSTATUS(wstatus); - ret &= bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); + bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit); } +} -unset: - if (old_path) { - ret &= bfs_echeck(setenv("PATH", old_path, true) == 0); - } else { - ret &= bfs_echeck(unsetenv("PATH") == 0); +/** Check that we resolve executables in $PATH correctly. */ +static void check_use_path(bool use_posix) { + struct bfs_spawn spawn; + if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) { + return; + } + + spawn.flags |= BFS_SPAWN_USE_PATH; + if (!use_posix) { + spawn.flags &= ~BFS_SPAWN_USE_POSIX; } -path: - dstrfree(new_path); - free(old_path); + + bool init = bfs_echeck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0) + && bfs_echeck(bfs_spawn_adddup2(&spawn, 10, 11) == 0) + && bfs_echeck(bfs_spawn_addclose(&spawn, 10) == 0) + && bfs_echeck(bfs_spawn_addfchdir(&spawn, 11) == 0) + && bfs_echeck(bfs_spawn_addclose(&spawn, 11) == 0); + if (!init) { + goto destroy; + } + + // Check that $PATH is resolved in the parent's environment + char **envp = envdup(); + if (!bfs_echeck(envp, "envdup()")) { + goto destroy; + } + + // Check that $PATH is resolved after the file actions + char *old_path; + if (!bfs_echeck(add_path("tests", &old_path) == 0)) { + goto env; + } + + char *argv[] = {"xspawnee", old_path, NULL}; + check_spawnee("xspawnee", &spawn, argv, envp); + check_spawnee("tests/xspawnee", &spawn, argv, envp); + + bfs_echeck(reset_path(old_path) == 0); env: for (char **var = envp; *var; ++var) { free(*var); } free(envp); destroy: - ret &= bfs_echeck(bfs_spawn_destroy(&spawn) == 0); -out: - return ret; + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); } /** Check path resolution of non-existent executables. */ -static bool check_enoent(bool use_posix) { - bool ret = true; - +static void check_enoent(bool use_posix) { struct bfs_spawn spawn; - ret &= bfs_echeck(bfs_spawn_init(&spawn) == 0); - if (!ret) { - goto out; + if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) { + return; } spawn.flags |= BFS_SPAWN_USE_PATH; @@ -154,44 +176,45 @@ static bool check_enoent(bool use_posix) { char *argv[] = {"eW6f5RM9Qi", NULL}; pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL); - ret &= bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()"); + bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()"); - ret &= bfs_echeck(bfs_spawn_destroy(&spawn) == 0); -out: - return ret; + bfs_echeck(bfs_spawn_destroy(&spawn) == 0); } -static bool check_resolve(void) { - bool ret = true; +static void check_resolve(void) { char *exe; exe = bfs_spawn_resolve("sh"); - ret &= bfs_echeck(exe, "bfs_spawn_resolve('sh')"); + bfs_echeck(exe, "bfs_spawn_resolve('sh')"); free(exe); exe = bfs_spawn_resolve("/bin/sh"); - ret &= bfs_echeck(exe && strcmp(exe, "/bin/sh") == 0); + bfs_echeck(exe && strcmp(exe, "/bin/sh") == 0); free(exe); exe = bfs_spawn_resolve("bin/tests/xspawnee"); - ret &= bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); + bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); free(exe); - ret &= bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); - - return ret; -} + bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); -bool check_xspawn(void) { - bool ret = true; + bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT); - ret &= check_use_path(true); - ret &= check_use_path(false); + char *old_path; + if (bfs_echeck(add_path("bin/tests", &old_path) == 0)) { + exe = bfs_spawn_resolve("xspawnee"); + bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0); + free(exe); + bfs_echeck(reset_path(old_path) == 0); + } +} - ret &= check_enoent(true); - ret &= check_enoent(false); +void check_xspawn(void) { + check_use_path(true); + check_use_path(false); - ret &= check_resolve(); + check_enoent(true); + check_enoent(false); - return ret; + check_resolve(); } diff --git a/tests/xtime.c b/tests/xtime.c index 1907e26..c890a1e 100644 --- a/tests/xtime.c +++ b/tests/xtime.c @@ -1,11 +1,12 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "tests.h" -#include "xtime.h" -#include "bfstd.h" + +#include "bfs.h" #include "diag.h" +#include "xtime.h" + #include <errno.h> #include <limits.h> #include <stdint.h> @@ -39,35 +40,31 @@ static bool check_one_xgetdate(const char *str, int error, time_t expected) { } /** xgetdate() tests. */ -static bool check_xgetdate(void) { - bool ret = true; - - ret &= check_one_xgetdate("", EINVAL, 0); - ret &= check_one_xgetdate("????", EINVAL, 0); - ret &= check_one_xgetdate("1991", EINVAL, 0); - ret &= check_one_xgetdate("1991-??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14", 0, 692668800); - ret &= check_one_xgetdate("1991-12-14-", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T10:??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10:11", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472); - ret &= check_one_xgetdate("1991-12-14T10Z", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472); - ret &= check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0); - ret &= check_one_xgetdate("1991-12-14T03-07", 0, 692704800); - ret &= check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460); - ret &= check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472); - ret &= check_one_xgetdate("19911214 031112-0700", 0, 692705472);; - - return ret; +static void check_xgetdate(void) { + check_one_xgetdate("", EINVAL, 0); + check_one_xgetdate("????", EINVAL, 0); + check_one_xgetdate("1991", EINVAL, 0); + check_one_xgetdate("1991-??", EINVAL, 0); + check_one_xgetdate("1991-12", EINVAL, 0); + check_one_xgetdate("1991-12-", EINVAL, 0); + check_one_xgetdate("1991-12-??", EINVAL, 0); + check_one_xgetdate("1991-12-14", 0, 692668800); + check_one_xgetdate("1991-12-14-", EINVAL, 0); + check_one_xgetdate("1991-12-14T", EINVAL, 0); + check_one_xgetdate("1991-12-14T??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10", 0, 692704800); + check_one_xgetdate("1991-12-14T10:??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10:11", 0, 692705460); + check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0); + check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472); + check_one_xgetdate("1991-12-14T10Z", 0, 692704800); + check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460); + check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472); + check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0); + check_one_xgetdate("1991-12-14T03-07", 0, 692704800); + check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460); + check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472); + check_one_xgetdate("19911214 031112-0700", 0, 692705472);; } #define TM_FORMAT "%04d-%02d-%02d %02d:%02d:%02d (%d/7, %d/365%s)" @@ -92,11 +89,9 @@ static bool check_one_xmktime(time_t expected) { } /** xmktime() tests. */ -static bool check_xmktime(void) { - bool ret = true; - +static void check_xmktime(void) { for (time_t time = -10; time <= 10; ++time) { - ret &= check_one_xmktime(time); + check_one_xmktime(time); } // Attempt to trigger overflow (but don't test for it, since it's not mandatory) @@ -111,12 +106,10 @@ static bool check_xmktime(void) { }; time_t time; xmktime(&tm, &time); - - return ret; } /** Check one xtimegm() result. */ -static bool check_one_xtimegm(const struct tm *tm) { +static void check_one_xtimegm(const struct tm *tm) { struct tm tma = *tm, tmb = *tm; time_t ta, tb; ta = mktime(&tma); @@ -124,47 +117,51 @@ static bool check_one_xtimegm(const struct tm *tm) { tb = -1; } - bool ret = true; - ret &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb); - ret &= bfs_check(ta == -1 || tm_equal(&tma, &tmb)); + bool pass = true; + pass &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb); + if (ta != -1) { + pass &= bfs_check(tm_equal(&tma, &tmb)); + } - if (!ret) { + if (!pass) { bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma)); bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb)); bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); } - - return ret; } #if !BFS_HAS_TIMEGM /** Check an overflowing xtimegm() call. */ -static bool check_xtimegm_overflow(const struct tm *tm) { +static void check_xtimegm_overflow(const struct tm *tm) { struct tm copy = *tm; time_t time = 123; - bool ret = true; - ret &= bfs_check(xtimegm(©, &time) == -1 && errno == EOVERFLOW); - ret &= bfs_check(tm_equal(©, tm)); - ret &= bfs_check(time == 123); + bool pass = true; + pass &= bfs_check(xtimegm(©, &time) == -1 && errno == EOVERFLOW); + pass &= bfs_check(tm_equal(©, tm)); + pass &= bfs_check(time == 123); - if (!ret) { + if (!pass) { bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy)); bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm)); } - - return ret; } #endif /** xtimegm() tests. */ -static bool check_xtimegm(void) { - bool ret = true; - +static void check_xtimegm(void) { struct tm tm = { .tm_isdst = -1, }; +#if BFS_HAS_TIMEGM + // Check that xtimegm(-1) isn't an error + for (time_t time = -10; time <= 10; ++time) { + if (bfs_check(gmtime_r(&time, &tm), "gmtime_r(%jd)", (intmax_t)time)) { + check_one_xtimegm(&tm); + } + } +#else // Check equivalence with mktime() for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10) for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3) @@ -172,24 +169,19 @@ static bool check_xtimegm(void) { for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5) for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31) for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) { - ret &= check_one_xtimegm(&tm); + check_one_xtimegm(&tm); } -#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; + check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX }); + check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX }); +#endif // !BFS_HAS_TIMEGM } -bool check_xtime(void) { - bool ret = true; - ret &= check_xgetdate(); - ret &= check_xmktime(); - ret &= check_xtimegm(); - return ret; +void check_xtime(void) { + check_xgetdate(); + check_xmktime(); + check_xtimegm(); } diff --git a/tests/xtouch.c b/tests/xtouch.c index 427e3e0..f33c573 100644 --- a/tests/xtouch.c +++ b/tests/xtouch.c @@ -1,10 +1,10 @@ // Copyright © Tavian Barnes <tavianator@tavianator.com> // SPDX-License-Identifier: 0BSD -#include "prelude.h" #include "bfstd.h" #include "sanity.h" #include "xtime.h" + #include <errno.h> #include <fcntl.h> #include <stdio.h> @@ -217,11 +217,8 @@ int main(int argc, char *argv[]) { } if (marg) { - char *end; - long mode = strtol(marg, &end, 8); - // https://github.com/llvm/llvm-project/issues/64946 - sanitize_init(&end); - if (*marg && !*end && mode >= 0 && mode < 01000) { + unsigned int mode; + if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) { args.fmode = args.dmode = mode; } else { fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg); @@ -247,8 +244,8 @@ int main(int argc, char *argv[]) { times[1] = times[0]; } else { // Don't use UTIME_NOW, so that multiple paths all get the same timestamp - if (xgettime(×[0]) != 0) { - perror("xgettime()"); + if (clock_gettime(CLOCK_REALTIME, ×[0]) != 0) { + perror("clock_gettime()"); return EXIT_FAILURE; } times[1] = times[0]; |