diff options
133 files changed, 1545 insertions, 568 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml index 6ff8337..b61c30e 100644 --- a/.github/codeql.yml +++ b/.github/codeql.yml @@ -2,6 +2,8 @@ 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 diff --git a/.github/diag.sh b/.github/diag.sh index 942487a..fe78be8 100755 --- a/.github/diag.sh +++ b/.github/diag.sh @@ -13,4 +13,4 @@ filter() { \1/' } -"$@" 2> >(filter >&2) | filter +exec "$@" > >(filter) 2> >(filter >&2) @@ -1,4 +1,4 @@ -Copyright © 2015-2023 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors +Copyright © 2015-2024 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. @@ -66,22 +66,20 @@ ${OBJS}: gen/config.mk ${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:: +gen/version.i.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' >>$@ + echo "3.3.1"; \ + fi | tr -d '\n' | build/embed.sh >$@ -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 $@ ${.ALLSRC} && ${RM} ${.ALLSRC} || mv ${.ALLSRC} $@ -obj/gen/version.o: gen/version.c +obj/src/version.o: gen/version.i ## Test phase (`make check`) @@ -217,6 +215,11 @@ 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" \ + groff -man -rCHECKSTYLE=3 -ww -b -z docs/bfs.1 + ## Cleanup (`make clean`) # Clean all build products diff --git a/build/cc.sh b/build/cc.sh index 45d51ca..23a4c01 100755 --- a/build/cc.sh +++ b/build/cc.sh @@ -5,12 +5,5 @@ # Run the compiler and check if it succeeded -set -eu - -TMP=$(mktemp) -trap 'rm -f "$TMP"' EXIT - -( - set -x - $XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS -o "$TMP" -) +set -eux +$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS diff --git a/build/config.mk b/build/config.mk index 24873ec..80206c7 100644 --- a/build/config.mk +++ b/build/config.mk @@ -7,7 +7,7 @@ include build/prelude.mk include build/exports.mk # All configuration steps -config: gen/config.mk gen/config.h +config: gen/config.mk .PHONY: config # Makefile fragments generated by `./configure` @@ -18,10 +18,10 @@ MKS := \ gen/pkgs.mk # The main configuration file, which includes the others -gen/config.mk: ${MKS} +gen/config.mk: ${MKS} gen/config.h ${MSG} "[ GEN] $@" @printf '# %s\n' "$@" >$@ - @printf 'include %s\n' ${.ALLSRC} >>$@ + @printf 'include %s\n' ${MKS} >>$@ ${VCAT} gen/config.mk .PHONY: gen/config.mk @@ -57,6 +57,6 @@ gen/pkgs.mk: gen/flags.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/deps.mk b/build/deps.mk index d382f5d..a6ea673 100644 --- a/build/deps.mk +++ b/build/deps.mk @@ -11,7 +11,7 @@ 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 \ + @if build/cc.sh -MD -MP build/empty.c -o gen/.deps.out; then \ printf '_CPPFLAGS += -MD -MP\n'; \ fi >>$@ 2>$@.log ${VCAT} $@ diff --git a/build/embed.sh b/build/embed.sh new file mode 100755 index 0000000..8d7d0f1 --- /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\1,/g' 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/header.mk b/build/header.mk index 09454c5..afd04d0 100644 --- a/build/header.mk +++ b/build/header.mk @@ -4,7 +4,10 @@ # Makefile that generates gen/config.h include build/prelude.mk -include gen/config.mk +include gen/vars.mk +include gen/flags.mk +include gen/deps.mk +include gen/pkgs.mk include build/exports.mk # All header fragments we generate @@ -16,6 +19,7 @@ HEADERS := \ 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/extattr-get-file.h \ gen/has/extattr-get-link.h \ @@ -60,14 +64,30 @@ gen/config.h: ${PKG_HEADERS} ${HEADERS} @printf '#define BFS_CONFIG_H\n' >>$@ @cat ${.ALLSRC} >>$@ @printf '#endif // BFS_CONFIG_H\n' >>$@ - @cat ${.ALLSRC:%=%.log} >gen/config.log + @cat gen/cc.log ${.ALLSRC:%=%.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}:: +${HEADERS}: cc @${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 +.PHONY: ${HEADERS} + +# Check that the C compiler works at all +cc:: + @build/cc.sh build/empty.c -o gen/.cc.out 2>gen/cc.log; \ + ret=$$?; \ + build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \ + exit $$ret 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..a8a3341 100755 --- a/build/pkgconf.sh +++ b/build/pkgconf.sh @@ -37,7 +37,7 @@ if [ -z "$MODE" ]; then 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 + build/cc.sh $CFLAGS $LDFLAGS "build/with/$LIB.c" $LDLIBS -o "gen/with/.$LIB.out" || exit 1 done fi diff --git a/build/prelude.mk b/build/prelude.mk index d0968ea..76fbce8 100644 --- a/build/prelude.mk +++ b/build/prelude.mk @@ -97,10 +97,10 @@ LIBBFS := \ 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 \ - obj/gen/version.o # Unit test objects UNIT_OBJS := \ @@ -108,6 +108,7 @@ UNIT_OBJS := \ 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 \ @@ -7,19 +7,62 @@ 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_' +# Print the help message +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 } # Report an argument parsing error @@ -29,7 +72,26 @@ invalid() { exit 1 } +# Get the number of cores to use +nproc() { + { + command 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 + # --[(enable|disable|with|without)-]$name[=$value] value="${arg#*=}" name="${arg%%=*}" @@ -77,67 +139,13 @@ for arg; do 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 set -- "$@" "$NAME=$yn" ;; *) @@ -149,7 +157,6 @@ EOF --with-*|--without-*) case "$name" in libacl|libcap|libselinux|liburing|oniguruma) - shift set -- "$@" "WITH_$NAME=$yn" ;; *) @@ -159,23 +166,20 @@ EOF ;; --prefix=*|--mandir=*) - shift set -- "$@" "$NAME=$value" ;; --infodir=*|--build=*|--host=*|--target=*) - shift printf 'warning: Ignoring option "%s"\n' "$arg" >&2 ;; MAKE=*) - shift MAKE="$value" ;; # make flag (-j2) or variable (CC=clang) -*|*=*) - continue + set -- "$@" "$arg" ;; *) @@ -193,6 +197,6 @@ for f in Makefile build completions docs src tests; do done # Set MAKEFLAGS to -j$(nproc) if it's unset -export MAKEFLAGS="${MAKEFLAGS-$j}" +export MAKEFLAGS="${MAKEFLAGS--j$(nproc)}" $MAKE -rf build/config.mk "$@" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 62b6480..db978b8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,50 @@ 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 --- 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..f26efc5 --- /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, incuding: + +- **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 we 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. @@ -1,4 +1,4 @@ -.TH BFS 1 +.TH BFS 1 2024-06-17 "bfs 3.3.1" .SH NAME bfs \- breadth-first search for your files .SH SYNOPSIS @@ -45,7 +45,7 @@ For example, .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 @@ -71,7 +71,7 @@ will also accept .I \-N or .IR +N . -.IR \-N +.I \-N means "less than .IR N ," and @@ -479,6 +479,10 @@ Find files the current user can execute/read/write. Always false/true. .RE .TP +\fB\-flags\fR [\fI\-+\fR]\fIFLAGS\fR +Find files with matching inode +.BR FLAGS . +.TP \fB\-fstype \fITYPE\fR Find files on file systems with the given .IR TYPE . @@ -582,7 +586,7 @@ 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 @@ -913,7 +917,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/atomic.h b/src/atomic.h index 360de20..ad5303b 100644 --- a/src/atomic.h +++ b/src/atomic.h @@ -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) @@ -144,14 +144,7 @@ struct bfs_bar *bfs_bar_show(void) { return NULL; } - char term[L_ctermid]; - ctermid(term); - if (strlen(term) == 0) { - errno = ENOTTY; - goto fail; - } - - bar->fd = open(term, O_RDWR | O_CLOEXEC); + bar->fd = open_cterm(O_RDWR | O_CLOEXEC); if (bar->fd < 0) { goto fail; } diff --git a/src/bfstd.c b/src/bfstd.c index ce2218c..6d244ca 100644 --- a/src/bfstd.c +++ b/src/bfstd.c @@ -184,6 +184,16 @@ 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 @@ -199,6 +209,35 @@ const char *xgetprogname(void) { return cmd; } +int xstrtoll(const char *str, char **end, int base, long long *value) { + // strtoll() skips leading spaces, but we want to reject them + if (xisspace(str[0])) { + errno = EINVAL; + return -1; + } + + // If end is NULL, make sure the entire string is valid + bool entire = !end; + char *endp; + if (!end) { + end = &endp; + } + + errno = 0; + long long result = strtoll(str, end, base); + if (errno != 0) { + return -1; + } + + if (*end == str || (entire && **end != '\0')) { + errno = EINVAL; + return -1; + } + + *value = result; + return 0; +} + /** Compile and execute a regular expression for xrpmatch(). */ static int xrpregex(nl_item item, const char *response) { const char *pattern = nl_langinfo(item); diff --git a/src/bfstd.h b/src/bfstd.h index 8055c55..afb5282 100644 --- a/src/bfstd.h +++ b/src/bfstd.h @@ -158,6 +158,16 @@ 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> /** @@ -169,6 +179,23 @@ char *xgetdelim(FILE *file, char delim); const char *xgetprogname(void); /** + * Wrapper for strtoll() that forbids leading spaces. + * + * @param str + * The string to parse. + * @param end + * If non-NULL, will hold a pointer to the first invalid character. + * If NULL, the entire string must be valid. + * @param base + * The base for the conversion, or 0 to auto-detect. + * @param value + * Will hold the parsed integer value, on success. + * @return + * 0 on success, -1 on failure. + */ +int xstrtoll(const char *str, char **end, int base, long long *value); + +/** * Process a yes/no prompt. * * @return 1 for yes, 0 for no, and -1 for unknown. @@ -349,7 +376,7 @@ 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); @@ -1529,11 +1529,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 +1563,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 +1576,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 +1679,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 +1737,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; } } @@ -56,6 +56,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; @@ -12,7 +12,7 @@ #include <limits.h> #include <stdint.h> -#if __STDC_VERSION__ >= C23 +#if BFS_HAS_STDBIT_H # include <stdbit.h> #endif @@ -173,11 +173,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 @@ -222,25 +218,17 @@ 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) // 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 +261,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 +294,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 +302,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 +310,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 +321,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 +332,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 +370,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..701a89c 100644 --- a/src/color.c +++ b/src/color.c @@ -583,7 +583,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) { @@ -968,7 +968,7 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, if (path == ftwbuf->path) { if (ftwbuf->depth == 0) { at_fd = AT_FDCWD; - at_path = dstrndup(path, max); + at_path = dstrxdup(path, max); } else { // The parent must have existed to get here goto out; @@ -977,13 +977,13 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, // 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) { + at_path = dstrxdup(ftwbuf->path, ftwbuf->nameoff); + if (at_path && dstrxcat(&at_path, path, max) != 0) { ret = -1; goto out_path; } } else { - at_path = dstrndup(path, max); + at_path = dstrxdup(path, max); } } @@ -1206,7 +1206,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; @@ -229,9 +229,7 @@ static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) { error = errno; } - if (ctx_file->hook) { - sigunhook(ctx_file->hook); - } + sigunhook(ctx_file->hook); // Close the CFILE, except for stdio streams, which are closed later if (cfile != ctx->cout && cfile != ctx->cerr) { @@ -7,6 +7,7 @@ #include "prelude.h" #include "eval.h" +#include "atomic.h" #include "bar.h" #include "bfstd.h" #include "bftw.h" @@ -22,6 +23,7 @@ #include "printf.h" #include "pwcache.h" #include "sanity.h" +#include "sighook.h" #include "stat.h" #include "trie.h" #include "xregex.h" @@ -997,6 +999,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; @@ -1284,7 +1293,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); @@ -1373,6 +1382,11 @@ struct callback_args { struct bfs_bar *bar; /** The time of the last status update. */ struct timespec last_status; + /** SIGINFO hook. */ + struct sighook *info_hook; + /** Flag set by SIGINFO hook. */ + atomic bool info_flag; + /** The number of files visited so far. */ size_t count; @@ -1399,15 +1413,38 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) { state.ret = &args->ret; state.quit = false; + // Check whether SIGINFO was delivered and show/hide the bar + if (exchange(&args->info_flag, false, relaxed)) { + if (args->bar) { + bfs_bar_hide(args->bar); + args->bar = NULL; + } else { + args->bar = bfs_bar_show(); + if (!args->bar) { + bfs_warning(ctx, "Couldn't show status bar: %s.\n", errstr()); + } + } + } + if (args->bar) { eval_status(&state, args->bar, &args->last_status, 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; } @@ -1462,6 +1499,12 @@ done: return state.action; } +/** 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); +} + /** Raise RLIMIT_NOFILE if possible, and return the new limit. */ static int raise_fdlimit(struct bfs_ctx *ctx) { rlim_t cur = ctx->orig_nofile.rlim_cur; @@ -1624,6 +1667,13 @@ int bfs_eval(struct bfs_ctx *ctx) { } } +#ifdef SIGINFO + int siginfo = SIGINFO; +#else + int siginfo = SIGUSR1; +#endif + args.info_hook = sighook(siginfo, eval_siginfo, &args, SH_CONTINUE); + struct trie seen; if (ctx->unique) { trie_init(&seen); @@ -1691,6 +1741,7 @@ int bfs_eval(struct bfs_ctx *ctx) { trie_destroy(&seen); } + sigunhook(args.info_hook); bfs_bar_hide(args.bar); return args.ret; @@ -234,7 +234,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) { @@ -313,9 +313,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 +325,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; @@ -357,9 +358,8 @@ static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool bloc // 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 +378,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); } @@ -324,6 +324,25 @@ LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__)) /** + * Splice a singly-linked list into another. + * + * @param dest + * The destination list. + * @param cursor + * A pointer to the item to splice after, e.g. &list->head or list->tail. + * @param 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 @@ -332,10 +351,7 @@ * 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. @@ -41,6 +41,7 @@ * - 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) @@ -102,39 +102,20 @@ 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. @@ -242,29 +223,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 +300,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. */ @@ -503,7 +470,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 +491,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]; diff --git a/src/parse.c b/src/parse.c index 539aa05..86ce72c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -115,21 +115,28 @@ struct bfs_parser { }; /** - * Possible token types. + * Token types and flags. */ -enum token_type { +enum token_info { /** A flag. */ - T_FLAG, + T_FLAG = 1, /** A root path. */ - T_PATH, + T_PATH = 2, /** An option. */ - T_OPTION, + T_OPTION = 3, /** A test. */ - T_TEST, + T_TEST = 4, /** An action. */ - T_ACTION, + T_ACTION = 5, /** An operator. */ - T_OPERATOR, + T_OPERATOR = 6, + /** Mask for token types. */ + T_TYPE = (1 << 3) - 1, + + /** A token can match a prefix of an argument, like -On, -newerXY, etc. */ + T_PREFIX = 1 << 3, + /** A flag that takes an argument. */ + T_NEEDS_ARG = 1 << 4, }; /** @@ -160,7 +167,6 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, */ attr(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 +176,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); } @@ -180,7 +185,6 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ... */ attr(printf(4, 5)) static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -190,7 +194,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); } @@ -200,7 +203,6 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_ */ attr(printf(6, 7)) static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -211,7 +213,6 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -221,14 +222,12 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, */ attr(printf(3, 4)) static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bfs_expr_error(ctx, expr); va_list args; va_start(args, format); - errno = error; bfs_verror(ctx, format, args); va_end(args); } @@ -238,7 +237,6 @@ static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_e */ attr(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 +248,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; @@ -261,7 +258,6 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, . */ attr(printf(6, 7)) static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; bool highlight[ctx->argc]; @@ -274,7 +270,6 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1 va_list args; va_start(args, format); - errno = error; bool ret = bfs_vwarning(ctx, format, args); va_end(args); return ret; @@ -285,7 +280,6 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1 */ attr(printf(3, 4)) static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) { - int error = errno; const struct bfs_ctx *ctx = parser->ctx; if (!bfs_expr_warning(ctx, expr)) { @@ -294,7 +288,6 @@ 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; @@ -414,7 +407,9 @@ 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) { +static char **parser_advance(struct bfs_parser *parser, enum token_info type, size_t argc) { + bfs_assert(type == (type & T_TYPE)); + if (type != T_FLAG && type != T_PATH) { parser->expr_started = true; } @@ -517,20 +512,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 +527,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; } @@ -657,9 +639,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,6 +651,29 @@ 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) { @@ -811,7 +818,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 +828,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 +876,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; @@ -1613,13 +1622,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; } @@ -2253,16 +2263,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 +2298,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 +2352,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) { @@ -2928,12 +2958,23 @@ static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2 static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) { cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version); - printf("%s\n", BFS_HOMEPAGE); + printf("Copyright © Tavian Barnes and the bfs contributors\n"); + printf("No rights reserved (https://opensource.org/license/0BSD)\n\n"); + + printf("CONFFLAGS := %s\n", bfs_confflags); + printf("CC := %s\n", bfs_cc); + printf("CPPFLAGS := %s\n", bfs_cppflags); + printf("CFLAGS := %s\n", bfs_cflags); + printf("LDFLAGS := %s\n", bfs_ldflags); + printf("LDLIBS := %s\n", bfs_ldlibs); + + printf("\n%s\n", BFS_HOMEPAGE); parser->just_info = true; return NULL; } +/** Parser callback function type. */ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2); /** @@ -2941,11 +2982,10 @@ typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2) */ struct table_entry { char *arg; - enum token_type type; + enum token_info info; parse_fn *parse; int arg1; int arg2; - bool prefix; }; /** @@ -2959,13 +2999,13 @@ static const struct table_entry parse_table[] = { {"-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}, + {"-D", T_FLAG | T_PREFIX, 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}, + {"-O", T_FLAG | T_PREFIX, parse_optlevel}, {"-P", T_FLAG, parse_follow, 0, false}, - {"-S", T_FLAG, parse_search_strategy}, + {"-S", T_FLAG | T_PREFIX, parse_search_strategy}, {"-X", T_FLAG, parse_xargs_safe}, {"-a", T_OPERATOR}, {"-acl", T_TEST, parse_acl}, @@ -2991,7 +3031,7 @@ static const struct table_entry parse_table[] = { {"-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}, + {"-f", T_FLAG | T_NEEDS_ARG, parse_f}, {"-false", T_TEST, parse_const, false}, {"-files0-from", T_OPTION, parse_files0_from}, {"-flags", T_TEST, parse_flags}, @@ -3012,7 +3052,7 @@ static const struct table_entry parse_table[] = { {"-ipath", T_TEST, parse_path, true}, {"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE}, {"-iwholename", T_TEST, parse_path, true}, - {"-j", T_FLAG, parse_jobs, 0, 0, true}, + {"-j", T_FLAG | T_PREFIX, parse_jobs}, {"-limit", T_ACTION, parse_limit}, {"-links", T_TEST, parse_links}, {"-lname", T_TEST, parse_lname, false}, @@ -3026,7 +3066,7 @@ static const struct table_entry parse_table[] = { {"-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}, + {"-newer", T_TEST | T_PREFIX, parse_newerxy}, {"-nocolor", T_OPTION, parse_color, false}, {"-nogroup", T_TEST, parse_nogroup}, {"-nohidden", T_TEST, parse_nohidden}, @@ -3079,7 +3119,7 @@ static const struct table_entry parse_table[] = { static const struct table_entry *table_lookup(const char *arg) { for (const struct table_entry *entry = parse_table; entry->arg; ++entry) { bool match; - if (entry->prefix) { + if (entry->info & T_PREFIX) { match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0; } else { match = strcmp(arg, entry->arg) == 0; @@ -3092,6 +3132,85 @@ 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 token_info type = entry->info & T_TYPE; + if (type == T_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->info & T_PREFIX) { + // The rest is the flag's argument + break; + } + + if (entry->info & T_NEEDS_ARG) { + needs_arg = true; + } + } + + 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->info & T_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; @@ -3130,11 +3249,15 @@ 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; parse_error(parser, "Unknown argument; did you mean "); - switch (match->type) { + switch (match->info & T_TYPE) { case T_FLAG: cfprintf(cerr, "${cyn}%s${rs}?", match->arg); break; diff --git a/src/prelude.h b/src/prelude.h index 3521fe8..34ce803 100644 --- a/src/prelude.h +++ b/src/prelude.h @@ -40,6 +40,13 @@ // 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[]; + // Check for system headers #ifdef __has_include @@ -50,6 +57,9 @@ extern const char bfs_version[]; #if __has_include(<paths.h>) # define BFS_HAS_PATHS_H true #endif +#if __has_include(<stdbit.h>) +# define BFS_HAS_STDBIT_H true +#endif #if __has_include(<sys/extattr.h>) # define BFS_HAS_SYS_EXTATTR_H true #endif @@ -76,6 +86,7 @@ extern const char bfs_version[]; #define BFS_HAS_MNTENT_H __GLIBC__ #define BFS_HAS_PATHS_H true +#define BFS_HAS_STDBIT_H (__STDC_VERSION__ >= C23) #define BFS_HAS_SYS_EXTATTR_H __FreeBSD__ #define BFS_HAS_SYS_MKDEV_H false #define BFS_HAS_SYS_PARAM_H true @@ -133,7 +144,7 @@ extern const char bfs_version[]; /** * Get the length of an array. */ -#define countof(array) (sizeof(array) / sizeof(0[array])) +#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__])) /** * False sharing/destructive interference/largest cache line size. diff --git a/src/printf.c b/src/printf.c index be09ebd..6b07c54 100644 --- a/src/printf.c +++ b/src/printf.c @@ -505,30 +505,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 +539,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"; diff --git a/src/sighook.c b/src/sighook.c index ff5b96f..6d6ff01 100644 --- a/src/sighook.c +++ b/src/sighook.c @@ -52,6 +52,8 @@ 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; @@ -166,6 +168,8 @@ static void *RCU_NULL = &RCU_NULL; /** Initialize an RCU block. */ static void rcu_init(struct rcu *rcu) { + bfs_verify(atomic_is_lock_free(&rcu->active)); + atomic_init(&rcu->active, 0); arc_init(&rcu->slots[0]); arc_init(&rcu->slots[1]); @@ -323,11 +327,16 @@ static int rcu_sigtable_add(struct rcu *rcu, struct sighook *hook) { return 0; } -/** The global table of signal hooks. */ -static struct rcu rcu_sighooks; -/** The global table of atsigexit() hooks. */ +/** The sharded table of signal hooks. */ +static struct rcu rcu_sighooks[64]; +/** The table of atsigexit() hooks. */ static struct rcu rcu_exithooks; +/** Get the table for a particular signal. */ +static struct rcu *sigshard(int sig) { + return &rcu_sighooks[sig % countof(rcu_sighooks)]; +} + /** Mutex for initialization and RCU writer exclusion. */ static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER; @@ -475,7 +484,8 @@ 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 *shard = sigshard(sig); + enum sigflags flags = run_hooks(shard, sig, info); // Run the atsigexit() hooks, if we're exiting if (!(flags & SH_CONTINUE) && is_fatal(sig)) { @@ -501,8 +511,12 @@ static int siginit(int sig) { || sigemptyset(&action.sa_mask) != 0) { return -1; } - rcu_init(&rcu_sighooks); + + for (size_t i = 0; i < countof(rcu_sighooks); ++i) { + rcu_init(&rcu_sighooks[i]); + } rcu_init(&rcu_exithooks); + initialized = true; } @@ -552,7 +566,8 @@ struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) goto done; } - ret = sighook_impl(&rcu_sighooks, sig, fn, arg, flags); + struct rcu *shard = sigshard(sig); + ret = sighook_impl(shard, sig, fn, arg, flags); done: mutex_unlock(&sigmutex); return ret; @@ -561,32 +576,38 @@ 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(&rcu_exithooks, 0, fn, arg, 0); mutex_unlock(&sigmutex); return ret; } void sigunhook(struct sighook *hook) { + if (!hook) { + return; + } + mutex_lock(&sigmutex); - struct rcu *rcu = hook->sig ? &rcu_sighooks : &rcu_exithooks; + struct rcu *rcu; + if (hook->sig) { + rcu = sigshard(hook->sig); + } else { + rcu = &rcu_exithooks; + } + struct sigtable *table = rcu_peek(rcu); bfs_verify(sigtable_del(table, hook) == 0); @@ -51,7 +51,7 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) { return "modification time"; } - bfs_bug("Unrecognized stat field"); + bfs_bug("Unrecognized stat field %d", (int)field); return "???"; } @@ -621,7 +621,7 @@ static void trie_free_singletons(struct trie *trie, uintptr_t ptr) { struct trie_node *node = trie_decode_node(ptr); // Make sure the bitmap is a power of two, i.e. it has just one child - 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); @@ -125,7 +125,7 @@ int typo_distance(const char *actual, const char *expected) { // This is the Wagner-Fischer algorithm for Levenshtein distance, using // Manhattan distance on the keyboard for individual characters. - const int insert_cost = 12; + const int insert_cost = (40 + 12 + 1) / 2; size_t rows = strlen(actual) + 1; size_t cols = strlen(expected) + 1; diff --git a/src/version.c b/src/version.c new file mode 100644 index 0000000..8383350 --- /dev/null +++ b/src/version.c @@ -0,0 +1,32 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.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..2d089b2 100644 --- a/src/xregex.c +++ b/src/xregex.c @@ -37,6 +37,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 +109,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 +178,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..750db24 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, }; /** diff --git a/src/xspawn.c b/src/xspawn.c index 33e5a4a..2c64011 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -426,8 +426,17 @@ static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const st }; if (bfs_can_skip_resolve(res, ctx)) { - res->done = true; - return 0; + // 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) { + res->done = true; + return 0; + } else { + return -1; + } } res->path = getenv("PATH"); diff --git a/src/xtime.c b/src/xtime.c index 2808455..186651b 100644 --- a/src/xtime.c +++ b/src/xtime.c @@ -206,6 +206,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 +341,7 @@ end: } } +done: result->tv_nsec = 0; return 0; diff --git a/tests/bfs/Dmulti.out b/tests/bfs/Dmulti.out new file mode 100644 index 0000000..a7ccfe4 --- /dev/null +++ b/tests/bfs/Dmulti.out @@ -0,0 +1,19 @@ +basic +basic/a +basic/b +basic/c +basic/c/d +basic/e +basic/e/f +basic/g +basic/g/h +basic/i +basic/j +basic/j/foo +basic/k +basic/k/foo +basic/k/foo/bar +basic/l +basic/l/foo +basic/l/foo/bar +basic/l/foo/bar/baz diff --git a/tests/bfs/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/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_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_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/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/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/bit.c b/tests/bit.c index 674d1b2..49e167d 100644 --- a/tests/bit.c +++ b/tests/bit.c @@ -76,15 +76,15 @@ bool check_bit(void) { 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_ones(0x0U), 0); + ret &= check_eq(count_ones(0x1U), 1); + ret &= check_eq(count_ones(0x2U), 1); + ret &= check_eq(count_ones(0x3U), 2); + ret &= check_eq(count_ones(0x137FU), 10); + + ret &= check_eq(count_zeros(0U), UINT_WIDTH); + ret &= check_eq(count_zeros(0UL), ULONG_WIDTH); + ret &= check_eq(count_zeros(0ULL), ULLONG_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); @@ -100,16 +100,16 @@ bool check_bit(void) { 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) { + 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_leading_one(nm), 16 - j); ret &= check_eq(first_trailing_one(nm), i + 1); ret &= check_eq(bit_width(nm), j + 1); ret &= check_eq(bit_floor(nm), m); @@ -127,13 +127,13 @@ bool check_bit(void) { 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 &= check_eq(first_leading_one(0U), 0); + ret &= check_eq(first_trailing_one(0U), 0); + ret &= check_eq(bit_width(0U), 0); + ret &= check_eq(bit_floor(0U), 0); + ret &= check_eq(bit_ceil(0U), 1); - ret &= bfs_check(!has_single_bit(0)); + ret &= bfs_check(!has_single_bit(0U)); ret &= bfs_check(!has_single_bit(UINT32_MAX)); ret &= 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..42f2fed 100644 --- a/tests/bsd/perm_000_plus.out +++ b/tests/bsd/perm_000_plus.out @@ -1,8 +1,10 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f--- +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out index 9a5b95a..5c78ecc 100644 --- a/tests/bsd/perm_222_plus.out +++ b/tests/bsd/perm_222_plus.out @@ -1,5 +1,6 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx +perms/f-w- +perms/f-wx +perms/frw- +perms/frwx diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out index 7e5ae98..774c0ea 100644 --- a/tests/bsd/perm_644_plus.out +++ b/tests/bsd/perm_644_plus.out @@ -1,7 +1,9 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx 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/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/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..c4d53fb --- /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/atime.out b/tests/common/atime.out new file mode 100644 index 0000000..5ed206b --- /dev/null +++ b/tests/common/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/common/atime.sh b/tests/common/atime.sh new file mode 100644 index 0000000..c2e4031 --- /dev/null +++ b/tests/common/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 -mindepth 1 \ + \( -atime -1 -exec printf -- '-atime -1: %s\n' {} \; -o -true \) \ + \( -atime 1 -exec printf -- '-atime 1: %s\n' {} \; -o -true \) \ + \( -atime +1 -exec printf -- '-atime +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/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..38e3337 --- /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/mtime.out b/tests/common/mtime.out new file mode 100644 index 0000000..91f0114 --- /dev/null +++ b/tests/common/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/common/mtime.sh b/tests/common/mtime.sh new file mode 100644 index 0000000..7ba127e --- /dev/null +++ b/tests/common/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 -mindepth 1 \ + \( -mtime -1 -exec printf -- '-mtime -1: %s\n' {} \; -o -true \) \ + \( -mtime 1 -exec printf -- '-mtime 1: %s\n' {} \; -o -true \) \ + \( -mtime +1 -exec printf -- '-mtime +1: %s\n' {} \; -o -true \) diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out index 49c1b21..08965bf 100644 --- a/tests/gnu/executable.out +++ b/tests/gnu/executable.out @@ -1,4 +1,6 @@ perms -perms/rwx -perms/rx -perms/wx +perms/dr-x +perms/drwx +perms/f-wx +perms/fr-x +perms/frwx 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_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/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/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/perm_000_slash.out b/tests/gnu/perm_000_slash.out index d7494b8..42f2fed 100644 --- a/tests/gnu/perm_000_slash.out +++ b/tests/gnu/perm_000_slash.out @@ -1,8 +1,10 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f--- +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out index 9a5b95a..5c78ecc 100644 --- a/tests/gnu/perm_222_slash.out +++ b/tests/gnu/perm_222_slash.out @@ -1,5 +1,6 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx +perms/f-w- +perms/f-wx +perms/frw- +perms/frwx diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out index 7e5ae98..774c0ea 100644 --- a/tests/gnu/perm_644_slash.out +++ b/tests/gnu/perm_644_slash.out @@ -1,7 +1,9 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out index 7e5ae98..774c0ea 100644 --- a/tests/gnu/perm_leading_plus_symbolic_slash.out +++ b/tests/gnu/perm_leading_plus_symbolic_slash.out @@ -1,7 +1,9 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out index 7e5ae98..774c0ea 100644 --- a/tests/gnu/perm_symbolic_slash.out +++ b/tests/gnu/perm_symbolic_slash.out @@ -1,7 +1,9 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out index 386feba..285aa43 100644 --- a/tests/gnu/readable.out +++ b/tests/gnu/readable.out @@ -1,5 +1,7 @@ perms -perms/r -perms/rw -perms/rwx -perms/rx +perms/dr-x +perms/drwx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx 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..5c78ecc 100644 --- a/tests/gnu/writable.out +++ b/tests/gnu/writable.out @@ -1,5 +1,6 @@ perms -perms/rw -perms/rwx -perms/w -perms/wx +perms/drwx +perms/f-w- +perms/f-wx +perms/frw- +perms/frwx 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/list.c b/tests/list.c new file mode 100644 index 0000000..e14570f --- /dev/null +++ b/tests/list.c @@ -0,0 +1,97 @@ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD + +#include "prelude.h" +#include "tests.h" +#include "list.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 + +bool 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))); + + return true; +} diff --git a/tests/main.c b/tests/main.c index aef0583..bef2e37 100644 --- a/tests/main.c +++ b/tests/main.c @@ -111,6 +111,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/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/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/perm_000.out b/tests/posix/perm_000.out index 5fd30bc..b46af62 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..42f2fed 100644 --- a/tests/posix/perm_000_minus.out +++ b/tests/posix/perm_000_minus.out @@ -1,8 +1,10 @@ perms -perms/0 -perms/r -perms/rw -perms/rwx -perms/rx -perms/w -perms/wx +perms/dr-x +perms/drwx +perms/f--- +perms/f-w- +perms/f-wx +perms/fr-- +perms/fr-x +perms/frw- +perms/frwx diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out index 1690e43..4876193 100644 --- a/tests/posix/perm_222.out +++ b/tests/posix/perm_222.out @@ -1 +1 @@ -perms/w +perms/f-w- diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out index 1690e43..4876193 100644 --- a/tests/posix/perm_222_minus.out +++ b/tests/posix/perm_222_minus.out @@ -1 +1 @@ -perms/w +perms/f-w- diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out index 4e64e49..4598cc1 100644 --- a/tests/posix/perm_644.out +++ b/tests/posix/perm_644.out @@ -1 +1 @@ -perms/rw +perms/frw- diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out index 2e2576b..9e041c3 100644 --- a/tests/posix/perm_644_minus.out +++ b/tests/posix/perm_644_minus.out @@ -1,3 +1,4 @@ perms -perms/rw -perms/rwx +perms/drwx +perms/frw- +perms/frwx diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out index 2e2576b..9e041c3 100644 --- a/tests/posix/perm_symbolic_minus.out +++ b/tests/posix/perm_symbolic_minus.out @@ -1,3 +1,4 @@ perms -perms/rw -perms/rwx +perms/drwx +perms/frw- +perms/frwx diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out index 4e64e49..4598cc1 100644 --- a/tests/posix/permcopy.out +++ b/tests/posix/permcopy.out @@ -1 +1 @@ -perms/rw +perms/frw- 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/run.sh b/tests/run.sh index 381b03e..115a036 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -410,14 +410,20 @@ make_xattrs() { esac } +# Get the Unix epoch time in seconds +epoch_time() { + # https://stackoverflow.com/a/12746260/502399 + awk 'BEGIN { srand(); print srand(); }' +} + ## Snapshot testing # Return value when a difference is detected EX_DIFF=20 # Detect colored diff support -if diff --color /dev/null /dev/null &>/dev/null; then - DIFF="diff --color" +if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then + DIFF="diff --color=always" else DIFF="diff" fi diff --git a/tests/stddirs.sh b/tests/stddirs.sh index e08e6bf..b3cd521 100644 --- a/tests/stddirs.sh +++ b/tests/stddirs.sh @@ -14,13 +14,13 @@ 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 -M444 "$1/fr--" + "$XTOUCH" -p -M222 "$1/f-w-" + "$XTOUCH" -p -M644 "$1/frw-" + "$XTOUCH" -p -M311 "$1/f-wx" + "$XTOUCH" -p -M555 "$1/fr-x" "$1/dr-x/" + "$XTOUCH" -p -M755 "$1/frwx" "$1/drwx/" } # Creates a file+directory structure with various symbolic and hard links @@ -48,6 +48,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 +77,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 @@ -133,6 +142,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 +158,6 @@ clean_stddirs() { fi done + chmod -R +rwX "$TMP" rm -rf "$TMP" } diff --git a/tests/tests.h b/tests/tests.h index de95a49..2958fe1 100644 --- a/tests/tests.h +++ b/tests/tests.h @@ -27,6 +27,9 @@ bool check_bit(void); /** I/O queue tests. */ bool check_ioq(void); +/** Linked list tests. */ +bool check_list(void); + /** Signal hook tests. */ bool check_sighook(void); diff --git a/tests/trie.c b/tests/trie.c index 4667322..ebaae5d 100644 --- a/tests/trie.c +++ b/tests/trie.c @@ -8,7 +8,7 @@ #include <stdlib.h> #include <string.h> -const char *keys[] = { +static const char *keys[] = { "foo", "bar", "baz", @@ -37,7 +37,7 @@ const char *keys[] = { ">>>", }; -const size_t nkeys = countof(keys); +static const size_t nkeys = countof(keys); bool check_trie(void) { bool ret = true; diff --git a/tests/xspawn.c b/tests/xspawn.c index b1d6dc1..f48e220 100644 --- a/tests/xspawn.c +++ b/tests/xspawn.c @@ -179,6 +179,8 @@ static bool check_resolve(void) { ret &= bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT); + ret &= bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT); + return ret; } |