summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/codecov.yml4
-rw-r--r--.github/codeql.yml13
-rw-r--r--.github/dependabot.yml6
-rwxr-xr-x.github/diag.sh20
-rw-r--r--.github/workflows/ci.yml255
-rw-r--r--.github/workflows/codecov.yml34
-rw-r--r--.github/workflows/codeql.yml60
-rw-r--r--.github/workflows/freebsd.yml34
-rw-r--r--.github/workflows/linux.yml35
-rw-r--r--.github/workflows/macos.yml14
-rw-r--r--.gitignore13
-rw-r--r--LICENSE19
-rw-r--r--Makefile533
-rw-r--r--README.md341
-rw-r--r--RELEASES.md559
-rw-r--r--bar.c253
-rw-r--r--bar.h57
-rw-r--r--bench/.gitignore3
-rw-r--r--bench/README.md51
-rw-r--r--bench/bench.sh749
-rwxr-xr-xbench/clone-tree.sh143
-rw-r--r--bench/ioq.c455
-rw-r--r--bfs.h32
-rw-r--r--bftw.c1557
-rwxr-xr-xbuild/cc.sh34
-rw-r--r--build/config.mk51
-rwxr-xr-xbuild/define-if.sh18
-rwxr-xr-xbuild/embed.sh12
-rw-r--r--build/empty.c6
-rw-r--r--build/exports.mk20
-rwxr-xr-xbuild/flags-if.sh28
-rw-r--r--build/flags.mk136
-rw-r--r--build/flags/Wformat.c9
-rw-r--r--build/flags/Wimplicit-fallthrough.c9
-rw-r--r--build/flags/Wimplicit.c9
-rw-r--r--build/flags/Wmissing-decls.c9
-rw-r--r--build/flags/Wmissing-var-decls.c9
-rw-r--r--build/flags/Wshadow.c9
-rw-r--r--build/flags/Wsign-compare.c9
-rw-r--r--build/flags/Wstrict-prototypes.c9
-rw-r--r--build/flags/Wundef-prefix.c9
-rw-r--r--build/flags/bind-now.c8
-rw-r--r--build/flags/deps.c8
-rw-r--r--build/flags/pthread.c8
-rw-r--r--build/has/--st-birthtim.c9
-rw-r--r--build/has/_Fork.c8
-rw-r--r--build/has/acl-get-entry.c11
-rw-r--r--build/has/acl-get-file.c11
-rw-r--r--build/has/acl-get-tag-type.c13
-rw-r--r--build/has/acl-is-trivial-np.c12
-rw-r--r--build/has/acl-trivial.c8
-rw-r--r--build/has/builtin-riscv-pause.c7
-rw-r--r--build/has/confstr.c9
-rw-r--r--build/has/dprintf.c8
-rw-r--r--build/has/extattr-get-file.c10
-rw-r--r--build/has/extattr-get-link.c10
-rw-r--r--build/has/extattr-list-file.c10
-rw-r--r--build/has/extattr-list-link.c10
-rw-r--r--build/has/fdclosedir.c8
-rw-r--r--build/has/getdents.c9
-rw-r--r--build/has/getdents64-syscall.c11
-rw-r--r--build/has/getdents64.c9
-rw-r--r--build/has/getmntent-1.c9
-rw-r--r--build/has/getmntent-2.c10
-rw-r--r--build/has/getmntinfo.c10
-rw-r--r--build/has/getprogname-gnu.c9
-rw-r--r--build/has/getprogname.c9
-rw-r--r--build/has/io-uring-max-workers.c11
-rw-r--r--build/has/pipe2.c10
-rw-r--r--build/has/posix-getdents.c9
-rw-r--r--build/has/posix-spawn-addfchdir-np.c11
-rw-r--r--build/has/posix-spawn-addfchdir.c11
-rw-r--r--build/has/pragma-nounroll.c10
-rw-r--r--build/has/pthread-set-name-np.c10
-rw-r--r--build/has/pthread-setname-np.c8
-rw-r--r--build/has/sched-getaffinity.c9
-rw-r--r--build/has/st-acmtim.c12
-rw-r--r--build/has/st-acmtimespec.c12
-rw-r--r--build/has/st-birthtim.c9
-rw-r--r--build/has/st-birthtimespec.c9
-rw-r--r--build/has/st-flags.c9
-rw-r--r--build/has/statx-syscall.c13
-rw-r--r--build/has/statx.c11
-rw-r--r--build/has/strerror-l.c11
-rw-r--r--build/has/strerror-r-gnu.c11
-rw-r--r--build/has/strerror-r-posix.c11
-rw-r--r--build/has/string-to-flags.c9
-rw-r--r--build/has/strtofflags.c9
-rw-r--r--build/has/tcgetwinsize.c9
-rw-r--r--build/has/tcsetwinsize.c9
-rw-r--r--build/has/timegm.c9
-rw-r--r--build/has/timer-create.c9
-rw-r--r--build/has/tm-gmtoff.c9
-rw-r--r--build/has/uselocale.c9
-rw-r--r--build/header.mk93
-rwxr-xr-xbuild/msg-if.sh31
-rwxr-xr-xbuild/msg.sh62
-rwxr-xr-xbuild/pkgconf.sh96
-rw-r--r--build/pkgs.mk33
-rw-r--r--build/prelude.mk68
-rwxr-xr-xbuild/version.sh18
-rw-r--r--build/with/libacl.c9
-rw-r--r--build/with/libcap.c9
-rw-r--r--build/with/libselinux.c9
-rw-r--r--build/with/liburing.c9
-rw-r--r--build/with/oniguruma.c9
-rw-r--r--color.c1121
-rw-r--r--color.h127
-rw-r--r--completions/bfs.bash27
-rw-r--r--completions/bfs.fish148
-rw-r--r--completions/bfs.zsh176
-rwxr-xr-xconfigure237
-rw-r--r--ctx.c284
-rw-r--r--darray.c103
-rw-r--r--darray.h110
-rw-r--r--diag.c104
-rw-r--r--diag.h86
-rw-r--r--dir.c303
-rw-r--r--dir.h124
-rw-r--r--docs/BUILDING.md201
-rw-r--r--docs/CHANGELOG.md1238
-rw-r--r--docs/CONTRIBUTING.md61
-rw-r--r--docs/RELATED.md43
-rw-r--r--docs/SECURITY.md126
-rw-r--r--docs/USAGE.md192
-rw-r--r--docs/bfs.1 (renamed from bfs.1)429
-rw-r--r--dstring.c215
-rw-r--r--dstring.h194
-rw-r--r--eval.h113
-rw-r--r--expr.h207
-rwxr-xr-xflags.sh11
-rw-r--r--fsade.h83
-rw-r--r--mtab.c246
-rw-r--r--mtab.h71
-rw-r--r--opt.c1051
-rw-r--r--opt.h37
-rw-r--r--parse.c3787
-rw-r--r--parse.h36
-rw-r--r--printf.c881
-rw-r--r--printf.h65
-rw-r--r--pwcache.c293
-rw-r--r--pwcache.h117
-rw-r--r--spawn.c321
-rw-r--r--spawn.h123
-rw-r--r--src/alloc.c382
-rw-r--r--src/alloc.h401
-rw-r--r--src/atomic.h118
-rw-r--r--src/bar.c220
-rw-r--r--src/bar.h44
-rw-r--r--src/bfs.h241
-rw-r--r--src/bfstd.c1270
-rw-r--r--src/bfstd.h619
-rw-r--r--src/bftw.c2344
-rw-r--r--src/bftw.h (renamed from bftw.h)72
-rw-r--r--src/bit.h473
-rw-r--r--src/color.c1548
-rw-r--r--src/color.h120
-rw-r--r--src/ctx.c295
-rw-r--r--src/ctx.h (renamed from ctx.h)143
-rw-r--r--src/diag.c291
-rw-r--r--src/diag.h265
-rw-r--r--src/dir.c373
-rw-r--r--src/dir.h178
-rw-r--r--src/dstring.c308
-rw-r--r--src/dstring.h355
-rw-r--r--src/eval.c (renamed from eval.c)1055
-rw-r--r--src/eval.h100
-rw-r--r--src/exec.c (renamed from exec.c)271
-rw-r--r--src/exec.h (renamed from exec.h)31
-rw-r--r--src/expr.c89
-rw-r--r--src/expr.h279
-rw-r--r--src/fsade.c (renamed from fsade.c)275
-rw-r--r--src/fsade.h85
-rw-r--r--src/ioq.c1330
-rw-r--r--src/ioq.h227
-rw-r--r--src/list.h613
-rw-r--r--src/main.c (renamed from main.c)73
-rw-r--r--src/mtab.c304
-rw-r--r--src/mtab.h56
-rw-r--r--src/opt.c2357
-rw-r--r--src/opt.h23
-rw-r--r--src/parse.c3978
-rw-r--r--src/parse.h23
-rw-r--r--src/prelude.h130
-rw-r--r--src/printf.c965
-rw-r--r--src/printf.h55
-rw-r--r--src/pwcache.c219
-rw-r--r--src/pwcache.h124
-rw-r--r--src/sanity.h94
-rw-r--r--src/sighook.c692
-rw-r--r--src/sighook.h83
-rw-r--r--src/stat.c376
-rw-r--r--src/stat.h (renamed from stat.h)121
-rw-r--r--src/thread.c94
-rw-r--r--src/thread.h95
-rw-r--r--src/trie.c782
-rw-r--r--src/trie.h (renamed from trie.h)164
-rw-r--r--src/typo.c (renamed from typo.c)23
-rw-r--r--src/typo.h18
-rw-r--r--src/version.c32
-rw-r--r--src/xregex.c344
-rw-r--r--src/xregex.h87
-rw-r--r--src/xspawn.c778
-rw-r--r--src/xspawn.h139
-rw-r--r--src/xtime.c503
-rw-r--r--src/xtime.h108
-rw-r--r--stat.c375
-rwxr-xr-xtests.sh3132
-rw-r--r--tests/alloc.c78
-rw-r--r--tests/bfs/D_all.out (renamed from tests/test_S_dfs.out)0
-rw-r--r--tests/bfs/D_all.sh1
-rw-r--r--tests/bfs/D_incomplete.sh1
-rw-r--r--tests/bfs/D_multi.out (renamed from tests/test_fprint.out)0
-rw-r--r--tests/bfs/D_multi.sh1
-rw-r--r--tests/bfs/D_opt.out (renamed from tests/test_type_f.out)0
-rw-r--r--tests/bfs/D_opt.sh1
-rw-r--r--tests/bfs/D_unknown.out (renamed from tests/test_O1.out)12
-rw-r--r--tests/bfs/D_unknown.sh4
-rw-r--r--tests/bfs/Dmulti.out (renamed from tests/test_O2.out)12
-rw-r--r--tests/bfs/Dmulti.sh1
-rw-r--r--tests/bfs/LD_stat.out (renamed from tests/test_L.out)12
-rw-r--r--tests/bfs/LD_stat.sh1
-rw-r--r--tests/bfs/LDstat.out (renamed from tests/test_L_depth.out)12
-rw-r--r--tests/bfs/LDstat.sh1
-rw-r--r--tests/bfs/L_capable.out2
-rw-r--r--tests/bfs/L_capable.sh10
-rw-r--r--tests/bfs/L_noerror.out (renamed from tests/test_L_loops_continue.out)6
-rw-r--r--tests/bfs/L_noerror.sh1
-rw-r--r--tests/bfs/L_unique.out (renamed from tests/test_L_unique.out)0
-rw-r--r--tests/bfs/L_unique.sh1
-rw-r--r--tests/bfs/L_unique_depth.out (renamed from tests/test_L_unique_depth.out)0
-rw-r--r--tests/bfs/L_unique_depth.sh1
-rw-r--r--tests/bfs/L_unique_loops.out (renamed from tests/test_L_unique_loops.out)0
-rw-r--r--tests/bfs/L_unique_loops.sh1
-rw-r--r--tests/bfs/O0.out (renamed from tests/test_O3.out)12
-rw-r--r--tests/bfs/O0.sh1
-rw-r--r--tests/bfs/O1.out (renamed from tests/test_Ofast.out)12
-rw-r--r--tests/bfs/O1.sh1
-rw-r--r--tests/bfs/O2.out19
-rw-r--r--tests/bfs/O2.sh1
-rw-r--r--tests/bfs/O3.out19
-rw-r--r--tests/bfs/O3.sh1
-rw-r--r--tests/bfs/O9.out19
-rw-r--r--tests/bfs/O9.sh4
-rw-r--r--tests/bfs/O_3.sh1
-rw-r--r--tests/bfs/Ofast.out19
-rw-r--r--tests/bfs/Ofast.sh1
-rw-r--r--tests/bfs/S_bfs.out (renamed from tests/test_D_all.out)0
-rw-r--r--tests/bfs/S_bfs.sh2
-rw-r--r--tests/bfs/S_dfs.out19
-rw-r--r--tests/bfs/S_dfs.sh2
-rw-r--r--tests/bfs/S_ids.out (renamed from tests/test_D_multi.out)0
-rw-r--r--tests/bfs/S_ids.sh2
-rw-r--r--tests/bfs/Sbfs.out (renamed from tests/test_O0.out)0
-rw-r--r--tests/bfs/Sbfs.sh2
-rw-r--r--tests/bfs/and_incomplete.sh1
-rw-r--r--tests/bfs/capable.out1
-rw-r--r--tests/bfs/capable.sh10
-rw-r--r--tests/bfs/closed_stderr.sh4
-rw-r--r--tests/bfs/closed_stdin.out19
-rw-r--r--tests/bfs/closed_stdin.sh1
-rw-r--r--tests/bfs/closed_stdout.sh4
-rw-r--r--tests/bfs/color.out (renamed from tests/test_color.out)13
-rw-r--r--tests/bfs/color.sh1
-rw-r--r--tests/bfs/color_L.out (renamed from tests/test_color_L.out)13
-rw-r--r--tests/bfs/color_L.sh1
-rw-r--r--tests/bfs/color_L_ln_target.out (renamed from tests/test_color_ln_target.out)13
-rw-r--r--tests/bfs/color_L_ln_target.sh1
-rw-r--r--tests/bfs/color_L_no_stat.out (renamed from tests/test_color_L_no_stat.out)19
-rw-r--r--tests/bfs/color_L_no_stat.sh1
-rw-r--r--tests/bfs/color_auto.out (renamed from tests/test_color_ext_override.out)13
-rw-r--r--tests/bfs/color_auto.sh4
-rw-r--r--tests/bfs/color_ca.out4
-rw-r--r--tests/bfs/color_ca.sh10
-rw-r--r--tests/bfs/color_ca_incapable.out (renamed from tests/test_color_ext_underride.out)13
-rw-r--r--tests/bfs/color_ca_incapable.sh1
-rw-r--r--tests/bfs/color_cd0_no.out27
-rw-r--r--tests/bfs/color_cd0_no.sh1
-rw-r--r--tests/bfs/color_deep.out16
-rw-r--r--tests/bfs/color_deep.sh7
-rw-r--r--tests/bfs/color_escapes.out (renamed from tests/test_color_escapes.out)13
-rw-r--r--tests/bfs/color_escapes.sh1
-rw-r--r--tests/bfs/color_ext.out (renamed from tests/test_color_missing_colon.out)13
-rw-r--r--tests/bfs/color_ext.sh1
-rw-r--r--tests/bfs/color_ext0.out (renamed from tests/test_color_ext0.out)13
-rw-r--r--tests/bfs/color_ext0.sh1
-rw-r--r--tests/bfs/color_ext_case.out27
-rw-r--r--tests/bfs/color_ext_case.sh6
-rw-r--r--tests/bfs/color_ext_case_flipflop.out27
-rw-r--r--tests/bfs/color_ext_case_flipflop.sh1
-rw-r--r--tests/bfs/color_ext_case_nul.out27
-rw-r--r--tests/bfs/color_ext_case_nul.sh5
-rw-r--r--tests/bfs/color_ext_case_priority.out27
-rw-r--r--tests/bfs/color_ext_case_priority.sh1
-rw-r--r--tests/bfs/color_ext_override.out27
-rw-r--r--tests/bfs/color_ext_override.sh1
-rw-r--r--tests/bfs/color_ext_underride.out27
-rw-r--r--tests/bfs/color_ext_underride.sh1
-rw-r--r--tests/bfs/color_fi0_no.out (renamed from tests/test_color_mh0.out)13
-rw-r--r--tests/bfs/color_fi0_no.sh1
-rw-r--r--tests/bfs/color_fi_no.out27
-rw-r--r--tests/bfs/color_fi_no.sh1
-rw-r--r--tests/bfs/color_ln_target.out (renamed from tests/test_color_L_ln_target.out)13
-rw-r--r--tests/bfs/color_ln_target.sh1
-rw-r--r--tests/bfs/color_ls.out12
-rw-r--r--tests/bfs/color_ls.sh15
-rw-r--r--tests/bfs/color_mh.out (renamed from tests/test_color_mh.out)13
-rw-r--r--tests/bfs/color_mh.sh1
-rw-r--r--tests/bfs/color_mh0.out27
-rw-r--r--tests/bfs/color_mh0.sh1
-rw-r--r--tests/bfs/color_mi.out27
-rw-r--r--tests/bfs/color_mi.sh1
-rw-r--r--tests/bfs/color_missing_colon.out (renamed from tests/test_color_ext.out)13
-rw-r--r--tests/bfs/color_missing_colon.sh1
-rw-r--r--tests/bfs/color_no.out27
-rw-r--r--tests/bfs/color_no.sh1
-rw-r--r--tests/bfs/color_no_stat.out (renamed from tests/test_color_no_stat.out)19
-rw-r--r--tests/bfs/color_no_stat.sh1
-rw-r--r--tests/bfs/color_notdir_slash_error.out (renamed from tests/test_L_ilname.out)0
-rw-r--r--tests/bfs/color_notdir_slash_error.sh2
-rw-r--r--tests/bfs/color_nul.out27
-rw-r--r--tests/bfs/color_nul.sh3
-rw-r--r--tests/bfs/color_or.out (renamed from tests/test_color_or.out)13
-rw-r--r--tests/bfs/color_or.sh1
-rw-r--r--tests/bfs/color_or0_mi.out27
-rw-r--r--tests/bfs/color_or0_mi.sh1
-rw-r--r--tests/bfs/color_or0_mi0.out27
-rw-r--r--tests/bfs/color_or0_mi0.sh1
-rw-r--r--tests/bfs/color_or_mi.out (renamed from tests/test_color_or_mi.out)13
-rw-r--r--tests/bfs/color_or_mi.sh1
-rw-r--r--tests/bfs/color_or_mi0.out (renamed from tests/test_color_or_mi0.out)13
-rw-r--r--tests/bfs/color_or_mi0.sh1
-rw-r--r--tests/bfs/color_rs_lc_rc_ec.out (renamed from tests/test_color_rs_lc_rc_ec.out)15
-rw-r--r--tests/bfs/color_rs_lc_rc_ec.sh1
-rw-r--r--tests/bfs/color_st0_tw0_ow.out (renamed from tests/test_color_st0_tw0_ow.out)15
-rw-r--r--tests/bfs/color_st0_tw0_ow.sh1
-rw-r--r--tests/bfs/color_st0_tw0_ow0.out27
-rw-r--r--tests/bfs/color_st0_tw0_ow0.sh1
-rw-r--r--tests/bfs/color_st0_tw_ow.out (renamed from tests/test_color_st0_tw_ow.out)15
-rw-r--r--tests/bfs/color_st0_tw_ow.sh1
-rw-r--r--tests/bfs/color_st0_tw_ow0.out (renamed from tests/test_color_st0_tw_ow0.out)17
-rw-r--r--tests/bfs/color_st0_tw_ow0.sh1
-rw-r--r--tests/bfs/color_st_tw0_ow.out (renamed from tests/test_color_st_tw0_ow.out)13
-rw-r--r--tests/bfs/color_st_tw0_ow.sh1
-rw-r--r--tests/bfs/color_st_tw0_ow0.out (renamed from tests/test_color_st_tw0_ow0.out)15
-rw-r--r--tests/bfs/color_st_tw0_ow0.sh1
-rw-r--r--tests/bfs/color_st_tw_ow0.out (renamed from tests/test_color_st_tw_ow0.out)15
-rw-r--r--tests/bfs/color_st_tw_ow0.sh1
-rw-r--r--tests/bfs/color_star.sh2
-rw-r--r--tests/bfs/color_su0_sg.out (renamed from tests/test_color_su0_sg.out)13
-rw-r--r--tests/bfs/color_su0_sg.sh1
-rw-r--r--tests/bfs/color_su0_sg0.out (renamed from tests/test_color_su0_sg0.out)13
-rw-r--r--tests/bfs/color_su0_sg0.sh1
-rw-r--r--tests/bfs/color_su_sg0.out (renamed from tests/test_color_su_sg0.out)13
-rw-r--r--tests/bfs/color_su_sg0.sh1
-rw-r--r--tests/bfs/comma_incomplete.sh1
-rw-r--r--tests/bfs/data_flow_hidden.out19
-rw-r--r--tests/bfs/data_flow_hidden.sh1
-rw-r--r--tests/bfs/deep_strict.out (renamed from tests/test_deep.out)0
-rw-r--r--tests/bfs/deep_strict.sh3
-rw-r--r--tests/bfs/exclude_depth.out (renamed from tests/test_not_prune.out)6
-rw-r--r--tests/bfs/exclude_depth.sh1
-rw-r--r--tests/bfs/exclude_exclude.sh1
-rw-r--r--tests/bfs/exclude_mindepth.out (renamed from tests/test_L_lname.out)0
-rw-r--r--tests/bfs/exclude_mindepth.sh1
-rw-r--r--tests/bfs/exclude_name.out (renamed from tests/test_prune_or_print.out)6
-rw-r--r--tests/bfs/exclude_name.sh1
-rw-r--r--tests/bfs/exclude_print.sh1
-rw-r--r--tests/bfs/exec_flush_fprint.out (renamed from tests/test_name_root.out)0
-rw-r--r--tests/bfs/exec_flush_fprint.sh2
-rw-r--r--tests/bfs/exec_flush_fprint_fail.sh2
-rw-r--r--tests/bfs/execdir_path_relative_slash.out (renamed from tests/test_execdir.out)0
-rw-r--r--tests/bfs/execdir_path_relative_slash.sh1
-rw-r--r--tests/bfs/execdir_plus.out (renamed from tests/test_execdir_plus.out)2
-rw-r--r--tests/bfs/execdir_plus.sh4
-rw-r--r--tests/bfs/execdir_plus_nonexistent.out19
-rw-r--r--tests/bfs/execdir_plus_nonexistent.sh2
-rw-r--r--tests/bfs/expr_flag_path.out (renamed from tests/test_H_type_l.out)0
-rw-r--r--tests/bfs/expr_flag_path.sh1
-rw-r--r--tests/bfs/expr_path_flag.out (renamed from tests/test_expr_flag_path.out)0
-rw-r--r--tests/bfs/expr_path_flag.sh1
-rw-r--r--tests/bfs/files0_from_root.sh2
-rw-r--r--tests/bfs/flag_expr_path.out (renamed from tests/test_expr_path_flag.out)0
-rw-r--r--tests/bfs/flag_expr_path.sh1
-rw-r--r--tests/bfs/fprint_duplicate_stdout.out (renamed from tests/test_fprint_duplicate_stdout.out)0
-rw-r--r--tests/bfs/fprint_duplicate_stdout.sh3
-rw-r--r--tests/bfs/fprint_error_stderr.sh2
-rw-r--r--tests/bfs/fprint_error_stdout.sh2
-rw-r--r--tests/bfs/help.sh4
-rw-r--r--tests/bfs/hidden.out (renamed from tests/test_hidden.out)0
-rw-r--r--tests/bfs/hidden.sh1
-rw-r--r--tests/bfs/hidden_root.out (renamed from tests/test_hidden_root.out)2
-rw-r--r--tests/bfs/hidden_root.sh2
-rw-r--r--tests/bfs/high_byte.sh1
-rw-r--r--tests/bfs/j0.sh1
-rw-r--r--tests/bfs/j1.out19
-rw-r--r--tests/bfs/j1.sh1
-rw-r--r--tests/bfs/j64.out19
-rw-r--r--tests/bfs/j64.sh1
-rw-r--r--tests/bfs/j_negative.sh1
-rw-r--r--tests/bfs/limit.out4
-rw-r--r--tests/bfs/limit.sh1
-rw-r--r--tests/bfs/limit_0.sh1
-rw-r--r--tests/bfs/limit_implicit_print.sh1
-rw-r--r--tests/bfs/limit_incomplete.sh1
-rw-r--r--tests/bfs/limit_one.sh1
-rw-r--r--tests/bfs/links_empty.sh1
-rw-r--r--tests/bfs/links_invalid.sh1
-rw-r--r--tests/bfs/links_leading_space.sh1
-rw-r--r--tests/bfs/links_negative.sh1
-rw-r--r--tests/bfs/links_noarg.sh1
-rw-r--r--tests/bfs/newerma_nonexistent.sh1
-rw-r--r--tests/bfs/newermq.sh1
-rw-r--r--tests/bfs/newermt_invalid.sh1
-rw-r--r--tests/bfs/newerqm.sh1
-rw-r--r--tests/bfs/nocolor.out27
-rw-r--r--tests/bfs/nocolor.sh1
-rw-r--r--tests/bfs/nocolor_env.out27
-rw-r--r--tests/bfs/nocolor_env.sh3
-rw-r--r--tests/bfs/nocolor_env_empty.out27
-rw-r--r--tests/bfs/nocolor_env_empty.sh3
-rw-r--r--tests/bfs/noerror.out4
-rw-r--r--tests/bfs/noerror.sh1
-rw-r--r--tests/bfs/noerror_nowarn.sh2
-rw-r--r--tests/bfs/noerror_warn.sh2
-rw-r--r--tests/bfs/nohidden.out (renamed from tests/test_nohidden.out)24
-rw-r--r--tests/bfs/nohidden.sh1
-rw-r--r--tests/bfs/nohidden_depth.out (renamed from tests/test_nohidden_depth.out)24
-rw-r--r--tests/bfs/nohidden_depth.sh1
-rw-r--r--tests/bfs/nowarn.sh2
-rw-r--r--tests/bfs/ok_plus_semicolon.out (renamed from tests/test_ok_plus_semicolon.out)12
-rw-r--r--tests/bfs/ok_plus_semicolon.sh8
-rw-r--r--tests/bfs/okdir_plus_semicolon.out (renamed from tests/test_okdir_plus_semicolon.out)0
-rw-r--r--tests/bfs/okdir_plus_semicolon.sh1
-rw-r--r--tests/bfs/or_incomplete.sh1
-rw-r--r--tests/bfs/path_expr_flag.out (renamed from tests/test_flag_expr_path.out)0
-rw-r--r--tests/bfs/path_expr_flag.sh1
-rw-r--r--tests/bfs/path_flag_expr.out (renamed from tests/test_path_expr_flag.out)0
-rw-r--r--tests/bfs/path_flag_expr.sh1
-rw-r--r--tests/bfs/perm_leading_plus_symbolic.out3
-rw-r--r--tests/bfs/perm_leading_plus_symbolic.sh1
-rw-r--r--tests/bfs/perm_symbolic_double_comma.sh1
-rw-r--r--tests/bfs/perm_symbolic_missing_action.sh1
-rw-r--r--tests/bfs/perm_symbolic_trailing_comma.sh1
-rw-r--r--tests/bfs/printf_color.out28
-rw-r--r--tests/bfs/printf_color.sh1
-rw-r--r--tests/bfs/printf_duplicate_flag.sh1
-rw-r--r--tests/bfs/printf_everything.sh15
-rw-r--r--tests/bfs/printf_incomplete_escape.sh1
-rw-r--r--tests/bfs/printf_incomplete_format.sh1
-rw-r--r--tests/bfs/printf_invalid_escape.sh1
-rw-r--r--tests/bfs/printf_invalid_flag.sh1
-rw-r--r--tests/bfs/printf_invalid_format.sh1
-rw-r--r--tests/bfs/printf_must_be_numeric.sh1
-rw-r--r--tests/bfs/printf_w.out (renamed from tests/test_and_purity.out)0
-rw-r--r--tests/bfs/printf_w.sh2
-rw-r--r--tests/bfs/status.sh1
-rw-r--r--tests/bfs/stderr_fails_loudly.sh2
-rw-r--r--tests/bfs/stderr_fails_silently.out19
-rw-r--r--tests/bfs/stderr_fails_silently.sh2
-rw-r--r--tests/bfs/type_multi.out (renamed from tests/test_type_multi.out)4
-rw-r--r--tests/bfs/type_multi.sh1
-rw-r--r--tests/bfs/typo.sh1
-rw-r--r--tests/bfs/unexpected_operator.sh1
-rw-r--r--tests/bfs/unique.out (renamed from tests/test_unique.out)0
-rw-r--r--tests/bfs/unique.sh1
-rw-r--r--tests/bfs/unique_depth.out19
-rw-r--r--tests/bfs/unique_depth.sh1
-rw-r--r--tests/bfs/version.sh1
-rw-r--r--tests/bfs/warn_O9.out19
-rw-r--r--tests/bfs/warn_O9.sh3
-rw-r--r--tests/bfs/warn_depth_prune.sh2
-rw-r--r--tests/bfs/warn_exclude_path.sh2
-rw-r--r--tests/bfs/warn_without_noerror.sh2
-rw-r--r--tests/bfs/warn_xdev_mount.out19
-rw-r--r--tests/bfs/warn_xdev_mount.sh2
-rw-r--r--tests/bfs/xtype_depth.sh2
-rw-r--r--tests/bfs/xtype_multi.out (renamed from tests/test_xtype_multi.out)8
-rw-r--r--tests/bfs/xtype_multi.sh1
-rw-r--r--tests/bfs/xtype_reorder.out (renamed from tests/test_data_flow_type.out)0
-rw-r--r--tests/bfs/xtype_reorder.sh3
-rw-r--r--tests/bfstd.c212
-rw-r--r--tests/bit.c160
-rw-r--r--tests/bsd/E.out (renamed from tests/test_E.out)0
-rw-r--r--tests/bsd/E.sh2
-rw-r--r--tests/bsd/H_mnewer.out (renamed from tests/test_H_mnewer.out)0
-rw-r--r--tests/bsd/H_mnewer.sh1
-rw-r--r--tests/bsd/Hf.out (renamed from tests/test_H.out)0
-rw-r--r--tests/bsd/Hf.sh1
-rw-r--r--tests/bsd/L_acl.out2
-rw-r--r--tests/bsd/L_acl.sh9
-rw-r--r--tests/bsd/L_xattr.out3
-rw-r--r--tests/bsd/L_xattr.sh3
-rw-r--r--tests/bsd/L_xattrname.out2
-rw-r--r--tests/bsd/L_xattrname.sh11
-rw-r--r--tests/bsd/X.out (renamed from tests/test_X.out)18
-rw-r--r--tests/bsd/X.sh1
-rw-r--r--tests/bsd/acl.out1
-rw-r--r--tests/bsd/acl.sh9
-rw-r--r--tests/bsd/asince.out (renamed from tests/test_asince.out)0
-rw-r--r--tests/bsd/asince.sh1
-rw-r--r--tests/bsd/d_path.out19
-rw-r--r--tests/bsd/d_path.sh1
-rw-r--r--tests/bsd/data_flow_depth.out (renamed from tests/test_data_flow_depth.out)2
-rw-r--r--tests/bsd/data_flow_depth.sh1
-rw-r--r--tests/bsd/data_flow_sparse.out19
-rw-r--r--tests/bsd/data_flow_sparse.sh1
-rw-r--r--tests/bsd/depth_depth_n.out (renamed from tests/test_depth_depth_n.out)0
-rw-r--r--tests/bsd/depth_depth_n.sh1
-rw-r--r--tests/bsd/depth_depth_n_minus.out (renamed from tests/test_depth_depth_n_minus.out)0
-rw-r--r--tests/bsd/depth_depth_n_minus.sh1
-rw-r--r--tests/bsd/depth_depth_n_plus.out (renamed from tests/test_depth_depth_n_plus.out)0
-rw-r--r--tests/bsd/depth_depth_n_plus.sh1
-rw-r--r--tests/bsd/depth_n.out (renamed from tests/test_depth_n.out)0
-rw-r--r--tests/bsd/depth_n.sh1
-rw-r--r--tests/bsd/depth_n_minus.out (renamed from tests/test_depth_maxdepth_1.out)0
-rw-r--r--tests/bsd/depth_n_minus.sh1
-rw-r--r--tests/bsd/depth_n_plus.out (renamed from tests/test_depth_n_plus.out)0
-rw-r--r--tests/bsd/depth_n_plus.sh1
-rw-r--r--tests/bsd/depth_overflow.out19
-rw-r--r--tests/bsd/depth_overflow.sh1
-rw-r--r--tests/bsd/exit.out (renamed from tests/test_exit.out)0
-rw-r--r--tests/bsd/exit.sh5
-rw-r--r--tests/bsd/exit_no_implicit_print.out (renamed from tests/test_exclude_mindepth.out)0
-rw-r--r--tests/bsd/exit_no_implicit_print.sh1
-rw-r--r--tests/bsd/f.out (renamed from tests/test_f.out)2
-rw-r--r--tests/bsd/f.sh2
-rw-r--r--tests/bsd/f_incomplete.sh1
-rw-r--r--tests/bsd/flags.out1
-rw-r--r--tests/bsd/flags.sh8
-rw-r--r--tests/bsd/gid_name.out19
-rw-r--r--tests/bsd/gid_name.sh1
-rw-r--r--tests/bsd/mnewer.out (renamed from tests/test_H_newer.out)0
-rw-r--r--tests/bsd/mnewer.sh1
-rw-r--r--tests/bsd/msince.out (renamed from tests/test_msince.out)0
-rw-r--r--tests/bsd/msince.sh1
-rw-r--r--tests/bsd/mtime_bad_unit.sh1
-rw-r--r--tests/bsd/mtime_missing_unit.sh1
-rw-r--r--tests/bsd/mtime_units.out (renamed from tests/test_mtime_units.out)0
-rw-r--r--tests/bsd/mtime_units.sh1
-rw-r--r--tests/bsd/okdir_stdin.out (renamed from tests/test_okdir_stdin.out)0
-rw-r--r--tests/bsd/okdir_stdin.sh2
-rw-r--r--tests/bsd/perm_000_plus.out29
-rw-r--r--tests/bsd/perm_000_plus.sh1
-rw-r--r--tests/bsd/perm_222_plus.out20
-rw-r--r--tests/bsd/perm_222_plus.sh1
-rw-r--r--tests/bsd/perm_644_plus.out26
-rw-r--r--tests/bsd/perm_644_plus.sh1
-rw-r--r--tests/bsd/printx.out (renamed from tests/test_printx.out)26
-rw-r--r--tests/bsd/printx.sh1
-rw-r--r--tests/bsd/quit_implicit_print.out (renamed from tests/test_and_false_or_true.out)0
-rw-r--r--tests/bsd/quit_implicit_print.sh1
-rw-r--r--tests/bsd/rm.out1
-rw-r--r--tests/bsd/rm.sh4
-rw-r--r--tests/bsd/s.out (renamed from tests/test_s.out)5
-rw-r--r--tests/bsd/s.sh2
-rw-r--r--tests/bsd/s_quit.out1
-rw-r--r--tests/bsd/s_quit.sh4
-rw-r--r--tests/bsd/size_T.out (renamed from tests/test_size_T.out)0
-rw-r--r--tests/bsd/size_T.sh1
-rw-r--r--tests/bsd/sparse.out1
-rw-r--r--tests/bsd/sparse.sh12
-rw-r--r--tests/bsd/type_w.out34
-rw-r--r--tests/bsd/type_w.sh56
-rw-r--r--tests/bsd/uid_name.out19
-rw-r--r--tests/bsd/uid_name.sh1
-rw-r--r--tests/bsd/xattr.out3
-rw-r--r--tests/bsd/xattr.sh3
-rw-r--r--tests/bsd/xattrname.out2
-rw-r--r--tests/bsd/xattrname.sh11
-rw-r--r--tests/color.sh151
-rw-r--r--tests/common/HLP.out (renamed from tests/test_P.out)0
-rw-r--r--tests/common/HLP.sh1
-rw-r--r--tests/common/H_newer.out (renamed from tests/test_anewer.out)0
-rw-r--r--tests/common/H_newer.sh1
-rw-r--r--tests/common/H_samefile_broken.out (renamed from tests/test_H_broken.out)0
-rw-r--r--tests/common/H_samefile_broken.sh1
-rw-r--r--tests/common/H_samefile_notdir.out (renamed from tests/test_H_notdir.out)0
-rw-r--r--tests/common/H_samefile_notdir.sh1
-rw-r--r--tests/common/H_samefile_symlink.out (renamed from tests/test_H_samefile_symlink.out)0
-rw-r--r--tests/common/H_samefile_symlink.sh1
-rw-r--r--tests/common/L_ilname.out (renamed from tests/test_false.out)0
-rw-r--r--tests/common/L_ilname.sh2
-rw-r--r--tests/common/L_lname.out (renamed from tests/test_ilname.out)0
-rw-r--r--tests/common/L_lname.sh1
-rw-r--r--tests/common/L_ls.sh1
-rw-r--r--tests/common/L_samefile_broken.out (renamed from tests/test_H_samefile_broken.out)0
-rw-r--r--tests/common/L_samefile_broken.sh1
-rw-r--r--tests/common/L_samefile_notdir.out (renamed from tests/test_H_samefile_notdir.out)0
-rw-r--r--tests/common/L_samefile_notdir.sh1
-rw-r--r--tests/common/L_samefile_symlink.out (renamed from tests/test_L_samefile_symlink.out)0
-rw-r--r--tests/common/L_samefile_symlink.sh1
-rw-r--r--tests/common/P.out1
-rw-r--r--tests/common/P.sh1
-rw-r--r--tests/common/P_slash.out (renamed from tests/test_H_slash.out)0
-rw-r--r--tests/common/P_slash.sh1
-rw-r--r--tests/common/amin.out6
-rw-r--r--tests/common/amin.sh15
-rw-r--r--tests/common/anewer.out (renamed from tests/test_mnewer.out)0
-rw-r--r--tests/common/anewer.sh1
-rw-r--r--tests/common/delete.out1
-rw-r--r--tests/common/delete.sh4
-rw-r--r--tests/common/delete_error.out8
-rw-r--r--tests/common/delete_error.sh9
-rw-r--r--tests/common/delete_many.out1
-rw-r--r--tests/common/delete_many.sh8
-rw-r--r--tests/common/depth_maxdepth_1.out (renamed from tests/test_depth_n_minus.out)0
-rw-r--r--tests/common/depth_maxdepth_1.sh1
-rw-r--r--tests/common/depth_maxdepth_2.out (renamed from tests/test_depth_maxdepth_2.out)10
-rw-r--r--tests/common/depth_maxdepth_2.sh1
-rw-r--r--tests/common/depth_mindepth_1.out (renamed from tests/test_depth_mindepth_1.out)12
-rw-r--r--tests/common/depth_mindepth_1.sh1
-rw-r--r--tests/common/depth_mindepth_2.out (renamed from tests/test_depth_mindepth_2.out)2
-rw-r--r--tests/common/depth_mindepth_2.sh1
-rw-r--r--tests/common/double_dash.out (renamed from tests/test_double_dash.out)0
-rw-r--r--tests/common/double_dash.sh2
-rw-r--r--tests/common/empty.out (renamed from tests/test_empty.out)2
-rw-r--r--tests/common/empty.sh1
-rw-r--r--tests/common/empty_error.out1
-rw-r--r--tests/common/empty_error.sh1
-rw-r--r--tests/common/empty_special.out20
-rw-r--r--tests/common/empty_special.sh1
-rw-r--r--tests/common/exec_substring.out (renamed from tests/test_exec_substring.out)12
-rw-r--r--tests/common/exec_substring.sh1
-rw-r--r--tests/common/execdir_nonexistent.out19
-rw-r--r--tests/common/execdir_nonexistent.sh2
-rw-r--r--tests/common/execdir_pwd.out (renamed from tests/test_execdir_pwd.out)2
-rw-r--r--tests/common/execdir_pwd.sh3
-rw-r--r--tests/common/execdir_slash.out (renamed from tests/test_execdir_slash.out)0
-rw-r--r--tests/common/execdir_slash.sh2
-rw-r--r--tests/common/execdir_slash_pwd.out (renamed from tests/test_execdir_slash_pwd.out)0
-rw-r--r--tests/common/execdir_slash_pwd.sh1
-rw-r--r--tests/common/execdir_slashes.out (renamed from tests/test_execdir_slashes.out)0
-rw-r--r--tests/common/execdir_slashes.sh1
-rw-r--r--tests/common/execdir_ulimit.out (renamed from tests/test_execdir_ulimit.out)2
-rw-r--r--tests/common/execdir_ulimit.sh6
-rw-r--r--tests/common/flag_double_dash.out (renamed from tests/test_flag_double_dash.out)0
-rw-r--r--tests/common/flag_double_dash.sh2
-rw-r--r--tests/common/follow.out (renamed from tests/test_follow.out)12
-rw-r--r--tests/common/follow.sh1
-rw-r--r--tests/common/gid.out19
-rw-r--r--tests/common/gid.sh1
-rw-r--r--tests/common/gid_invalid_id.sh1
-rw-r--r--tests/common/gid_invalid_name.sh1
-rw-r--r--tests/common/gid_minus.out19
-rw-r--r--tests/common/gid_minus.sh1
-rw-r--r--tests/common/gid_minus_plus.out19
-rw-r--r--tests/common/gid_minus_plus.sh1
-rw-r--r--tests/common/gid_plus.out19
-rw-r--r--tests/common/gid_plus.sh2
-rw-r--r--tests/common/gid_plus_plus.out19
-rw-r--r--tests/common/gid_plus_plus.sh2
-rw-r--r--tests/common/ilname.out (renamed from tests/test_lname.out)0
-rw-r--r--tests/common/ilname.sh2
-rw-r--r--tests/common/inum.out (renamed from tests/test_inum.out)0
-rw-r--r--tests/common/inum.sh1
-rw-r--r--tests/common/inum_bind_mount.out2
-rw-r--r--tests/common/inum_bind_mount.sh9
-rw-r--r--tests/common/inum_mount.out1
-rw-r--r--tests/common/inum_mount.sh9
-rw-r--r--tests/common/ipath.out (renamed from tests/test_path.out)2
-rw-r--r--tests/common/ipath.sh2
-rw-r--r--tests/common/iregex.out (renamed from tests/test_iregex.out)0
-rw-r--r--tests/common/iregex.sh1
-rw-r--r--tests/common/lname.out (renamed from tests/test_nogroup.out)0
-rw-r--r--tests/common/lname.sh1
-rw-r--r--tests/common/ls.sh1
-rw-r--r--tests/common/maxdepth.out (renamed from tests/test_maxdepth.out)0
-rw-r--r--tests/common/maxdepth.sh1
-rw-r--r--tests/common/maxdepth_incomplete.sh1
-rw-r--r--tests/common/mindepth.out (renamed from tests/test_mindepth.out)12
-rw-r--r--tests/common/mindepth.sh1
-rw-r--r--tests/common/mindepth_incomplete.sh1
-rw-r--r--tests/common/mmin.out6
-rw-r--r--tests/common/mmin.sh15
-rw-r--r--tests/common/newerma.out (renamed from tests/test_newer.out)0
-rw-r--r--tests/common/newerma.sh1
-rw-r--r--tests/common/newermt.out (renamed from tests/test_newermt.out)0
-rw-r--r--tests/common/newermt.sh3
-rw-r--r--tests/common/newermt_epoch_minus_one.out (renamed from tests/test_newermt_epoch_minus_one.out)0
-rw-r--r--tests/common/newermt_epoch_minus_one.sh1
-rw-r--r--tests/common/ok_closed_stdin.out (renamed from tests/test_nogroup_ulimit.out)0
-rw-r--r--tests/common/ok_closed_stdin.sh1
-rw-r--r--tests/common/okdir_closed_stdin.out (renamed from tests/test_nouser.out)0
-rw-r--r--tests/common/okdir_closed_stdin.sh1
-rw-r--r--tests/common/quit.out (renamed from tests/test_name_root_depth.out)0
-rw-r--r--tests/common/quit.sh1
-rw-r--r--tests/common/quit_after_print.out (renamed from tests/test_comma_reachability.out)0
-rw-r--r--tests/common/quit_after_print.sh1
-rw-r--r--tests/common/quit_before_print.out (renamed from tests/test_nouser_ulimit.out)0
-rw-r--r--tests/common/quit_before_print.sh1
-rw-r--r--tests/common/quit_child.out (renamed from tests/test_quit_child.out)0
-rw-r--r--tests/common/quit_child.sh1
-rw-r--r--tests/common/quit_depth.out (renamed from tests/test_quit_depth.out)0
-rw-r--r--tests/common/quit_depth.sh1
-rw-r--r--tests/common/quit_depth_child.out (renamed from tests/test_quit_depth_child.out)0
-rw-r--r--tests/common/quit_depth_child.sh1
-rw-r--r--tests/common/regex.out (renamed from tests/test_regex.out)0
-rw-r--r--tests/common/regex.sh1
-rw-r--r--tests/common/regex_parens.out (renamed from tests/test_regex_parens.out)0
-rw-r--r--tests/common/regex_parens.sh2
-rw-r--r--tests/common/samefile.out (renamed from tests/test_links.out)0
-rw-r--r--tests/common/samefile.sh1
-rw-r--r--tests/common/samefile_broken.out (renamed from tests/test_L_broken.out)0
-rw-r--r--tests/common/samefile_broken.sh1
-rw-r--r--tests/common/samefile_notdir.out (renamed from tests/test_L_notdir.out)0
-rw-r--r--tests/common/samefile_notdir.sh1
-rw-r--r--tests/common/samefile_symlink.out (renamed from tests/test_samefile_symlink.out)0
-rw-r--r--tests/common/samefile_symlink.sh1
-rw-r--r--tests/common/samefile_wordesc.sh4
-rw-r--r--tests/common/size_big.out (renamed from tests/test_ok_closed_stdin.out)0
-rw-r--r--tests/common/size_big.sh1
-rw-r--r--tests/common/uid.out19
-rw-r--r--tests/common/uid.sh1
-rw-r--r--tests/common/uid_invalid_id.sh1
-rw-r--r--tests/common/uid_invalid_name.sh1
-rw-r--r--tests/common/uid_minus.out19
-rw-r--r--tests/common/uid_minus.sh1
-rw-r--r--tests/common/uid_minus_plus.out19
-rw-r--r--tests/common/uid_minus_plus.sh1
-rw-r--r--tests/common/uid_plus.out19
-rw-r--r--tests/common/uid_plus.sh2
-rw-r--r--tests/common/uid_plus_plus.out19
-rw-r--r--tests/common/uid_plus_plus.sh2
-rwxr-xr-xtests/find-color.sh17
-rw-r--r--tests/getopts.sh174
-rw-r--r--tests/gnu/L_delete.out2
-rw-r--r--tests/gnu/L_delete.sh8
-rw-r--r--tests/gnu/L_loops_continue.out11
-rw-r--r--tests/gnu/L_loops_continue.sh1
-rw-r--r--tests/gnu/L_printf_types.out17
-rw-r--r--tests/gnu/L_printf_types.sh1
-rw-r--r--tests/gnu/L_xtype_f.out (renamed from tests/test_L_xtype_f.out)2
-rw-r--r--tests/gnu/L_xtype_f.sh1
-rw-r--r--tests/gnu/L_xtype_l.out (renamed from tests/test_L_xtype_l.out)6
-rw-r--r--tests/gnu/L_xtype_l.sh1
-rw-r--r--tests/gnu/and.out (renamed from tests/test_a.out)0
-rw-r--r--tests/gnu/and.sh1
-rw-r--r--tests/gnu/and_false_or_true.out (renamed from tests/test_comma_redundant_false.out)0
-rw-r--r--tests/gnu/and_false_or_true.sh3
-rw-r--r--tests/gnu/and_purity.out (renamed from tests/test_okdir_closed_stdin.out)0
-rw-r--r--tests/gnu/and_purity.sh2
-rw-r--r--tests/gnu/comma.out (renamed from tests/test_comma.out)14
-rw-r--r--tests/gnu/comma.sh1
-rw-r--r--tests/gnu/comma_reachability.out (renamed from tests/test_comma_redundant_true.out)0
-rw-r--r--tests/gnu/comma_reachability.sh1
-rw-r--r--tests/gnu/comma_redundant_false.out (renamed from tests/test_not_reachability.out)0
-rw-r--r--tests/gnu/comma_redundant_false.sh2
-rw-r--r--tests/gnu/comma_redundant_true.out (renamed from tests/test_printf_leak.out)0
-rw-r--r--tests/gnu/comma_redundant_true.sh2
-rw-r--r--tests/gnu/daystart.out19
-rw-r--r--tests/gnu/daystart.sh1
-rw-r--r--tests/gnu/daystart_twice.out19
-rw-r--r--tests/gnu/daystart_twice.sh1
-rw-r--r--tests/gnu/exec_flush.out19
-rw-r--r--tests/gnu/exec_flush.sh4
-rw-r--r--tests/gnu/exec_flush_fail.sh3
-rw-r--r--tests/gnu/exec_nothing.sh2
-rw-r--r--tests/gnu/exec_plus_flush.outbin0 -> 22 bytes
-rw-r--r--tests/gnu/exec_plus_flush.sh2
-rw-r--r--tests/gnu/exec_plus_flush_fail.sh2
-rw-r--r--tests/gnu/execdir.out19
-rw-r--r--tests/gnu/execdir.sh1
-rw-r--r--tests/gnu/execdir_path_dot.sh1
-rw-r--r--tests/gnu/execdir_path_empty.sh1
-rw-r--r--tests/gnu/execdir_path_relative.sh1
-rw-r--r--tests/gnu/execdir_plus_semicolon.out (renamed from tests/test_execdir_plus_semicolon.out)0
-rw-r--r--tests/gnu/execdir_plus_semicolon.sh1
-rw-r--r--tests/gnu/execdir_self.out1
-rw-r--r--tests/gnu/execdir_self.sh9
-rw-r--r--tests/gnu/execdir_substring.out (renamed from tests/test_execdir_substring.out)0
-rw-r--r--tests/gnu/execdir_substring.sh1
-rw-r--r--tests/gnu/execdir_ulimit.out16
-rw-r--r--tests/gnu/execdir_ulimit.sh2
-rw-r--r--tests/gnu/executable.out19
-rw-r--r--tests/gnu/executable.sh1
-rw-r--r--tests/gnu/false.out (renamed from tests/test_or_purity.out)0
-rw-r--r--tests/gnu/false.sh1
-rw-r--r--tests/gnu/files0_from_empty.sh1
-rw-r--r--tests/gnu/files0_from_error.sh1
-rw-r--r--tests/gnu/files0_from_file.out (renamed from tests/test_files0_from_stdin.out)33
-rw-r--r--tests/gnu/files0_from_file.sh4
-rw-r--r--tests/gnu/files0_from_file_file.out2
-rw-r--r--tests/gnu/files0_from_file_file.sh3
-rw-r--r--tests/gnu/files0_from_none.out (renamed from tests/test_perm_leading_plus_symbolic.out)0
-rw-r--r--tests/gnu/files0_from_none.sh1
-rw-r--r--tests/gnu/files0_from_nothing.sh1
-rw-r--r--tests/gnu/files0_from_nowhere.sh1
-rw-r--r--tests/gnu/files0_from_stdin.out (renamed from tests/test_files0_from_file.out)33
-rw-r--r--tests/gnu/files0_from_stdin.sh2
-rw-r--r--tests/gnu/files0_from_stdin_ok.sh1
-rw-r--r--tests/gnu/files0_from_stdin_ok_file.out45
-rw-r--r--tests/gnu/files0_from_stdin_ok_file.sh4
-rw-r--r--tests/gnu/files0_from_stdin_stdin.out45
-rw-r--r--tests/gnu/files0_from_stdin_stdin.sh2
-rw-r--r--tests/gnu/fls.sh1
-rw-r--r--tests/gnu/fls_nonexistent.sh1
-rw-r--r--tests/gnu/fls_overflow.sh4
-rw-r--r--tests/gnu/follow_comma.out (renamed from tests/test_follow_comma.out)26
-rw-r--r--tests/gnu/follow_comma.sh3
-rw-r--r--tests/gnu/follow_files0_from.out42
-rw-r--r--tests/gnu/follow_files0_from.sh1
-rw-r--r--tests/gnu/fprint.out19
-rw-r--r--tests/gnu/fprint.sh3
-rw-r--r--tests/gnu/fprint0.out (renamed from tests/test_fprint0.out)bin16 -> 16 bytes
-rw-r--r--tests/gnu/fprint0.sh2
-rw-r--r--tests/gnu/fprint0_nonexistent.sh1
-rw-r--r--tests/gnu/fprint_duplicate.out (renamed from tests/test_fprint_duplicate.out)0
-rw-r--r--tests/gnu/fprint_duplicate.sh7
-rw-r--r--tests/gnu/fprint_error.sh2
-rw-r--r--tests/gnu/fprint_noarg.sh1
-rw-r--r--tests/gnu/fprint_nonexistent.sh1
-rw-r--r--tests/gnu/fprint_truncate.out (renamed from tests/test_quit_after_print.out)0
-rw-r--r--tests/gnu/fprint_truncate.sh5
-rw-r--r--tests/gnu/fprint_unreached_error.sh3
-rw-r--r--tests/gnu/fprintf.out (renamed from tests/test_fprintf.out)0
-rw-r--r--tests/gnu/fprintf.sh3
-rw-r--r--tests/gnu/fprintf_nofile.sh1
-rw-r--r--tests/gnu/fprintf_noformat.sh1
-rw-r--r--tests/gnu/fprintf_nonexistent.sh1
-rw-r--r--tests/gnu/fstype.out19
-rw-r--r--tests/gnu/fstype.sh2
-rw-r--r--tests/gnu/fstype_btrfs_subvol.out4
-rw-r--r--tests/gnu/fstype_btrfs_subvol.sh25
-rw-r--r--tests/gnu/fstype_stacked.out1
-rw-r--r--tests/gnu/fstype_stacked.sh12
-rw-r--r--tests/gnu/fstype_umount.out (renamed from tests/test_perm_leading_plus_symbolic_minus.out)0
-rw-r--r--tests/gnu/fstype_umount.sh12
-rw-r--r--tests/gnu/ignore_readdir_race.sh5
-rw-r--r--tests/gnu/ignore_readdir_race_loop.out11
-rw-r--r--tests/gnu/ignore_readdir_race_loop.sh2
-rw-r--r--tests/gnu/ignore_readdir_race_notdir.sh7
-rw-r--r--tests/gnu/ignore_readdir_race_rmdir.out2
-rw-r--r--tests/gnu/ignore_readdir_race_rmdir.sh5
-rw-r--r--tests/gnu/ignore_readdir_race_root.sh2
-rw-r--r--tests/gnu/inum_automount.out1
-rw-r--r--tests/gnu/inum_automount.sh14
-rw-r--r--tests/gnu/iwholename.out (renamed from tests/test_ipath.out)2
-rw-r--r--tests/gnu/iwholename.sh2
-rw-r--r--tests/gnu/newer_link.out (renamed from tests/test_newer_link.out)0
-rw-r--r--tests/gnu/newer_link.sh1
-rw-r--r--tests/gnu/noleaf.out19
-rw-r--r--tests/gnu/noleaf.sh1
-rw-r--r--tests/gnu/not.out (renamed from tests/test_bang.out)8
-rw-r--r--tests/gnu/not.sh1
-rw-r--r--tests/gnu/not_comma.out34
-rw-r--r--tests/gnu/not_comma.sh2
-rw-r--r--tests/gnu/not_reachability.out (renamed from tests/test_quit_implicit_print.out)0
-rw-r--r--tests/gnu/not_reachability.sh1
-rw-r--r--tests/gnu/ok_files0_from_stdin.sh1
-rw-r--r--tests/gnu/ok_flush.out19
-rw-r--r--tests/gnu/ok_flush.sh4
-rw-r--r--tests/gnu/ok_nothing.sh2
-rw-r--r--tests/gnu/okdir_path_dot.sh1
-rw-r--r--tests/gnu/okdir_path_empty.sh1
-rw-r--r--tests/gnu/okdir_path_relative.sh1
-rw-r--r--tests/gnu/or.out (renamed from tests/test_o.out)6
-rw-r--r--tests/gnu/or.sh1
-rw-r--r--tests/gnu/path_d.out19
-rw-r--r--tests/gnu/path_d.sh1
-rw-r--r--tests/gnu/perm_000_slash.out29
-rw-r--r--tests/gnu/perm_000_slash.sh1
-rw-r--r--tests/gnu/perm_222_slash.out20
-rw-r--r--tests/gnu/perm_222_slash.sh1
-rw-r--r--tests/gnu/perm_644_slash.out26
-rw-r--r--tests/gnu/perm_644_slash.sh1
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.out28
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.sh1
-rw-r--r--tests/gnu/perm_symbolic_slash.out24
-rw-r--r--tests/gnu/perm_symbolic_slash.sh1
-rw-r--r--tests/gnu/precedence.out (renamed from tests/test_precedence.out)2
-rw-r--r--tests/gnu/precedence.sh1
-rw-r--r--tests/gnu/print_error.sh2
-rw-r--r--tests/gnu/printf.out (renamed from tests/test_printf.out)12
-rw-r--r--tests/gnu/printf.sh1
-rw-r--r--tests/gnu/printf_H.out (renamed from tests/test_printf_H.out)30
-rw-r--r--tests/gnu/printf_H.sh1
-rw-r--r--tests/gnu/printf_Y_error.out3
-rw-r--r--tests/gnu/printf_Y_error.sh8
-rw-r--r--tests/gnu/printf_empty.out (renamed from tests/test_perm_symbolic.out)0
-rw-r--r--tests/gnu/printf_empty.sh1
-rw-r--r--tests/gnu/printf_escapes.out (renamed from tests/test_printf_escapes.out)0
-rw-r--r--tests/gnu/printf_escapes.sh1
-rw-r--r--tests/gnu/printf_flags.out (renamed from tests/test_printf_flags.out)10
-rw-r--r--tests/gnu/printf_flags.sh1
-rw-r--r--tests/gnu/printf_l_nonlink.out (renamed from tests/test_printf_l_nonlink.out)4
-rw-r--r--tests/gnu/printf_l_nonlink.sh1
-rw-r--r--tests/gnu/printf_leak.out1
-rw-r--r--tests/gnu/printf_leak.sh2
-rw-r--r--tests/gnu/printf_nul.outbin0 -> 16 bytes
-rw-r--r--tests/gnu/printf_nul.sh3
-rw-r--r--tests/gnu/printf_slash.out (renamed from tests/test_printf_slash.out)0
-rw-r--r--tests/gnu/printf_slash.sh1
-rw-r--r--tests/gnu/printf_slashes.out (renamed from tests/test_printf_slashes.out)0
-rw-r--r--tests/gnu/printf_slashes.sh1
-rw-r--r--tests/gnu/printf_times.out (renamed from tests/test_printf_times.out)0
-rw-r--r--tests/gnu/printf_times.sh1
-rw-r--r--tests/gnu/printf_trailing_slash.out (renamed from tests/test_printf_trailing_slash.out)4
-rw-r--r--tests/gnu/printf_trailing_slash.sh1
-rw-r--r--tests/gnu/printf_trailing_slashes.out (renamed from tests/test_printf_trailing_slashes.out)4
-rw-r--r--tests/gnu/printf_trailing_slashes.sh1
-rw-r--r--tests/gnu/printf_types.out (renamed from tests/test_printf_types.out)8
-rw-r--r--tests/gnu/printf_types.sh1
-rw-r--r--tests/gnu/printf_u_g_ulimit.sh2
-rw-r--r--tests/gnu/readable.out19
-rw-r--r--tests/gnu/readable.sh1
-rw-r--r--tests/gnu/regex_error.sh1
-rw-r--r--tests/gnu/regex_invalid_utf8.out1
-rw-r--r--tests/gnu/regex_invalid_utf8.sh8
-rw-r--r--tests/gnu/regextype_awk.out2
-rw-r--r--tests/gnu/regextype_awk.sh3
-rw-r--r--tests/gnu/regextype_ed.out (renamed from tests/test_regextype_posix_basic.out)0
-rw-r--r--tests/gnu/regextype_ed.sh2
-rw-r--r--tests/gnu/regextype_egrep.out (renamed from tests/test_printf_empty.out)0
-rw-r--r--tests/gnu/regextype_egrep.sh3
-rw-r--r--tests/gnu/regextype_emacs.out6
-rw-r--r--tests/gnu/regextype_emacs.sh3
-rw-r--r--tests/gnu/regextype_findutils_default.out3
-rw-r--r--tests/gnu/regextype_findutils_default.sh3
-rw-r--r--tests/gnu/regextype_gnu_awk.out2
-rw-r--r--tests/gnu/regextype_gnu_awk.sh3
-rw-r--r--tests/gnu/regextype_grep.out (renamed from tests/test_iname.out)0
-rw-r--r--tests/gnu/regextype_grep.sh3
-rw-r--r--tests/gnu/regextype_posix_awk.out2
-rw-r--r--tests/gnu/regextype_posix_awk.sh3
-rw-r--r--tests/gnu/regextype_posix_basic.out (renamed from tests/test_regextype_posix_extended.out)0
-rw-r--r--tests/gnu/regextype_posix_basic.sh2
-rw-r--r--tests/gnu/regextype_posix_extended.out1
-rw-r--r--tests/gnu/regextype_posix_extended.sh2
-rw-r--r--tests/gnu/regextype_posix_minimal_basic.out1
-rw-r--r--tests/gnu/regextype_posix_minimal_basic.sh2
-rw-r--r--tests/gnu/regextype_sed.out1
-rw-r--r--tests/gnu/regextype_sed.sh2
-rw-r--r--tests/gnu/true.out19
-rw-r--r--tests/gnu/true.sh1
-rw-r--r--tests/gnu/used.out4
-rw-r--r--tests/gnu/used.sh21
-rw-r--r--tests/gnu/wholename.out7
-rw-r--r--tests/gnu/wholename.sh1
-rw-r--r--tests/gnu/writable.out20
-rw-r--r--tests/gnu/writable.sh1
-rw-r--r--tests/gnu/xtype_bind_mount.out2
-rw-r--r--tests/gnu/xtype_bind_mount.sh10
-rw-r--r--tests/gnu/xtype_f.out (renamed from tests/test_xtype_f.out)4
-rw-r--r--tests/gnu/xtype_f.sh1
-rw-r--r--tests/gnu/xtype_l.out (renamed from tests/test_xtype_l.out)2
-rw-r--r--tests/gnu/xtype_l.sh1
-rw-r--r--tests/gnu/xtype_l_loops.out3
-rw-r--r--tests/gnu/xtype_l_loops.sh1
-rw-r--r--tests/ioq.c76
-rw-r--r--tests/list.c99
-rwxr-xr-xtests/ls-color.sh54
-rw-r--r--tests/main.c271
-rw-r--r--tests/mksock.c46
-rw-r--r--tests/posix/H.out1
-rw-r--r--tests/posix/H.sh1
-rw-r--r--tests/posix/HL.out17
-rw-r--r--tests/posix/HL.sh1
-rw-r--r--tests/posix/H_broken.out (renamed from tests/test_L_samefile_broken.out)0
-rw-r--r--tests/posix/H_broken.sh1
-rw-r--r--tests/posix/H_loops.out (renamed from tests/test_H_loops.out)0
-rw-r--r--tests/posix/H_loops.sh1
-rw-r--r--tests/posix/H_notdir.out (renamed from tests/test_L_samefile_notdir.out)0
-rw-r--r--tests/posix/H_notdir.sh1
-rw-r--r--tests/posix/H_slash.out (renamed from tests/test_P_slash.out)0
-rw-r--r--tests/posix/H_slash.sh1
-rw-r--r--tests/posix/H_type_l.out (renamed from tests/test_path_flag_expr.out)0
-rw-r--r--tests/posix/H_type_l.sh1
-rw-r--r--tests/posix/L.out17
-rw-r--r--tests/posix/L.sh1
-rw-r--r--tests/posix/LH.out1
-rw-r--r--tests/posix/LH.sh1
-rw-r--r--tests/posix/L_broken.out (renamed from tests/test_samefile_broken.out)0
-rw-r--r--tests/posix/L_broken.sh1
-rw-r--r--tests/posix/L_depth.out17
-rw-r--r--tests/posix/L_depth.sh1
-rw-r--r--tests/posix/L_loops.sh4
-rw-r--r--tests/posix/L_mount.out2
-rw-r--r--tests/posix/L_mount.sh13
-rw-r--r--tests/posix/L_notdir.out (renamed from tests/test_samefile_notdir.out)0
-rw-r--r--tests/posix/L_notdir.sh1
-rw-r--r--tests/posix/L_type_l.out (renamed from tests/test_L_type_l.out)0
-rw-r--r--tests/posix/L_type_l.sh1
-rw-r--r--tests/posix/L_xdev.out5
-rw-r--r--tests/posix/L_xdev.sh13
-rw-r--r--tests/posix/a.out (renamed from tests/test_and.out)0
-rw-r--r--tests/posix/a.sh1
-rw-r--r--tests/posix/atime.out6
-rw-r--r--tests/posix/atime.sh15
-rw-r--r--tests/posix/bang.out (renamed from tests/test_not.out)8
-rw-r--r--tests/posix/bang.sh1
-rw-r--r--tests/posix/basic.out19
-rw-r--r--tests/posix/basic.sh1
-rw-r--r--tests/posix/data_flow_and_swap.out (renamed from tests/test_data_flow_and_swap.out)4
-rw-r--r--tests/posix/data_flow_and_swap.sh1
-rw-r--r--tests/posix/data_flow_group.out19
-rw-r--r--tests/posix/data_flow_group.sh1
-rw-r--r--tests/posix/data_flow_or_swap.out (renamed from tests/test_data_flow_or_swap.out)4
-rw-r--r--tests/posix/data_flow_or_swap.sh1
-rw-r--r--tests/posix/data_flow_type.out (renamed from tests/test_printf_w.out)0
-rw-r--r--tests/posix/data_flow_type.sh1
-rw-r--r--tests/posix/data_flow_user.out19
-rw-r--r--tests/posix/data_flow_user.sh1
-rw-r--r--tests/posix/de_morgan_and.out (renamed from tests/test_de_morgan_and.out)2
-rw-r--r--tests/posix/de_morgan_and.sh1
-rw-r--r--tests/posix/de_morgan_not.out (renamed from tests/test_de_morgan_not.out)0
-rw-r--r--tests/posix/de_morgan_not.sh1
-rw-r--r--tests/posix/de_morgan_or.out (renamed from tests/test_de_morgan_or.out)10
-rw-r--r--tests/posix/de_morgan_or.sh1
-rw-r--r--tests/posix/deep.out (renamed from tests/test_deep_strict.out)0
-rw-r--r--tests/posix/deep.sh2
-rw-r--r--tests/posix/depth.out19
-rw-r--r--tests/posix/depth.sh1
-rw-r--r--tests/posix/depth_error.out4
-rw-r--r--tests/posix/depth_error.sh1
-rw-r--r--tests/posix/depth_slash.out (renamed from tests/test_depth_slash.out)12
-rw-r--r--tests/posix/depth_slash.sh1
-rw-r--r--tests/posix/double_negation.out (renamed from tests/test_double_negation.out)0
-rw-r--r--tests/posix/double_negation.sh1
-rw-r--r--tests/posix/exec.out19
-rw-r--r--tests/posix/exec.sh1
-rw-r--r--tests/posix/exec_nonexistent.out19
-rw-r--r--tests/posix/exec_nonexistent.sh4
-rw-r--r--tests/posix/exec_nopath.out19
-rw-r--r--tests/posix/exec_nopath.sh7
-rw-r--r--tests/posix/exec_plus.out (renamed from tests/test_exec_plus.out)0
-rw-r--r--tests/posix/exec_plus.sh1
-rw-r--r--tests/posix/exec_plus_nonexistent.out19
-rw-r--r--tests/posix/exec_plus_nonexistent.sh2
-rw-r--r--tests/posix/exec_plus_nothing.sh2
-rw-r--r--tests/posix/exec_plus_semicolon.out (renamed from tests/test_exec_plus_semicolon.out)12
-rw-r--r--tests/posix/exec_plus_semicolon.sh5
-rw-r--r--tests/posix/exec_plus_status.out19
-rw-r--r--tests/posix/exec_plus_status.sh3
-rw-r--r--tests/posix/exec_return.out18
-rw-r--r--tests/posix/exec_return.sh1
-rw-r--r--tests/posix/exec_sigmask.out1
-rw-r--r--tests/posix/exec_sigmask.sh16
-rw-r--r--tests/posix/exec_substring_plus.out19
-rw-r--r--tests/posix/exec_substring_plus.sh14
-rw-r--r--tests/posix/exec_ulimit.out16
-rw-r--r--tests/posix/exec_ulimit.sh2
-rw-r--r--tests/posix/extra_paren.sh1
-rw-r--r--tests/posix/flag_comma.out (renamed from tests/test_flag_comma.out)0
-rw-r--r--tests/posix/flag_comma.sh3
-rw-r--r--tests/posix/flag_weird_names.out (renamed from tests/test_flag_weird_names.out)20
-rw-r--r--tests/posix/flag_weird_names.sh2
-rw-r--r--tests/posix/group_id.out19
-rw-r--r--tests/posix/group_id.sh1
-rw-r--r--tests/posix/group_invalid_id.sh1
-rw-r--r--tests/posix/group_invalid_name.sh1
-rw-r--r--tests/posix/group_name.out19
-rw-r--r--tests/posix/group_name.sh1
-rw-r--r--tests/posix/group_nogroup.out19
-rw-r--r--tests/posix/group_nogroup.sh2
-rw-r--r--tests/posix/group_o_group.out19
-rw-r--r--tests/posix/group_o_group.sh3
-rw-r--r--tests/posix/implicit_and.out (renamed from tests/test_implicit_and.out)0
-rw-r--r--tests/posix/implicit_and.sh1
-rw-r--r--tests/posix/iname.out (renamed from tests/test_name.out)0
-rw-r--r--tests/posix/iname.sh1
-rw-r--r--tests/posix/incomplete.sh1
-rw-r--r--tests/posix/links.out (renamed from tests/test_links_plus.out)0
-rw-r--r--tests/posix/links.sh1
-rw-r--r--tests/posix/links_minus.out (renamed from tests/test_links_minus.out)0
-rw-r--r--tests/posix/links_minus.sh1
-rw-r--r--tests/posix/links_plus.out (renamed from tests/test_samefile.out)0
-rw-r--r--tests/posix/links_plus.sh1
-rw-r--r--tests/posix/missing_paren.sh1
-rw-r--r--tests/posix/mount.out3
-rw-r--r--tests/posix/mount.sh11
-rw-r--r--tests/posix/mtime.out6
-rw-r--r--tests/posix/mtime.sh15
-rw-r--r--tests/posix/name.out (renamed from tests/test_parens.out)0
-rw-r--r--tests/posix/name.sh1
-rw-r--r--tests/posix/name_backslash.out (renamed from tests/test_quit_before_print.out)0
-rw-r--r--tests/posix/name_backslash.sh2
-rw-r--r--tests/posix/name_bracket.out1
-rw-r--r--tests/posix/name_bracket.sh9
-rw-r--r--tests/posix/name_character_class.out (renamed from tests/test_prune.out)0
-rw-r--r--tests/posix/name_character_class.sh1
-rw-r--r--tests/posix/name_double_backslash.out1
-rw-r--r--tests/posix/name_double_backslash.sh2
-rw-r--r--tests/posix/name_root.out1
-rw-r--r--tests/posix/name_root.sh1
-rw-r--r--tests/posix/name_root_depth.out (renamed from tests/test_quit.out)0
-rw-r--r--tests/posix/name_root_depth.sh1
-rw-r--r--tests/posix/name_slash.out (renamed from tests/test_name_slash.out)0
-rw-r--r--tests/posix/name_slash.sh1
-rw-r--r--tests/posix/name_slashes.out (renamed from tests/test_name_slashes.out)0
-rw-r--r--tests/posix/name_slashes.sh1
-rw-r--r--tests/posix/name_star_star.out4
-rw-r--r--tests/posix/name_star_star.sh1
-rw-r--r--tests/posix/name_trailing_slash.out (renamed from tests/test_name_trailing_slash.out)0
-rw-r--r--tests/posix/name_trailing_slash.sh1
-rw-r--r--tests/posix/newer.out (renamed from tests/test_newerma.out)0
-rw-r--r--tests/posix/newer.sh1
-rw-r--r--tests/posix/newer_broken.out1
-rw-r--r--tests/posix/newer_broken.sh4
-rw-r--r--tests/posix/newer_nonexistent.sh1
-rw-r--r--tests/posix/nogroup.out (renamed from tests/test_size_big.out)0
-rw-r--r--tests/posix/nogroup.sh1
-rw-r--r--tests/posix/nogroup_ulimit.out (renamed from tests/test_xtype_reorder.out)0
-rw-r--r--tests/posix/nogroup_ulimit.sh2
-rw-r--r--tests/posix/not_prune.out (renamed from tests/test_exclude_depth.out)6
-rw-r--r--tests/posix/not_prune.sh1
-rw-r--r--tests/posix/nouser.out0
-rw-r--r--tests/posix/nouser.sh1
-rw-r--r--tests/posix/nouser_ulimit.out0
-rw-r--r--tests/posix/nouser_ulimit.sh2
-rw-r--r--tests/posix/o.out (renamed from tests/test_or.out)6
-rw-r--r--tests/posix/o.sh1
-rw-r--r--tests/posix/ok_plus_nothing.sh2
-rw-r--r--tests/posix/ok_stdin.out (renamed from tests/test_ok_stdin.out)20
-rw-r--r--tests/posix/ok_stdin.sh3
-rw-r--r--tests/posix/or_purity.out0
-rw-r--r--tests/posix/or_purity.sh2
-rw-r--r--tests/posix/overlayfs.out5
-rw-r--r--tests/posix/overlayfs.sh11
-rw-r--r--tests/posix/parens.out4
-rw-r--r--tests/posix/parens.sh1
-rw-r--r--tests/posix/path.out7
-rw-r--r--tests/posix/path.sh1
-rw-r--r--tests/posix/perm_000.out1
-rw-r--r--tests/posix/perm_000.sh1
-rw-r--r--tests/posix/perm_000_minus.out29
-rw-r--r--tests/posix/perm_000_minus.sh1
-rw-r--r--tests/posix/perm_222.out1
-rw-r--r--tests/posix/perm_222.sh1
-rw-r--r--tests/posix/perm_222_minus.out5
-rw-r--r--tests/posix/perm_222_minus.sh1
-rw-r--r--tests/posix/perm_644.out1
-rw-r--r--tests/posix/perm_644.sh1
-rw-r--r--tests/posix/perm_644_minus.out10
-rw-r--r--tests/posix/perm_644_minus.sh1
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.out7
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.sh1
-rw-r--r--tests/posix/perm_leading_plus_umask.out10
-rw-r--r--tests/posix/perm_leading_plus_umask.sh3
-rw-r--r--tests/posix/perm_setid.out (renamed from tests/test_perm_setid.out)0
-rw-r--r--tests/posix/perm_setid.sh1
-rw-r--r--tests/posix/perm_sticky.out (renamed from tests/test_perm_sticky.out)0
-rw-r--r--tests/posix/perm_sticky.sh1
-rw-r--r--tests/posix/perm_symbolic.out0
-rw-r--r--tests/posix/perm_symbolic.sh1
-rw-r--r--tests/posix/perm_symbolic_minus.out10
-rw-r--r--tests/posix/perm_symbolic_minus.sh1
-rw-r--r--tests/posix/permcopy.out1
-rw-r--r--tests/posix/permcopy.sh1
-rw-r--r--tests/posix/print0.out (renamed from tests/test_print0.out)bin16 -> 16 bytes
-rw-r--r--tests/posix/print0.sh2
-rw-r--r--tests/posix/prune.out3
-rw-r--r--tests/posix/prune.sh1
-rw-r--r--tests/posix/prune_error.out1
-rw-r--r--tests/posix/prune_error.sh1
-rw-r--r--tests/posix/prune_file.out10
-rw-r--r--tests/posix/prune_file.sh1
-rw-r--r--tests/posix/prune_or_print.out (renamed from tests/test_exclude_name.out)6
-rw-r--r--tests/posix/prune_or_print.sh1
-rw-r--r--tests/posix/readdir_error.sh37
-rw-r--r--tests/posix/root_order.out4
-rw-r--r--tests/posix/root_order.sh6
-rw-r--r--tests/posix/size.out (renamed from tests/test_size.out)0
-rw-r--r--tests/posix/size.sh1
-rw-r--r--tests/posix/size_bytes.out (renamed from tests/test_size_bytes.out)0
-rw-r--r--tests/posix/size_bytes.sh1
-rw-r--r--tests/posix/size_plus.out (renamed from tests/test_size_plus.out)0
-rw-r--r--tests/posix/size_plus.sh1
-rw-r--r--tests/posix/type_bind_mount.out1
-rw-r--r--tests/posix/type_bind_mount.sh9
-rw-r--r--tests/posix/type_d.out (renamed from tests/test_type_d.out)4
-rw-r--r--tests/posix/type_d.sh1
-rw-r--r--tests/posix/type_f.out7
-rw-r--r--tests/posix/type_f.sh1
-rw-r--r--tests/posix/type_l.out (renamed from tests/test_type_l.out)0
-rw-r--r--tests/posix/type_l.sh1
-rw-r--r--tests/posix/unionfs.out10
-rw-r--r--tests/posix/unionfs.sh9
-rw-r--r--tests/posix/user_id.out19
-rw-r--r--tests/posix/user_id.sh1
-rw-r--r--tests/posix/user_invalid_id.sh1
-rw-r--r--tests/posix/user_invalid_name.sh1
-rw-r--r--tests/posix/user_name.out19
-rw-r--r--tests/posix/user_name.sh1
-rw-r--r--tests/posix/user_nouser.out19
-rw-r--r--tests/posix/user_nouser.sh2
-rw-r--r--tests/posix/user_o_user.out19
-rw-r--r--tests/posix/user_o_user.sh3
-rw-r--r--tests/posix/weird_names.out (renamed from tests/test_weird_names.out)20
-rw-r--r--tests/posix/weird_names.sh2
-rw-r--r--tests/posix/xdev.out4
-rw-r--r--tests/posix/xdev.sh11
-rw-r--r--tests/ptyx.c252
-rw-r--r--tests/run.sh453
-rw-r--r--tests/sighook.c228
-rwxr-xr-xtests/sort-args.sh2
-rw-r--r--tests/stddirs.sh181
-rw-r--r--tests/test_L_acl.out2
-rw-r--r--tests/test_L_capable.out2
-rw-r--r--tests/test_L_delete.out2
-rw-r--r--tests/test_L_mount.out5
-rw-r--r--tests/test_L_xattr.out3
-rw-r--r--tests/test_L_xattrname.out2
-rw-r--r--tests/test_L_xdev.out5
-rw-r--r--tests/test_S_bfs.out19
-rw-r--r--tests/test_S_ids.out19
-rw-r--r--tests/test_acl.out1
-rw-r--r--tests/test_basic.out19
-rw-r--r--tests/test_capable.out1
-rw-r--r--tests/test_closed_stdin.out19
-rw-r--r--tests/test_color_ls.out12
-rw-r--r--tests/test_color_mi.out20
-rw-r--r--tests/test_color_nul.outbin20 -> 0 bytes
-rw-r--r--tests/test_color_or0_mi.out20
-rw-r--r--tests/test_color_or0_mi0.out20
-rw-r--r--tests/test_color_st0_tw0_ow0.out20
-rw-r--r--tests/test_color_star.out20
-rw-r--r--tests/test_d_path.out19
-rw-r--r--tests/test_data_flow_group.out19
-rw-r--r--tests/test_data_flow_hidden.out19
-rw-r--r--tests/test_data_flow_sparse.out19
-rw-r--r--tests/test_data_flow_user.out19
-rw-r--r--tests/test_daystart.out19
-rw-r--r--tests/test_daystart_twice.out19
-rw-r--r--tests/test_delete.out1
-rw-r--r--tests/test_delete_many.out1
-rw-r--r--tests/test_depth.out19
-rw-r--r--tests/test_depth_error.out2
-rw-r--r--tests/test_depth_overflow.out19
-rw-r--r--tests/test_empty_special.out14
-rw-r--r--tests/test_exec.out19
-rw-r--r--tests/test_exec_plus_status.out19
-rw-r--r--tests/test_executable.out4
-rw-r--r--tests/test_flags.out1
-rw-r--r--tests/test_fstype.out19
-rw-r--r--tests/test_gid.out19
-rw-r--r--tests/test_gid_minus.out19
-rw-r--r--tests/test_gid_minus_plus.out19
-rw-r--r--tests/test_gid_name.out19
-rw-r--r--tests/test_gid_plus.out19
-rw-r--r--tests/test_gid_plus_plus.out19
-rw-r--r--tests/test_group_id.out19
-rw-r--r--tests/test_group_name.out19
-rw-r--r--tests/test_group_nogroup.out19
-rw-r--r--tests/test_inum_bind_mount.out2
-rw-r--r--tests/test_inum_mount.out1
-rw-r--r--tests/test_mount.out4
-rw-r--r--tests/test_path_d.out19
-rw-r--r--tests/test_perm_000.out1
-rw-r--r--tests/test_perm_000_minus.out8
-rw-r--r--tests/test_perm_000_plus.out8
-rw-r--r--tests/test_perm_000_slash.out8
-rw-r--r--tests/test_perm_222.out1
-rw-r--r--tests/test_perm_222_minus.out1
-rw-r--r--tests/test_perm_222_plus.out5
-rw-r--r--tests/test_perm_222_slash.out5
-rw-r--r--tests/test_perm_644.out1
-rw-r--r--tests/test_perm_644_minus.out3
-rw-r--r--tests/test_perm_644_plus.out7
-rw-r--r--tests/test_perm_644_slash.out7
-rw-r--r--tests/test_perm_leading_plus_symbolic_slash.out7
-rw-r--r--tests/test_perm_symbolic_minus.out3
-rw-r--r--tests/test_perm_symbolic_slash.out7
-rw-r--r--tests/test_permcopy.out1
-rw-r--r--tests/test_printf_Y_error.out3
-rw-r--r--tests/test_printf_nul.outbin8 -> 0 bytes
-rw-r--r--tests/test_readable.out5
-rw-r--r--tests/test_rm.out1
-rw-r--r--tests/test_stderr_fails_silently.out19
-rw-r--r--tests/test_true.out19
-rw-r--r--tests/test_type_bind_mount.out1
-rw-r--r--tests/test_uid.out19
-rw-r--r--tests/test_uid_minus.out19
-rw-r--r--tests/test_uid_minus_plus.out19
-rw-r--r--tests/test_uid_name.out19
-rw-r--r--tests/test_uid_plus.out19
-rw-r--r--tests/test_uid_plus_plus.out19
-rw-r--r--tests/test_unique_depth.out19
-rw-r--r--tests/test_user_id.out19
-rw-r--r--tests/test_user_name.out19
-rw-r--r--tests/test_user_nouser.out19
-rw-r--r--tests/test_writable.out5
-rw-r--r--tests/test_xattr.out3
-rw-r--r--tests/test_xattrname.out2
-rw-r--r--tests/test_xdev.out4
-rw-r--r--tests/test_xtype_bind_mount.out2
-rw-r--r--tests/tests.h74
-rw-r--r--tests/tests.mk13
-rwxr-xr-xtests/tests.sh20
-rw-r--r--tests/trie.c90
-rw-r--r--tests/util.sh217
-rw-r--r--tests/xspawn.c220
-rw-r--r--tests/xspawnee.c17
-rw-r--r--tests/xtime.c187
-rw-r--r--tests/xtimegm.c91
-rw-r--r--tests/xtouch.c279
-rw-r--r--time.c323
-rw-r--r--time.h86
-rw-r--r--trie.c693
-rw-r--r--typo.h31
-rw-r--r--util.c428
-rw-r--r--util.h291
1302 files changed, 40801 insertions, 21014 deletions
diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 0000000..7d56722
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,4 @@
+coverage:
+ status:
+ patch: off
+ project: off
diff --git a/.github/codeql.yml b/.github/codeql.yml
new file mode 100644
index 0000000..a4271ec
--- /dev/null
+++ b/.github/codeql.yml
@@ -0,0 +1,13 @@
+query-filters:
+ - exclude:
+ id: cpp/commented-out-code
+ - exclude:
+ id: cpp/include-non-header
+ - exclude:
+ id: cpp/long-switch
+ - exclude:
+ id: cpp/loop-variable-changed
+ - exclude:
+ id: cpp/poorly-documented-function
+ - exclude:
+ id: cpp/constant-comparison
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/diag.sh b/.github/diag.sh
new file mode 100755
index 0000000..d89e7a4
--- /dev/null
+++ b/.github/diag.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Convert compiler diagnostics to GitHub Actions messages
+# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
+
+set -eu
+
+SEDFLAGS="-En"
+if sed -u 's/s/s/' </dev/null &>/dev/null; then
+ SEDFLAGS="${SEDFLAGS}u"
+fi
+
+filter() {
+ sed $SEDFLAGS 'p; s/^([^:]*):([^:]*):([^:]*): (warning|error): (.*)$/::\4 file=\1,line=\2,col=\3,title=Compiler \4::\5/p'
+}
+
+exec "$@" > >(filter) 2> >(filter >&2)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4075eb1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,255 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ linux-x86:
+ name: Linux (x86)
+ runs-on: ubuntu-24.04
+
+ # Don't run on both pushes and pull requests
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo dpkg --add-architecture i386
+ sudo apt-get update -y
+ sudo apt-get install -y \
+ mandoc \
+ gcc-multilib \
+ libgcc-s1:i386 \
+ acl \
+ libacl1-dev \
+ libacl1:i386 \
+ attr \
+ libcap2-bin \
+ libcap-dev \
+ libcap2:i386 \
+ libonig-dev \
+ libonig5:i386 \
+ liburing-dev
+ # Ubuntu doesn't let you install the -dev packages for both amd64 and
+ # i386 at once, so we make our own symlinks to fix -m32 -lacl -l...
+ sudo ln -s libacl.so.1 /lib/i386-linux-gnu/libacl.so
+ sudo ln -s libcap.so.2 /lib/i386-linux-gnu/libcap.so
+ sudo ln -s libonig.so.5 /lib/i386-linux-gnu/libonig.so
+
+ - name: Run tests
+ run: |
+ .github/diag.sh make -j$(nproc) distcheck
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-x86-config.log
+ path: distcheck-*/gen/config.log
+
+ linux-arm:
+ name: Linux (Arm64)
+ runs-on: ubuntu-24.04-arm
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update -y
+ sudo apt-get install -y \
+ mandoc \
+ acl \
+ libacl1-dev \
+ attr \
+ libcap2-bin \
+ libcap-dev \
+ libonig-dev \
+ liburing-dev
+
+ - name: Run tests
+ run: |
+ .github/diag.sh make -j$(nproc) distcheck
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-arm-config.log
+ path: distcheck-*/gen/config.log
+
+ macos:
+ name: macOS
+ runs-on: macos-15
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ brew install bash
+
+ - name: Run tests
+ run: |
+ jobs=$(sysctl -n hw.ncpu)
+ .github/diag.sh make -j$jobs distcheck
+
+ freebsd:
+ name: FreeBSD
+ runs-on: ubuntu-24.04
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: cross-platform-actions/action@v0.28.0
+ with:
+ operating_system: freebsd
+ version: "14.2"
+
+ run: |
+ sudo pkg install -y \
+ bash \
+ oniguruma \
+ pkgconf
+ sudo mount -t fdescfs none /dev/fd
+ .github/diag.sh make -j$(nproc) distcheck
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: freebsd-config.log
+ path: distcheck-*/gen/config.log
+
+ openbsd:
+ name: OpenBSD
+ runs-on: ubuntu-24.04
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: cross-platform-actions/action@v0.28.0
+ with:
+ operating_system: openbsd
+ version: "7.7"
+
+ run: |
+ sudo pkg_add \
+ bash \
+ gmake \
+ oniguruma
+ jobs=$(sysctl -n hw.ncpu)
+ ./configure MAKE=gmake
+ .github/diag.sh gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: openbsd-config.log
+ path: gen/config.log
+
+ netbsd:
+ name: NetBSD
+ runs-on: ubuntu-24.04
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: cross-platform-actions/action@v0.28.0
+ with:
+ operating_system: netbsd
+ version: "10.1"
+
+ run: |
+ PATH="/sbin:/usr/sbin:$PATH"
+ sudo pkgin -y install \
+ bash \
+ oniguruma \
+ pkgconf
+ jobs=$(sysctl -n hw.ncpu)
+ ./configure
+ .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: netbsd-config.log
+ path: gen/config.log
+
+ dragonflybsd:
+ name: DragonFly BSD
+ runs-on: ubuntu-24.04
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: vmactions/dragonflybsd-vm@v1
+ with:
+ release: "6.4.0"
+ usesh: true
+
+ prepare: |
+ pkg install -y \
+ bash \
+ oniguruma \
+ pkgconf \
+ sudo
+ pw useradd -n action -m -G wheel -s /usr/local/bin/bash
+ echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers
+
+ run: |
+ chown -R action:action .
+ jobs=$(sysctl -n hw.ncpu)
+ sudo -u action ./configure
+ sudo -u action .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dragonfly-config.log
+ path: gen/config.log
+
+ omnios:
+ name: OmniOS
+ runs-on: ubuntu-24.04
+
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: vmactions/omnios-vm@v1
+ with:
+ release: "r151052"
+ usesh: true
+
+ prepare: |
+ pkg install \
+ bash \
+ build-essential \
+ gnu-make \
+ onig \
+ sudo
+ useradd -m -g staff action
+ echo "%staff ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
+
+ run: |
+ PATH="/usr/xpg4/bin:$PATH"
+ chown -R action:staff .
+ jobs=$(getconf NPROCESSORS_ONLN)
+ sudo -u action ./configure MAKE=gmake
+ sudo -u action .github/diag.sh gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: omnios-config.log
+ path: gen/config.log
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
new file mode 100644
index 0000000..e4e8f71
--- /dev/null
+++ b/.github/workflows/codecov.yml
@@ -0,0 +1,34 @@
+name: codecov.io
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update -y
+ sudo apt-get install -y \
+ gcc \
+ acl \
+ libacl1-dev \
+ attr \
+ libcap2-bin \
+ libcap-dev \
+ libonig-dev \
+ liburing-dev
+
+ - name: Generate coverage
+ run: |
+ ./configure --enable-gcov
+ make -j$(nproc) check TEST_FLAGS="--sudo"
+ gcov -abcfpu obj/*/*.o
+
+ - uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..1f2041c
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,60 @@
+name: CodeQL
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ schedule:
+ - cron: "10 14 * * 2"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-24.04
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update -y
+ sudo apt-get install -y \
+ gcc \
+ acl \
+ libacl1-dev \
+ attr \
+ libcap2-bin \
+ libcap-dev \
+ libonig-dev \
+ liburing-dev
+
+ - name: Configure
+ run: |
+ ./configure
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: cpp
+ queries: +security-and-quality
+ config-file: .github/codeql.yml
+
+ - name: Build
+ run: |
+ make -j$(nproc) all
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:cpp"
diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml
deleted file mode 100644
index 74148bb..0000000
--- a/.github/workflows/freebsd.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: FreeBSD
-
-on: [push, pull_request]
-
-jobs:
- build:
- if: ${{ github.repository_owner == 'tavianator' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
-
- runs-on: ubuntu-latest
-
- concurrency: muon
-
- steps:
- - uses: actions/checkout@v2
-
- - uses: tailscale/github-action@main
- with:
- authkey: ${{ secrets.TAILSCALE_KEY }}
- version: 1.8.3
-
- - name: Configure SSH
- env:
- SSH_KEY: ${{ secrets.SSH_KEY }}
- run: |
- mkdir ~/.ssh
- printf '%s' "$SSH_KEY" >~/.ssh/github-actions
- chmod 0600 ~/.ssh/github-actions
- printf 'Host %s\n\tStrictHostKeyChecking=accept-new\n\tUser github\n\tIdentityFile ~/.ssh/github-actions\n' "$(tailscale ip -6 muon)" >~/.ssh/config
-
- - name: Run tests
- run: |
- muon=$(tailscale ip -6 muon)
- rsync -rl --delete . "[$muon]:bfs"
- ssh "$muon" 'gmake -C bfs -j$(sysctl -n hw.ncpu) distcheck'
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
deleted file mode 100644
index 67943f4..0000000
--- a/.github/workflows/linux.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Linux
-
-on: [push, pull_request]
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Install dependencies
- run: |
- sudo dpkg --add-architecture i386
- sudo apt-get update -y
- sudo apt-get install -y \
- gcc-multilib \
- acl \
- libacl1-dev \
- libacl1:i386 \
- attr \
- libattr1-dev \
- libattr1:i386 \
- libcap2-bin \
- libcap-dev \
- libcap2:i386
- # Ubuntu doesn't let you install the -dev packages for both amd64 and
- # i386 at once, so we make our own symlinks to fix -m32 -lacl -lattr -lcap
- sudo ln -s libacl.so.1 /lib/i386-linux-gnu/libacl.so
- sudo ln -s libattr.so.1 /lib/i386-linux-gnu/libattr.so
- sudo ln -s libcap.so.2 /lib/i386-linux-gnu/libcap.so
-
- - name: Run tests
- run: |
- make -j$(nproc) distcheck
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
deleted file mode 100644
index ab359e1..0000000
--- a/.github/workflows/macos.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: macOS
-
-on: [push, pull_request]
-
-jobs:
- build:
- runs-on: macos-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Run tests
- run: |
- make -j$(sysctl -n hw.ncpu) distcheck
diff --git a/.gitignore b/.gitignore
index 1424907..84e47fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,4 @@
-.flags
-*.o
-*.d
-*.gcda
-*.gcno
-/bfs
-/tests/mksock
-/tests/trie
-/tests/xtimegm
+/bin/
+/gen/
+/obj/
+/distcheck-*/
diff --git a/LICENSE b/LICENSE
index 069b145..b0b26e0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,12 +1,11 @@
-Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com>
+Copyright © 2015-2025 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted.
+Permission to use, copy, modify, and/or distribute this software for any purpose with or
+without fee is hereby granted.
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT
+SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
index 6c96fb8..5e6d25c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,231 +1,310 @@
-############################################################################
-# bfs #
-# Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
-
-ifeq ($(wildcard .git),)
-VERSION := 2.2.1
-else
-VERSION := $(shell git describe --always)
-endif
-
-ifndef OS
-OS := $(shell uname)
-endif
-
-ifndef ARCH
-ARCH := $(shell uname -m)
-endif
-
-CC ?= gcc
-INSTALL ?= install
-MKDIR ?= mkdir -p
-RM ?= rm -f
-
-DEFAULT_CFLAGS := \
- -g \
- -Wall \
- -Wmissing-declarations \
- -Wshadow \
- -Wsign-compare \
- -Wstrict-prototypes \
- -Wimplicit-fallthrough
-
-CFLAGS ?= $(DEFAULT_CFLAGS)
-LDFLAGS ?=
-DEPFLAGS ?= -MD -MP -MF $(@:.o=.d)
-
-DESTDIR ?=
-PREFIX ?= /usr
-MANDIR ?= $(PREFIX)/share/man
-
-LOCAL_CPPFLAGS := \
- -D__EXTENSIONS__ \
- -D_ATFILE_SOURCE \
- -D_BSD_SOURCE \
- -D_DARWIN_C_SOURCE \
- -D_DEFAULT_SOURCE \
- -D_FILE_OFFSET_BITS=64 \
- -D_GNU_SOURCE \
- -DBFS_VERSION=\"$(VERSION)\"
-
-LOCAL_CFLAGS := -std=c99
-LOCAL_LDFLAGS :=
-LOCAL_LDLIBS :=
-
-ASAN_CFLAGS := -fsanitize=address
-MSAN_CFLAGS := -fsanitize=memory -fsanitize-memory-track-origins
-UBSAN_CFLAGS := -fsanitize=undefined
-
-ifeq ($(OS),Linux)
-LOCAL_LDFLAGS += -Wl,--as-needed
-LOCAL_LDLIBS += -lacl -lcap -lattr -lrt
-
-# These libraries are not built with msan, so disable them
-MSAN_CFLAGS += -DBFS_HAS_SYS_ACL=0 -DBFS_HAS_SYS_CAPABILITY=0 -DBFS_HAS_SYS_XATTR=0
-
-DISTCHECK_FLAGS := TEST_FLAGS="--verbose --all --sudo"
-else
-DISTCHECK_FLAGS := TEST_FLAGS="--verbose"
-endif
-
-ifeq ($(OS),NetBSD)
-LOCAL_LDLIBS += -lutil
-endif
-
-ifneq ($(filter asan,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += $(ASAN_CFLAGS)
-SANITIZE := y
-endif
-
-ifneq ($(filter msan,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += $(MSAN_CFLAGS)
-SANITIZE := y
-endif
-
-ifneq ($(filter ubsan,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += $(UBSAN_CFLAGS)
-SANITIZE := y
-endif
-
-ifdef SANITIZE
-LOCAL_CFLAGS += -fno-sanitize-recover=all
-endif
-
-ifneq ($(filter gcov,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += --coverage
-endif
-
-ifneq ($(filter release,$(MAKECMDGOALS)),)
-CFLAGS := $(DEFAULT_CFLAGS) -O3 -flto -DNDEBUG
-endif
-
-ALL_CPPFLAGS = $(LOCAL_CPPFLAGS) $(CPPFLAGS)
-ALL_CFLAGS = $(ALL_CPPFLAGS) $(LOCAL_CFLAGS) $(CFLAGS) $(DEPFLAGS)
-ALL_LDFLAGS = $(ALL_CFLAGS) $(LOCAL_LDFLAGS) $(LDFLAGS)
-ALL_LDLIBS = $(LOCAL_LDLIBS) $(LDLIBS)
-
-# Save the full set of flags to rebuild everything when they change
-ALL_FLAGS := $(CC) : $(ALL_CFLAGS) : $(ALL_LDFLAGS) : $(ALL_LDLIBS)
-$(shell ./flags.sh $(ALL_FLAGS))
-
-# Goals that make binaries
-BIN_GOALS := bfs tests/mksock tests/trie tests/xtimegm
-
-# Goals that are treated like flags by this Makefile
-FLAG_GOALS := asan msan ubsan gcov release
-
-# These are the remaining non-flag goals
-GOALS := $(filter-out $(FLAG_GOALS),$(MAKECMDGOALS))
-
-# Build the default goal if only flag goals are specified
-FLAG_PREREQS :=
-ifndef GOALS
-FLAG_PREREQS += default
-endif
-
-# The different search strategies that we test
-STRATEGIES := bfs dfs ids eds
-STRATEGY_CHECKS := $(STRATEGIES:%=check-%)
-
-# All the different checks we run
-CHECKS := $(STRATEGY_CHECKS) check-trie check-xtimegm
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
-default: bfs
+# To build bfs, run
+#
+# $ ./configure
+# $ make
-all: $(BIN_GOALS)
-
-bfs: \
- bar.o \
- bftw.o \
- color.o \
- ctx.o \
- darray.o \
- diag.o \
- dir.o \
- dstring.o \
- eval.o \
- exec.o \
- fsade.o \
- main.o \
- mtab.o \
- opt.o \
- parse.o \
- printf.o \
- pwcache.o \
- spawn.o \
- stat.o \
- time.o \
- trie.o \
- typo.o \
- util.o
-
-tests/mksock: tests/mksock.o
-tests/trie: trie.o tests/trie.o
-tests/xtimegm: time.o tests/xtimegm.o
-
-$(BIN_GOALS):
- +$(CC) $(ALL_LDFLAGS) $^ $(ALL_LDLIBS) -o $@
-
-%.o: %.c .flags
- $(CC) $(ALL_CFLAGS) -c $< -o $@
-
-# Need a rule for .flags to convince make to apply the above pattern rule if
-# .flags didn't exist when make was run
-.flags:
-
-# Make sure that "make release" builds everything, but "make release main.o" doesn't
-$(FLAG_GOALS): $(FLAG_PREREQS)
- @:
-
-check: $(CHECKS)
-
-$(STRATEGY_CHECKS): check-%: bfs tests/mksock
- ./tests.sh --bfs="$(CURDIR)/bfs -S $*" $(TEST_FLAGS)
-
-check-trie check-xtimegm: check-%: tests/%
- $<
+# Utilities and GNU/BSD portability
+include build/prelude.mk
+# The default build target
+default: bfs
+.PHONY: default
+
+# Include the generated build config, if it exists
+-include gen/config.mk
+
+## Configuration phase (`./configure`)
+
+# bfs used to have flag-like targets (`make release`, `make asan ubsan`, etc.).
+# Direct users to the new configuration system.
+asan lsan msan tsan ubsan gcov lint release::
+ @printf 'error: `%s %s` is no longer supported. Use `./configure --enable-%s` instead.\n' \
+ "${MAKE}" $@ $@ >&2
+ @false
+
+# Print an error if `make` is run before `./configure`
+gen/config.mk::
+ if ! [ -e $@ ]; then \
+ printf 'error: You must run `./configure` before `%s`.\n' "${MAKE}" >&2; \
+ false; \
+ fi
+.SILENT: gen/config.mk
+
+## Build phase (`make`)
+
+# The main binary
+bfs: bin/bfs
+.PHONY: bfs
+
+# All binaries
+BINS := \
+ bin/bfs \
+ bin/tests/mksock \
+ bin/tests/ptyx \
+ bin/tests/units \
+ bin/tests/xspawnee \
+ bin/tests/xtouch \
+ bin/bench/ioq
+
+all: ${BINS}
+.PHONY: all
+
+# All object files except the entry point
+LIBBFS := \
+ obj/src/alloc.o \
+ obj/src/bar.o \
+ obj/src/bfstd.o \
+ obj/src/bftw.o \
+ obj/src/color.o \
+ obj/src/ctx.o \
+ obj/src/diag.o \
+ obj/src/dir.o \
+ obj/src/dstring.o \
+ obj/src/eval.o \
+ obj/src/exec.o \
+ obj/src/expr.o \
+ obj/src/fsade.o \
+ obj/src/ioq.o \
+ obj/src/mtab.o \
+ obj/src/opt.o \
+ obj/src/parse.o \
+ obj/src/printf.o \
+ obj/src/pwcache.o \
+ obj/src/sighook.o \
+ obj/src/stat.o \
+ obj/src/thread.o \
+ obj/src/trie.o \
+ obj/src/typo.o \
+ obj/src/version.o \
+ obj/src/xregex.o \
+ obj/src/xspawn.o \
+ obj/src/xtime.o
+
+# All object files
+OBJS := ${LIBBFS}
+
+# The main binary
+bin/bfs: obj/src/main.o ${LIBBFS}
+OBJS += obj/src/main.o
+
+${BINS}:
+ @${MKDIR} ${@D}
+ +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} $^ ${_LDLIBS} -o $@
+ ${POSTLINK}
+
+# Get the .c file for a .o file
+CSRC = ${@:obj/%.o=%.c}
+
+# Save the version number to this file, but only update version.c if it changes
+gen/version.i.new::
+ ${MKDIR} ${@D}
+ build/version.sh | tr -d '\n' | build/embed.sh >$@
+.SILENT: gen/version.i.new
+
+gen/version.i: gen/version.i.new
+ test -e $@ && cmp -s $@ $^ && ${RM} $^ || mv $^ $@
+.SILENT: gen/version.i
+
+obj/src/version.o: gen/version.i
+
+## Test phase (`make check`)
+
+# Unit test binaries
+UTEST_BINS := \
+ bin/tests/units \
+ bin/tests/xspawnee
+
+# Integration test binaries
+ITEST_BINS := \
+ bin/tests/mksock \
+ bin/tests/ptyx \
+ bin/tests/xtouch
+
+# Build (but don't run) test binaries
+tests: ${UTEST_BINS} ${ITEST_BINS}
+.PHONY: tests
+
+# Run all the tests
+check: unit-tests integration-tests
+.PHONY: check
+
+# Run the unit tests
+unit-tests: ${UTEST_BINS}
+ ${MSG} "[TEST] tests/units" bin/tests/units
+.PHONY: unit-tests
+
+# Unit test objects
+UNIT_OBJS := \
+ obj/tests/alloc.o \
+ obj/tests/bfstd.o \
+ obj/tests/bit.o \
+ obj/tests/ioq.o \
+ obj/tests/list.o \
+ obj/tests/main.o \
+ obj/tests/sighook.o \
+ obj/tests/trie.o \
+ obj/tests/xspawn.o \
+ obj/tests/xtime.o
+
+bin/tests/units: ${UNIT_OBJS} ${LIBBFS}
+OBJS += ${UNIT_OBJS}
+
+bin/tests/xspawnee: obj/tests/xspawnee.o
+OBJS += obj/tests/xspawnee.o
+
+# The different flag combinations we check
+INTEGRATIONS := default dfs ids eds j1 j2 j3 s
+INTEGRATION_TESTS := ${INTEGRATIONS:%=check-%}
+
+# Check just `bfs`
+check-default: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs" ${TEST_FLAGS}
+
+# Check the different search strategies
+check-dfs check-ids check-eds: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs -S ${@:check-%=%}" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -S ${@:check-%=%}" ${TEST_FLAGS}
+
+# Check various flags
+check-j1 check-j2 check-j3 check-s: bin/bfs ${ITEST_BINS}
+ +${MSG} "[TEST] bfs -${@:check-%=%}" \
+ ./tests/tests.sh --make="${MAKE}" --bfs="bin/bfs -${@:check-%=%}" ${TEST_FLAGS}
+
+# Run the integration tests
+integration-tests: ${INTEGRATION_TESTS}
+.PHONY: integration-tests
+
+bin/tests/mksock: obj/tests/mksock.o ${LIBBFS}
+OBJS += obj/tests/mksock.o
+
+bin/tests/ptyx: obj/tests/ptyx.o ${LIBBFS}
+OBJS += obj/tests/ptyx.o
+
+bin/tests/xtouch: obj/tests/xtouch.o ${LIBBFS}
+OBJS += obj/tests/xtouch.o
+
+# `make distcheck` configurations
+DISTCHECKS := \
+ distcheck-asan \
+ distcheck-msan \
+ distcheck-tsan \
+ distcheck-m32 \
+ distcheck-release
+
+# Test multiple configurations
distcheck:
- +$(MAKE) -B asan ubsan check $(DISTCHECK_FLAGS)
-ifneq ($(OS),Darwin)
- +$(MAKE) -B msan check CC=clang $(DISTCHECK_FLAGS)
-ifeq ($(ARCH),x86_64)
- +$(MAKE) -B check CFLAGS="-m32" $(DISTCHECK_FLAGS)
-endif
-endif
- +$(MAKE) -B release check $(DISTCHECK_FLAGS)
- +$(MAKE) -B check $(DISTCHECK_FLAGS)
-
-clean:
- $(RM) $(BIN_GOALS) .flags *.[od] *.gcda *.gcno tests/*.[od] tests/*.gcda tests/*.gcno
-
-install:
- $(MKDIR) $(DESTDIR)$(PREFIX)/bin
- $(INSTALL) -m755 bfs $(DESTDIR)$(PREFIX)/bin/bfs
- $(MKDIR) $(DESTDIR)$(MANDIR)/man1
- $(INSTALL) -m644 bfs.1 $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/bash-completion/completions
- $(INSTALL) -m644 completions/bfs.bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
-
-uninstall:
- $(RM) $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
- $(RM) $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(RM) $(DESTDIR)$(PREFIX)/bin/bfs
-
-.PHONY: default all $(FLAG_GOALS) check $(CHECKS) distcheck clean install uninstall
-
-.SUFFIXES:
-
--include $(wildcard *.d)
+ @+${MAKE} distcheck-asan
+ @+test "$$(uname)" = Darwin || ${MAKE} distcheck-msan
+ @+test "$$(uname)" = FreeBSD || ${MAKE} distcheck-tsan
+ @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} distcheck-m32
+ @+${MAKE} distcheck-release
+ @+${MAKE} -C distcheck-release check-install
+ @+test "$$(uname)" != Linux || ${MAKE} check-man
+.PHONY: distcheck
+
+# Per-distcheck configuration
+DISTCHECK_CONFIG_asan := --enable-asan --enable-ubsan
+DISTCHECK_CONFIG_msan := --enable-msan --enable-ubsan CC=clang
+DISTCHECK_CONFIG_tsan := --enable-tsan --enable-ubsan CC=clang
+DISTCHECK_CONFIG_m32 := EXTRA_CFLAGS="-m32" PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig
+DISTCHECK_CONFIG_release := --enable-release
+
+${DISTCHECKS}::
+ @${MKDIR} $@
+ @test "$${GITHUB_ACTIONS-}" != true || printf '::group::%s\n' $@
+ @+cd $@ \
+ && ../configure MAKE="${MAKE}" ${DISTCHECK_CONFIG_${@:distcheck-%=%}} \
+ && ${MAKE} check TEST_FLAGS="--sudo --verbose=skipped"
+ @test "$${GITHUB_ACTIONS-}" != true || printf '::endgroup::\n'
+
+## Benchmarks (`make bench`)
+
+bench: bin/bench/ioq
+.PHONY: bench
+
+bin/bench/ioq: obj/bench/ioq.o ${LIBBFS}
+OBJS += obj/bench/ioq.o
+
+## Automatic dependency tracking
+
+# Rebuild when the configuration changes
+${OBJS}: gen/config.mk
+ @${MKDIR} ${@D}
+ ${MSG} "[ CC ] ${CSRC}" ${CC} ${_CPPFLAGS} ${_CFLAGS} -c ${CSRC} -o $@
+
+# Include any generated dependency files
+-include ${OBJS:.o=.d}
+
+## Packaging (`make dist`, `make install`)
+
+TARBALL = bfs-$$(build/version.sh).tar.gz
+
+dist:
+ ${MSG} "[DIST] ${TARBALL}" git archive HEAD -o ${TARBALL}
+
+distsign: dist
+ ${MSG} "[SIGN] ${TARBALL}" ssh-keygen -Y sign -q -f $$(git config user.signingkey) -n file ${TARBALL}
+
+.PHONY: dist distsign
+
+DEST_PREFIX := ${DESTDIR}${PREFIX}
+DEST_MANDIR := ${DESTDIR}${MANDIR}
+
+install::
+ ${Q}${MKDIR} ${DEST_PREFIX}/bin
+ ${MSG} "[INST] bin/bfs" \
+ ${INSTALL} -m755 bin/bfs ${DEST_PREFIX}/bin/bfs
+ ${Q}${MKDIR} ${DEST_MANDIR}/man1
+ ${MSG} "[INST] man/man1/bfs.1" \
+ ${INSTALL} -m644 docs/bfs.1 ${DEST_MANDIR}/man1/bfs.1
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/bash-completion/completions
+ ${MSG} "[INST] completions/bfs.bash" \
+ ${INSTALL} -m644 completions/bfs.bash ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/zsh/site-functions
+ ${MSG} "[INST] completions/bfs.zsh" \
+ ${INSTALL} -m644 completions/bfs.zsh ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/fish/vendor_completions.d
+ ${MSG} "[INST] completions/bfs.fish" \
+ ${INSTALL} -m644 completions/bfs.fish ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+
+uninstall::
+ ${MSG} "[ RM ] completions/bfs.bash" \
+ ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${MSG} "[ RM ] completions/bfs.zsh" \
+ ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${MSG} "[ RM ] completions/bfs.fish" \
+ ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+ ${MSG} "[ RM ] man/man1/bfs.1" \
+ ${RM} ${DEST_MANDIR}/man1/bfs.1
+ ${MSG} "[ RM ] bin/bfs" \
+ ${RM} ${DEST_PREFIX}/bin/bfs
+
+# Check that `make install` works and `make uninstall` removes everything
+check-install::
+ +${MAKE} install DESTDIR=pkg
+ +${MAKE} uninstall DESTDIR=pkg
+ bin/bfs pkg -not -type d -print -exit 1
+ ${RM} -r pkg
+
+# Check man page markup
+check-man::
+ ${MSG} "[LINT] docs/bfs.1"
+ ${Q}groff -man -rCHECKSTYLE=3 -ww -b -z docs/bfs.1
+ ${Q}mandoc -Tlint -Wwarning docs/bfs.1
+
+## Cleanup (`make clean`)
+
+# Clean all build products
+clean::
+ ${MSG} "[ RM ] bin obj" \
+ ${RM} -r bin obj
+
+# Clean everything, including generated files
+distclean: clean
+ ${MSG} "[ RM ] gen distcheck-*" \
+ ${RM} -r gen ${DISTCHECKS}
+.PHONY: distclean
diff --git a/README.md b/README.md
index d613834..ad1fd09 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,60 @@
-`bfs`
-=====
-
-[![License](http://img.shields.io/badge/license-0BSD-blue.svg)](https://github.com/tavianator/bfs/blob/main/LICENSE)
-[![Version](https://img.shields.io/github/v/tag/tavianator/bfs?label=version)](https://github.com/tavianator/bfs/releases)
-[![Linux CI Status](https://github.com/tavianator/bfs/actions/workflows/linux.yml/badge.svg?branch=main)](https://github.com/tavianator/bfs/actions/workflows/linux.yml)
-[![macOS CI Status](https://github.com/tavianator/bfs/actions/workflows/macos.yml/badge.svg?branch=main)](https://github.com/tavianator/bfs/actions/workflows/macos.yml)
-[![FreeBSD CI Status](https://github.com/tavianator/bfs/actions/workflows/freebsd.yml/badge.svg?branch=main)](https://github.com/tavianator/bfs/actions/workflows/freebsd.yml)
+<div align="center">
+
+<h1>
+<code>bfs</code>
+<br clear="all">
+<a href="https://github.com/tavianator/bfs/releases"><img src="https://img.shields.io/github/v/tag/tavianator/bfs?label=version" alt="Version" align="left"></a>
+<a href="/LICENSE"><img src="https://img.shields.io/badge/license-0BSD-blue.svg" alt="License" align="left"></a>
+<a href="https://github.com/tavianator/bfs/actions/workflows/ci.yml"><img src="https://github.com/tavianator/bfs/actions/workflows/ci.yml/badge.svg" alt="CI Status" align="right"></a>
+<a href="https://codecov.io/gh/tavianator/bfs"><img src="https://img.shields.io/codecov/c/github/tavianator/bfs?token=PpBVuozOVC" alt="Code coverage" align="right"/></a>
+</h1>
+
+**[Features]   •   [Installation]   •   [Usage]   •   [Building]   •   [Contributing]   •   [Changelog]**
+
+[Features]: #features
+[Installation]: #installation
+[Usage]: /docs/USAGE.md
+[Building]: /docs/BUILDING.md
+[Contributing]: /docs/CONTRIBUTING.md
+[Changelog]: /docs/CHANGELOG.md
+
+<picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-dark.svg">
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg">
+ <img alt="Screencast" src="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg">
+</picture>
+<p></p>
+
+</div>
+
+`bfs` is a variant of the UNIX `find` command that operates [**breadth-first**](https://en.wikipedia.org/wiki/Breadth-first_search) rather than [**depth-first**](https://en.wikipedia.org/wiki/Depth-first_search).
+It is otherwise compatible with many versions of `find`, including
-Breadth-first search for your files.
+<div align="center">
-<img src="https://tavianator.github.io/bfs/animation.svg" alt="Screenshot" />
+**[POSIX]   •   [GNU]   •   [FreeBSD]   •   [OpenBSD]   •   [NetBSD]   •   [macOS]**
-`bfs` is a variant of the UNIX `find` command that operates [breadth-first](https://en.wikipedia.org/wiki/Breadth-first_search) rather than [depth-first](https://en.wikipedia.org/wiki/Depth-first_search).
-It is otherwise compatible with many versions of `find`, including
+[POSIX]: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html
+[GNU]: https://www.gnu.org/software/findutils/
+[FreeBSD]: https://www.freebsd.org/cgi/man.cgi?find(1)
+[OpenBSD]: https://man.openbsd.org/find.1
+[NetBSD]: https://man.netbsd.org/find.1
+[macOS]: https://ss64.com/osx/find.html
-- [POSIX `find`](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html)
-- [GNU `find`](https://www.gnu.org/software/findutils/)
-- {[Free](https://www.freebsd.org/cgi/man.cgi?find(1)),[Open](https://man.openbsd.org/find.1),[Net](https://man.netbsd.org/find.1)}BSD `find`
-- [macOS `find`](https://ss64.com/osx/find.html)
+</div>
If you're not familiar with `find`, the [GNU find manual](https://www.gnu.org/software/findutils/manual/html_mono/find.html) provides a good introduction.
-Breadth vs. depth
------------------
+Features
+--------
+
+<details>
+<summary>
+<code>bfs</code> operates breadth-first, which typically finds the file(s) you're looking for faster.
+</summary>
+<p></p>
-The advantage of breadth-first over depth first search is that it usually finds the file(s) you're looking for faster.
Imagine the following directory tree:
<pre>
@@ -41,112 +70,284 @@ haystack
</pre>
`find` will explore the entire `deep` directory tree before it ever gets to the `shallow` one that contains what you're looking for.
+On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree.
-<pre>
-$ <strong>find</strong> haystack
+<table>
+<tbody>
+<tr><th><code>bfs</code></th><th><code>find</code></th></tr>
+<tr>
+<td width="506" valign="top">
+
+```console
+$ bfs haystack
haystack
haystack/deep
+haystack/shallow
haystack/deep/1
-haystack/deep/1/2
-haystack/deep/1/2/3
-haystack/deep/1/2/3/4
+haystack/shallow/needle
...
-haystack/shallow
-<strong>haystack/shallow/needle</strong>
-</pre>
+```
-On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree.
+</td>
+<td width="506" valign="top">
-<pre>
-$ <strong>bfs</strong> haystack
+```console
+$ find haystack
haystack
haystack/deep
-haystack/shallow
haystack/deep/1
-<strong>haystack/shallow/needle</strong>
haystack/deep/1/2
haystack/deep/1/2/3
haystack/deep/1/2/3/4
...
-</pre>
+haystack/shallow
+haystack/shallow/needle
+```
+</td>
+</tr>
+</tbody>
+</table>
+</details>
-Easy
-----
+<details>
+<summary>
+<code>bfs</code> tries to be easier to use than <code>find</code>, while remaining compatible.
+</summary>
+<p></p>
-`bfs` tries to be easier to use than `find`, while remaining compatible.
For example, `bfs` is less picky about where you put its arguments:
-<pre>
-$ <strong>find</strong> -L -name 'needle' <em>haystack</em>
+<table>
+<tbody>
+<tr><th><code>bfs</code></th><th><code>find</code></th></tr>
+<tr>
+<td width="506">
+
+```console
+$ bfs -L -name 'needle' haystack
+haystack/needle
+
+$ bfs haystack -L -name 'needle'
+haystack/needle
+
+$ bfs -L haystack -name 'needle'
+haystack/needle
+```
+
+</td>
+<td width="506">
+
+```console
+$ find -L -name 'needle' haystack
find: paths must precede expression: haystack
-$ <strong>bfs</strong> -L -name 'needle' <em>haystack</em>
-<strong>haystack/needle</strong>
-$ <strong>find</strong> <em>haystack</em> -L -name 'needle'
+$ find haystack -L -name 'needle'
find: unknown predicate `-L'
-$ <strong>bfs</strong> <em>haystack</em> -L -name 'needle'
-<strong>haystack/needle</strong>
-$ <strong>find</strong> -L <em>haystack</em> -name 'needle'
-<strong>haystack/needle</strong>
-$ <strong>bfs</strong> -L <em>haystack</em> -name 'needle'
-<strong>haystack/needle</strong>
+$ find -L haystack -name 'needle'
+haystack/needle
+```
+
+</td>
+</tr>
+</tbody>
+</table>
+</details>
+
+<details>
+<summary>
+<code>bfs</code> gives helpful errors and warnings.
+</summary>
+<p></p>
+
+For example, `bfs` will detect and suggest corrections for typos:
+
+```console
+$ bfs -nam needle
+bfs: error: bfs -nam needle
+bfs: error: ~~~~
+bfs: error: Unknown argument; did you mean -name?
+```
+
+`bfs` also includes a powerful static analysis to help catch mistakes:
+
+```console
+$ bfs -print -name 'needle'
+bfs: warning: bfs -print -name needle
+bfs: warning: ~~~~~~~~~~~~
+bfs: warning: The result of this expression is ignored.
+```
+
+</details>
+
+<details>
+<summary>
+<code>bfs</code> adds some options that make common tasks easier.
+</summary>
+<p></p>
+
+For example, the `-exclude` operator skips over entire subtrees whenever an expression matches.
+`-exclude` is both more powerful and easier to use than the standard `-prune` action; compare
+
+<pre>
+$ bfs -name config <strong>-exclude -name .git</strong>
</pre>
-`bfs` also adds some extra options that make some common tasks easier.
-Compare
+to the equivalent
+
+<pre>
+$ find <strong>! \( -name .git -prune \)</strong> -name config
+</pre>
- bfs -name config -exclude -name .git
+As an additional shorthand, `-nohidden` skips over all hidden files and directories.
+See the [usage documentation](/docs/USAGE.md#extensions) for more about the extensions provided by `bfs`.
+</details>
-vs.
- find ! \( -name .git -prune \) -name config
+Installation
+------------
+<details open>
+<summary>
+<code>bfs</code> may already be packaged for your operating system.
+</summary>
+<p></p>
-Try it!
--------
+<table>
+<tbody>
+<tr><th>Linux</th><th>macOS</th></tr>
-`bfs` may already be packaged for your distribution of choice.
-For example:
+<tr>
+<td width="506" valign="top" rowspan="3">
<pre>
-<strong>Alpine Linux</strong>
+<strong><a href="https://pkgs.alpinelinux.org/packages?name=bfs">Alpine Linux</a></strong>
# apk add bfs
-<strong>Debian/Ubuntu</strong>
+<strong><a href="https://archlinux.org/packages/extra/x86_64/bfs/">Arch Linux</a></strong>
+# pacman -S bfs
+
+<strong><a href="https://packages.debian.org/sid/bfs">Debian</a>/<a href="https://packages.ubuntu.com/kinetic/bfs">Ubuntu</a></strong>
# apt install bfs
-<strong>NixOS</strong>
+<strong><a href="https://src.fedoraproject.org/rpms/bfs">Fedora Linux</a></strong>
+# dnf install bfs
+
+<strong><a href="https://packages.gentoo.org/packages/sys-apps/bfs">Gentoo</a></strong>
+# emerge sys-apps/bfs
+
+<strong><a href="https://packages.guix.gnu.org/packages/bfs/">GNU Guix</a></strong>
+# guix install bfs
+
+<strong><a href="https://search.nixos.org/packages?channel=unstable&show=bfs&from=0&size=1&sort=relevance&type=packages&query=bfs">NixOS</a></strong>
# nix-env -i bfs
-<strong>Void Linux</strong>
+<strong><a href="https://voidlinux.org/packages/?arch=x86_64&q=bfs">Void Linux</a></strong>
# xbps-install -S bfs
+</pre>
-<strong>FreeBSD</strong>
-# pkg install bfs
+</td>
+<td width="506" valign="top">
-<strong>MacPorts</strong>
+<pre>
+<strong><a href="https://formulae.brew.sh/formula/bfs">Homebrew</a></strong>
+$ brew install bfs
+
+<strong><a href="https://ports.macports.org/port/bfs/">MacPorts</a></strong>
# port install bfs
+</pre>
+
+</td>
+</tr>
+<tr><th height="1">BSD</th></tr>
+<tr>
+<td width="506" valign="top">
+
+<pre>
+<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong>
+# pkg install bfs
+
+<strong><a href="https://openports.pl/path/sysutils/bfs">OpenBSD</a></strong>
+# pkg_add bfs
+</pre>
+
+</td>
+</tr>
+</tbody>
+</table>
+</details>
+
+<details>
+<summary>
+To build <code>bfs</code> from source, you may need to install some dependencies.
+</summary>
+<p></p>
+
+The only absolute requirements for building `bfs` are a C compiler, [GNU make](https://www.gnu.org/software/make/), and [Bash](https://www.gnu.org/software/bash/).
+These are installed by default on many systems, and easy to install on most others.
+Refer to your operating system's documentation on building software.
+
+`bfs` also depends on some system libraries for some of its features.
+Here's how to install them on some common platforms:
+
+<pre>
+<strong>Alpine Linux</strong>
+# apk add acl{,-dev} attr libcap{,-dev} liburing-dev oniguruma-dev
+
+<strong>Arch Linux</strong>
+# pacman -S acl attr libcap liburing oniguruma
+
+<strong>Debian/Ubuntu</strong>
+# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev liburing-dev libonig-dev
+
+<strong>Fedora</strong>
+# dnf install acl libacl-devel attr libcap-devel liburing-devel oniguruma-devel
+
+<strong>NixOS</strong>
+# nix-env -i acl attr libcap liburing oniguruma
+
+<strong>Void Linux</strong>
+# xbps-install -S acl-{devel,progs} attr-progs libcap-{devel,progs} liburing-devel oniguruma-devel
<strong>Homebrew</strong>
-$ brew install tavianator/tap/bfs
+$ brew install oniguruma
+
+<strong>MacPorts</strong>
+# port install oniguruma6
+
+<strong>FreeBSD</strong>
+# pkg install oniguruma
</pre>
-To install `bfs` from source, download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs).
+These dependencies are technically optional, though strongly recommended.
+See the [build documentation](/docs/BUILDING.md#dependencies) for how to disable them.
+</details>
+
+<details>
+<summary>
+Once you have the dependencies, you can build <code>bfs</code>.
+</summary>
+<p></p>
+
+Download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs).
Then run
+ $ ./configure
$ make
-This will build the `bfs` binary in the current directory.
-You can test it out:
+This will build the `./bin/bfs` binary.
+Run the test suite to make sure it works correctly:
- $ ./bfs -nohidden
+ $ make check
If you're interested in speed, you may want to build the release version instead:
- $ make release
+ $ ./configure --enable-release
+ $ make
Finally, if you want to install it globally, run
- $ sudo make install
+ # make install
+
+</details>
diff --git a/RELEASES.md b/RELEASES.md
deleted file mode 100644
index 6400788..0000000
--- a/RELEASES.md
+++ /dev/null
@@ -1,559 +0,0 @@
-2.*
-===
-
-2.2.1
------
-
-**June 2, 2021**
-
-- Fixed some incorrect coloring of broken links when links are being followed (`-L`)
-
-- Made the tests work when run as root by dropping privileges.
- This may be helpful for certain packaging or CI environments, but is not recommended.
-
-- Treat empty `PAGER` and `LESS` environment variables like they're unset, for `bfs -help` ([#71]).
- Thanks @markus-oberhumer!
-
-- The soft `RLIMIT_NOFILE` is now raised automatically to a fairly large value when possible.
- This provides a minor performance benefit for large directory trees.
-
-- Implemented time units for `-mtime` as found in FreeBSD find ([#75])
-
-[#71]: https://github.com/tavianator/bfs/issues/71
-[#75]: https://github.com/tavianator/bfs/issues/75
-
-2.2
----
-
-**March 6, 2021**
-
-- Fixed `-hidden` on hidden start paths
-
-- Added a Bash completion script.
- Thanks @bmundt6!
-
-- Fixed rounding in `-used`.
- Corresponding fixes were made to GNU find in version 4.8.0.
-
-- Optimized the open directory representation.
- On Linux, much libc overhead is bypassed by issuing syscalls directly.
- On all platforms, a few fewer syscalls and open file descriptors will be used.
-
-- Implemented `-flags` from BSD find
-
-
-2.1
----
-
-**November 11, 2020**
-
-- Added a new `-status` option that displays the search progress in a bar at the bottom of the terminal
-
-- Fixed an optimizer bug introduced in version 2.0 that affected some combinations of `-user`/`-group` and `-nouser`/`-nogroup`
-
-
-2.0
----
-
-**October 14, 2020**
-
-- [#8]: New `-exclude <expression>` syntax to more easily and reliably filter out paths.
- For example:
-
- bfs -name config -exclude -name .git
-
- will find all files named `config`, without searching any directories (or files) named `.git`.
- In this case, the same effect could have been achieved (more awkwardly) with `-prune`:
-
- bfs ! \( -name .git -prune \) -name config
-
- But `-exclude` will work in more cases:
-
- # -exclude works with -depth, while -prune doesn't:
- bfs -depth -name config -exclude -name .git
-
- # -exclude applies even to paths below the minimum depth:
- bfs -mindepth 3 -name config -exclude -name .git
-
-- [#30]: `-nohidden` is now equivalent to `-exclude -hidden`.
- This changes the behavior of command lines like
-
- bfs -type f -nohidden
-
- to do what was intended.
-
-- Optimized the iterative deepening (`-S ids`) implementation
-
-- Added a new search strategy: exponential deepening search (`-S eds`).
- This strategy provides many of the benefits of iterative deepening, but much faster due to fewer re-traversals.
-
-- Fixed an optimizer bug that could skip `-empty`/`-xtype` if they didn't always lead to an action
-
-- Implemented `-xattrname` to find files with a particular extended attribute (from macOS find)
-
-- Made `-printf %l` still respect the width specifier (e.g. `%10l`) for non-links, to match GNU find
-
-- Made `bfs` fail if `-color` is given explicitly and `LS_COLORS` can't be parsed, rather than falling back to non-colored output
-
-[#8]: https://github.com/tavianator/bfs/issues/8
-[#30]: https://github.com/tavianator/bfs/issues/30
-
-
-1.*
-===
-
-1.7
----
-
-**April 22, 2020**
-
-- Fixed `-ls` printing numeric IDs instead of user/group names in large directory trees
-- Cached the user and group tables for a performance boost
-- Fixed interpretation of "default" ACLs
-- Implemented `-s` flag to sort results
-
-
-1.6
----
-
-**February 25, 2020**
-
-- Implemented `-newerXt` (explicit reference times), `-since`, `-asince`, etc.
-- Fixed `-empty` to skip special files (pipes, devices, sockets, etc.)
-
-
-1.5.2
------
-
-**January 9, 2020**
-
-- Fixed the build on NetBSD
-- Added support for NFSv4 ACLs on FreeBSD
-- Added a `+` after the file mode for files with ACLs in `-ls`
-- Supported more file types (whiteouts, doors) in symbolic modes for `-ls`/`-printf %M`
-- Implemented `-xattr` on FreeBSD
-
-
-1.5.1
------
-
-**September 14, 2019**
-
-- Added a warning to `-mount`, since it will change behaviour in the next POSIX revision
-- Added a workaround for environments that block `statx()` with `seccomp()`, like older Docker
-- Fixed coloring of nonexistent leading directories
-- Avoided calling `stat()` on all mount points at startup
-
-
-1.5
----
-
-**June 27, 2019**
-
-- New `-xattr` predicate to find files with extended attributes
-- Fixed the `-acl` implementation on macOS
-- Implemented depth-first (`-S dfs`) and iterative deepening search (`-S ids`)
-- Piped `-help` output into `$PAGER` by default
-- Fixed crashes on some invalid `LS_COLORS` values
-
-
-1.4.1
------
-
-**April 5, 2019**
-
-- Added a nicer error message when the tests are run as root
-- Fixed detection of comparison expressions with signs, to match GNU find for things like `-uid ++10`
-- Added support for https://no-color.org/
-- Decreased the number of `stat()` calls necessary in some cases
-
-
-1.4
----
-
-**April 15, 2019**
-
-- New `-unique` option that filters out duplicate files ([#48])
-- Optimized the file coloring implementation
-- Fixed the coloring implementation to match GNU ls more closely in many corner cases
- - Implemented escape sequence parsing for `LS_COLORS`
- - Implemented `ln=target` for coloring links like their targets
- - Fixed the order of fallbacks used when some color keys are unset
-- Add a workaround for incorrect file types for bind-mounted files on Linux ([#37])
-
-[#48]: https://github.com/tavianator/bfs/issues/48
-[#37]: https://github.com/tavianator/bfs/issues/37
-
-
-1.3.3
------
-
-**February 10, 2019**
-
-- Fixed unpredictable behaviour for empty responses to `-ok`/`-okdir` caused by an uninitialized string
-- Writing to standard output now causes `bfs` to fail if the descriptor was closed
-- Fixed incomplete file coloring in error messages
-- Added some data flow optimizations
-- Fixed `-nogroup`/`-nouser` in big directory trees
-- Added `-type w` for whiteouts, as supported by FreeBSD `find`
-- Re-wrote the `-help` message and manual page
-
-
-1.3.2
------
-
-**January 11, 2019**
-
-- Fixed an out-of-bounds read if LS_COLORS doesn't end with a `:`
-- Allowed multiple debug flags to be specified like `-D opt,tree`
-
-
-1.3.1
------
-
-**January 3, 2019**
-
-- Fixed some portability problems affecting FreeBSD
-
-
-1.3
----
-
-**January 2, 2019**
-
-New features:
-
-- `-acl` finds files with non-trivial Access Control Lists (from FreeBSD)
-- `-capable` finds files with capabilities set
-- `-D all` turns on all debugging flags at once
-
-Fixes:
-
-- `LS_COLORS` handling has been improved:
- - Extension colors are now case-insensitive like GNU `ls`
- - `or` (orphan) and `mi` (missing) files are now treated differently
- - Default colors can be unset with `di=00` or similar
- - Specific colors fall back to more general colors when unspecified in more places
- - `LS_COLORS` no longer needs a trailing colon
-- `-ls`/`-fls` now prints the major/minor numbers for device nodes
-- `-exec ;` is rejected rather than segfaulting
-- `bfs` now builds on old Linux versions that require `-lrt` for POSIX timers
-- For files whose access/change/modification times can't be read, `bfs` no longer fails unless those times are needed for tests
-- The testsuite is now more correct and portable
-
-
-1.2.4
------
-
-**September 24, 2018**
-
-- GNU find compatibility fixes for `-printf`:
- - `%Y` now prints `?` if an error occurs resolving the link
- - `%B` is now supported for birth/creation time (as well as `%W`/`%w`)
- - All standard `strftime()` formats are supported, not just the ones from the GNU find manual
-- Optimizations are now re-run if any expressions are reordered
-- `-exec` and friends no longer leave zombie processes around when `exec()` fails
-
-
-1.2.3
------
-
-**July 15, 2018**
-
-- Fixed `test_depth_error` on filesystems that don't fill in `d_type`
-- Fixed the build on Linux architectures that don't have the `statx()` syscall (ia64, sh4)
-- Fixed use of AT_EMPTY_PATH for fstatat on systems that don't support it (Hurd)
-- Fixed `ARG_MAX` accounting on architectures with large pages (ppc64le)
-- Fixed the build against the upcoming glibc 2.28 release that includes its own `statx()` wrapper
-
-
-1.2.2
------
-
-**June 23, 2018**
-
-- Minor bug fixes:
- - Fixed `-exec ... '{}' +` argument size tracking after recovering from `E2BIG`
- - Fixed `-fstype` if `/proc` is available but `/etc/mtab` is not
- - Fixed an uninitialized variable when given `-perm +rw...`
- - Fixed some potential "error: 'path': Success" messages
-- Reduced reliance on GNU coreutils in the testsuite
-- Refactored and simplified the internals of `bftw()`
-
-
-1.2.1
------
-
-**February 8, 2018**
-
-- Performance optimizations
-
-
-1.2
----
-
-**January 20, 2018**
-
-- Added support for the `-perm +7777` syntax deprecated by GNU find (equivalent to `-perm /7777`), for compatibility with BSD finds
-- Added support for file birth/creation times on platforms that report it
- - `-Bmin`/`-Btime`/`-Bnewer`
- - `B` flag for `-newerXY`
- - `%w` and `%Wk` directives for `-printf`
- - Uses the `statx(2)` system call on new enough Linux kernels
-- More robustness to `E2BIG` added to the `-exec` implementation
-
-
-1.1.4
------
-
-**October 27, 2017**
-
-- Added a man page
-- Fixed cases where multiple actions write to the same file
-- Report errors that occur when closing files/flushing streams
-- Fixed "argument list too long" errors with `-exec ... '{}' +`
-
-
-1.1.3
------
-
-**October 4, 2017**
-
-- Refactored the optimizer
-- Implemented data flow optimizations
-
-
-1.1.2
------
-
-**September 10, 2017**
-
-- Fixed `-samefile` and similar predicates when passed broken symbolic links
-- Implemented `-fstype` on Solaris
-- Fixed `-fstype` under musl
-- Implemented `-D search`
-- Implemented a cost-based optimizer
-
-
-1.1.1
------
-
-**August 10, 2017**
-
-- Re-licensed under the BSD Zero Clause License
-- Fixed some corner cases with `-exec` and `-ok` parsing
-
-
-1.1
----
-
-**July 22, 2017**
-
-- Implemented some primaries from NetBSD `find`:
- - `-exit [STATUS]` (like `-quit`, but with an optional explicit exit status)
- - `-printx` (escape special characters for `xargs`)
- - `-rm` (alias for `-delete`)
-- Warn if `-prune` will have no effect due to `-depth`
-- Handle y/n prompts according to the user's locale
-- Prompt the user to correct typos without having to re-run `bfs`
-- Fixed handling of paths longer than `PATH_MAX`
-- Fixed spurious "Inappropriate ioctl for device" errors when redirecting `-exec ... +` output
-- Fixed the handling of paths that treat a file as a directory (e.g. `a/b/c` where `a/b` is a regular file)
-- Fixed an expression optimizer bug that broke command lines like `bfs -name '*' -o -print`
-
-
-1.0.2
------
-
-**June 15, 2017**
-
-Bugfix release.
-
-- Fixed handling of \0 inside -printf format strings
-- Fixed `-perm` interpretation of permcopy actions (e.g. `u=rw,g=r`)
-
-
-1.0.1
------
-
-**May 17, 2017**
-
-Bugfix release.
-
-- Portability fixes that mostly affect GNU Hurd
-- Implemented `-D exec`
-- Made `-quit` not disable the implicit `-print`
-
-
-1.0
----
-
-**April 24, 2017**
-
-This is the first release of bfs with support for all of GNU find's primitives.
-
-Changes since 0.96:
-
-- Implemented `-fstype`
-- Implemented `-exec/-execdir ... +`
-- Implemented BSD's `-X`
-- Fixed the tests under Bash 3 (mostly for macOS)
-- Some minor optimizations and fixes
-
-
-0.*
-===
-
-
-0.96
-----
-
-**March 11, 2017**
-
-73/76 GNU find features supported.
-
-- Implemented -nouser and -nogroup
-- Implemented -printf and -fprintf
-- Implemented -ls and -fls
-- Implemented -type with multiple types at once (e.g. -type f,d,l)
-- Fixed 32-bit builds
-- Fixed -lname on "symlinks" in Linux /proc
-- Fixed -quit to take effect as soon as it's reached
-- Stopped redirecting standard input from /dev/null for -ok and -okdir, as that violates POSIX
-- Many test suite improvements
-
-
-0.88
-----
-
-**December 20, 2016**
-
-67/76 GNU find features supported.
-
-- Fixed the build on macOS, and some other UNIXes
-- Implemented `-regex`, `-iregex`, `-regextype`, and BSD's `-E`
-- Implemented `-x` (same as `-mount`/`-xdev`) from BSD
-- Implemented `-mnewer` (same as `-newer`) from BSD
-- Implemented `-depth N` from BSD
-- Implemented `-sparse` from FreeBSD
-- Implemented the `T` and `P` suffices for `-size`, for BSD compatibility
-- Added support for `-gid NAME` and `-uid NAME` as in BSD
-
-
-0.84.1
-------
-
-**November 24, 2016**
-
-Bugfix release.
-
-- Fixed [#7] again
-- Like GNU find, don't print warnings by default if standard input is not a terminal
-- Redirect standard input from /dev/null for -ok and -okdir
-- Skip . when -delete'ing
-- Fixed -execdir when the root path has no slashes
-- Fixed -execdir in /
-- Support -perm +MODE for symbolic modes
-- Fixed the build on FreeBSD
-
-[#7]: https://github.com/tavianator/bfs/issues/7
-
-
-0.84
-----
-
-**October 29, 2016**
-
-64/76 GNU find features supported.
-
-- Spelling suggestion improvements
-- Handle `--`
-- (Untested) support for exotic file types like doors, ports, and whiteouts
-- Improved robustness in the face of closed std{in,out,err}
-- Fixed the build on macOS
-- Implement `-ignore_readdir_race`, `-noignore_readdir_race`
-- Implement `-perm`
-
-
-0.82
-----
-
-**September 4, 2016**
-
-62/76 GNU find features supported.
-
-- Rework optimization levels
- - `-O1`
- - Simple boolean simplification
- - `-O2`
- - Purity-based optimizations, allowing side-effect-free tests like `-name` or `-type` to be moved or removed
- - `-O3` (**default**):
- - Re-order tests to reduce the expected cost (TODO)
- - `-O4`
- - Aggressive optimizations that may have surprising effects on warning/error messages and runtime, but should not otherwise affect the results
- - `-Ofast`:
- - Always the highest level, currently the same as `-O4`
-- Color files with multiple hard links correctly
-- Treat `-`, `)`, and `,` as paths when required to by POSIX
- - `)` and `,` are only supported before the expression begins
-- Implement `-D opt`
-- Implement `-D rates`
-- Implement `-fprint`
-- Implement `-fprint0`
-- Implement BSD's `-f`
-- Suggest fixes for typo'd arguments
-
-0.79
-----
-
-**May 27, 2016**
-
-60/76 GNU find features supported.
-
-- Remove an errant debug `printf()` from `-used`
-- Implement the `{} ;` variants of `-exec`, `-execdir`, `-ok`, and `-okdir`
-
-
-0.74
-----
-
-**March 12, 2016**
-
-56/76 GNU find features supported.
-
-- Color broken symlinks correctly
-- Fix [#7]
-- Fix `-daystart`'s rounding of midnight
-- Implement (most of) `-newerXY`
-- Implement `-used`
-- Implement `-size`
-
-[#7]: https://github.com/tavianator/bfs/issues/7
-
-
-0.70
-----
-
-**February 23, 2016**
-
-53/76 GNU find features supported.
-
-- New `make install` and `make uninstall` targets
-- Squelch non-positional warnings for `-follow`
-- Reduce memory footprint by as much as 64% by closing `DIR*`s earlier
-- Speed up `bfs` by ~5% by using a better FD cache eviction policy
-- Fix infinite recursion when evaluating `! expr`
-- Optimize unused pure expressions (e.g. `-empty -a -false`)
-- Optimize double-negation (e.g. `! ! -name foo`)
-- Implement `-D stat` and `-D tree`
-- Implement `-O`
-
-
-0.67
-----
-
-**February 14, 2016**
-
-Initial release.
-
-51/76 GNU find features supported.
diff --git a/bar.c b/bar.c
deleted file mode 100644
index 31734ac..0000000
--- a/bar.c
+++ /dev/null
@@ -1,253 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "bar.h"
-#include "dstring.h"
-#include "util.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <signal.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <unistd.h>
-
-struct bfs_bar {
- int fd;
- volatile sig_atomic_t width;
- volatile sig_atomic_t height;
-};
-
-/** The global status bar instance. */
-static struct bfs_bar the_bar = {
- .fd = -1,
-};
-
-/** Get the terminal size, if possible. */
-static int bfs_bar_getsize(struct bfs_bar *bar) {
-#ifdef TIOCGWINSZ
- struct winsize ws;
- if (ioctl(bar->fd, TIOCGWINSZ, &ws) != 0) {
- return -1;
- }
-
- bar->width = ws.ws_col;
- bar->height = ws.ws_row;
- return 0;
-#else
- errno = ENOTSUP;
- return -1;
-#endif
-}
-
-/** Async Signal Safe puts(). */
-static int ass_puts(int fd, const char *str) {
- size_t len = strlen(str);
- return xwrite(fd, str, len) == len ? 0 : -1;
-}
-
-/** Number of decimal digits needed for terminal sizes. */
-#define ITOA_DIGITS ((sizeof(unsigned short) * CHAR_BIT + 2) / 3)
-
-/** Async Signal Safe itoa(). */
-static char *ass_itoa(char *str, unsigned int n) {
- char *end = str + ITOA_DIGITS;
- *end = '\0';
-
- char *c = end;
- do {
- *--c = '0' + (n % 10);
- n /= 10;
- } while (n);
-
- size_t len = end - c;
- memmove(str, c, len + 1);
- return str + len;
-}
-
-/** Update the size of the scrollable region. */
-static int bfs_bar_resize(struct bfs_bar *bar) {
- char esc_seq[12 + ITOA_DIGITS] =
- "\0337" // DECSC: Save cursor
- "\033[;"; // DECSTBM: Set scrollable region
-
- // DECSTBM takes the height as the second argument
- char *ptr = esc_seq + strlen(esc_seq);
- ptr = ass_itoa(ptr, bar->height - 1);
-
- strcpy(ptr,
- "r" // DECSTBM
- "\0338" // DECRC: Restore the cursor
- "\033[J" // ED: Erase display from cursor to end
- );
-
- return ass_puts(bar->fd, esc_seq);
-}
-
-#ifdef SIGWINCH
-/** SIGWINCH handler. */
-static void sighand_winch(int sig) {
- int error = errno;
-
- bfs_bar_getsize(&the_bar);
- bfs_bar_resize(&the_bar);
-
- errno = error;
-}
-#endif
-
-/** Reset the scrollable region and hide the bar. */
-static int bfs_bar_reset(struct bfs_bar *bar) {
- return ass_puts(bar->fd,
- "\0337" // DECSC: Save cursor
- "\033[r" // DECSTBM: Reset scrollable region
- "\0338" // DECRC: Restore cursor
- "\033[J" // ED: Erase display from cursor to end
- );
-}
-
-/** Signal handler for process-terminating signals. */
-static void sighand_reset(int sig) {
- bfs_bar_reset(&the_bar);
- raise(sig);
-}
-
-/** Register sighand_reset() for a signal. */
-static void reset_before_death_by(int sig) {
- struct sigaction sa = {
- .sa_handler = sighand_reset,
- .sa_flags = SA_RESETHAND,
- };
- sigemptyset(&sa.sa_mask);
- sigaction(sig, &sa, NULL);
-}
-
-/** printf() to the status bar with a single write(). */
-BFS_FORMATTER(2, 3)
-static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) {
- va_list args;
- va_start(args, format);
- char *str = dstrvprintf(format, args);
- va_end(args);
-
- if (!str) {
- return -1;
- }
-
- int ret = ass_puts(bar->fd, str);
- dstrfree(str);
- return ret;
-}
-
-struct bfs_bar *bfs_bar_show(void) {
- int error;
-
- if (the_bar.fd >= 0) {
- error = EBUSY;
- goto fail;
- }
-
- char term[L_ctermid];
- ctermid(term);
- if (strlen(term) == 0) {
- error = ENOTTY;
- goto fail;
- }
-
- the_bar.fd = open(term, O_RDWR | O_CLOEXEC);
- if (the_bar.fd < 0) {
- error = errno;
- goto fail;
- }
-
- if (bfs_bar_getsize(&the_bar) != 0) {
- error = errno;
- goto fail_close;
- }
-
- reset_before_death_by(SIGABRT);
- reset_before_death_by(SIGINT);
- reset_before_death_by(SIGPIPE);
- reset_before_death_by(SIGQUIT);
- reset_before_death_by(SIGTERM);
-
-#ifdef SIGWINCH
- struct sigaction sa = {
- .sa_handler = sighand_winch,
- .sa_flags = SA_RESTART,
- };
- sigemptyset(&sa.sa_mask);
- sigaction(SIGWINCH, &sa, NULL);
-#endif
-
- bfs_bar_printf(&the_bar,
- "\n" // Make space for the bar
- "\0337" // DECSC: Save cursor
- "\033[;%ur" // DECSTBM: Set scrollable region
- "\0338" // DECRC: Restore cursor
- "\033[1A", // CUU: Move cursor up 1 row
- (unsigned int)(the_bar.height - 1)
- );
-
- return &the_bar;
-
-fail_close:
- close(the_bar.fd);
- the_bar.fd = -1;
-fail:
- errno = error;
- return NULL;
-}
-
-unsigned int bfs_bar_width(const struct bfs_bar *bar) {
- return bar->width;
-}
-
-int bfs_bar_update(struct bfs_bar *bar, const char *str) {
- return bfs_bar_printf(bar,
- "\0337" // DECSC: Save cursor
- "\033[%u;0f" // HVP: Move cursor to row, column
- "\033[K" // EL: Erase line
- "\033[7m" // SGR reverse video
- "%s"
- "\033[27m" // SGR reverse video off
- "\0338", // DECRC: Restore cursor
- (unsigned int)bar->height,
- str
- );
-}
-
-void bfs_bar_hide(struct bfs_bar *bar) {
- if (!bar) {
- return;
- }
-
- signal(SIGABRT, SIG_DFL);
- signal(SIGINT, SIG_DFL);
- signal(SIGPIPE, SIG_DFL);
- signal(SIGQUIT, SIG_DFL);
- signal(SIGTERM, SIG_DFL);
-#ifdef SIGWINCH
- signal(SIGWINCH, SIG_DFL);
-#endif
-
- bfs_bar_reset(bar);
-
- close(bar->fd);
- bar->fd = -1;
-}
diff --git a/bar.h b/bar.h
deleted file mode 100644
index 3e509d6..0000000
--- a/bar.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A terminal status bar.
- */
-
-#ifndef BFS_BAR_H
-#define BFS_BAR_H
-
-/** A terminal status bar. */
-struct bfs_bar;
-
-/**
- * Create a terminal status bar. Only one status bar is supported at a time.
- *
- * @return
- * A pointer to the new status bar, or NULL on failure.
- */
-struct bfs_bar *bfs_bar_show(void);
-
-/**
- * Get the width of the status bar.
- */
-unsigned int bfs_bar_width(const struct bfs_bar *bar);
-
-/**
- * Update the status bar message.
- *
- * @param bar
- * The status bar to update.
- * @param str
- * The string to display.
- * @return
- * 0 on success, -1 on failure.
- */
-int bfs_bar_update(struct bfs_bar *bar, const char *str);
-
-/**
- * Hide the status bar.
- */
-void bfs_bar_hide(struct bfs_bar *status);
-
-#endif // BFS_BAR_H
diff --git a/bench/.gitignore b/bench/.gitignore
new file mode 100644
index 0000000..170d850
--- /dev/null
+++ b/bench/.gitignore
@@ -0,0 +1,3 @@
+/corpus/
+/results/
+/worktree/
diff --git a/bench/README.md b/bench/README.md
new file mode 100644
index 0000000..56157a0
--- /dev/null
+++ b/bench/README.md
@@ -0,0 +1,51 @@
+This directory contains a suite of benchmarks used to evaluate `bfs` and detect performance regressions.
+To run them, you'll need the [tailfin] benchmark harness.
+You can read the full usage information with
+
+[tailfin]: https://github.com/tavianator/tailfin
+
+```console
+$ tailfin -n run bench/bench.sh --help
+Usage: tailfin run bench/bench.sh [--default]
+ [--complete] [--early-quit] [--print] [--strategies]
+ [--build=...] [--bfs] [--find] [--fd]
+ [--no-clean] [--help]
+...
+```
+
+The benchmarks use various git repositories to have a realistic and reproducible directory structure as a corpus.
+Currently, those are the [Linux], [Rust], and [Chromium] repos.
+The scripts will automatically clone those repos using [partial clone] filters to avoid downloading the actual file contents, saving bandwidth and space.
+
+[Linux]: https://github.com/torvalds/linux.git
+[Rust]: https://github.com/rust-lang/rust.git
+[Chromium]: https://chromium.googlesource.com/chromium/src.git
+[partial clone]: https://git-scm.com/docs/partial-clone
+
+You can try out a quick benchmark by running
+
+```console
+$ tailfin run bench/bench.sh --build=main --complete=linux
+```
+
+This will build the `main` branch, and measure the complete traversal of the Linux repo.
+Results will be both printed to the console and saved in a Markdown file, which you can find by running
+
+```console
+$ tailfin latest
+results/2023/09/29/15:32:49
+$ cat results/2023/09/29/15:32:49/runs/1/bench.md
+## Complete traversal
+...
+```
+
+To measure performance improvements/regressions of a change, compare the `main` branch to the topic branch on the full benchmark suite:
+
+```console
+$ tailfin run bench/bench.sh --build=main --build=branch --default
+```
+
+This will take a few minutes.
+Results from the full benchmark suite can be seen in performance-related pull requests, for example [#126].
+
+[#126]: https://github.com/tavianator/bfs/pull/126
diff --git a/bench/bench.sh b/bench/bench.sh
new file mode 100644
index 0000000..c9ed978
--- /dev/null
+++ b/bench/bench.sh
@@ -0,0 +1,749 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+declare -gA URLS=(
+ [chromium]="https://chromium.googlesource.com/chromium/src.git"
+ [linux]="https://github.com/torvalds/linux.git"
+ [rust]="https://github.com/rust-lang/rust.git"
+)
+
+declare -gA TAGS=(
+ [chromium]=119.0.6036.2
+ [linux]=v6.5
+ [rust]=1.72.1
+)
+
+COMPLETE_DEFAULT=(linux rust chromium)
+EARLY_QUIT_DEFAULT=(chromium)
+STAT_DEFAULT=(rust)
+PRINT_DEFAULT=(linux)
+STRATEGIES_DEFAULT=(rust)
+JOBS_DEFAULT=(rust)
+EXEC_DEFAULT=(linux)
+SORTED_DEFAULT=(chromium)
+
+usage() {
+ printf 'Usage: tailfin run %s\n' "${BASH_SOURCE[0]}"
+ printf ' [--default] [--<BENCHMARK> [--<BENCHMARK>...]]\n'
+ printf ' [--build=...] [--bfs] [--find] [--fd]\n'
+ printf ' [--no-clean] [--help]\n\n'
+
+ printf ' --default\n'
+ printf ' Run the default set of benchmarks\n\n'
+
+ printf ' --complete[=CORPUS]\n'
+ printf ' Complete traversal benchmark.\n'
+ printf ' Default corpus is --complete="%s"\n\n' "${COMPLETE_DEFAULT[*]}"
+
+ printf ' --early-quit[=CORPUS]\n'
+ printf ' Early quitting benchmark.\n'
+ printf ' Default corpus is --early-quit=%s\n\n' "${EARLY_QUIT_DEFAULT[*]}"
+
+ printf ' --stat[=CORPUS]\n'
+ printf ' Traversal with stat().\n'
+ printf ' Default corpus is --stat=%s\n\n' "${STAT_DEFAULT[*]}"
+
+ printf ' --print[=CORPUS]\n'
+ printf ' Path printing benchmark.\n'
+ printf ' Default corpus is --print=%s\n\n' "${PRINT_DEFAULT[*]}"
+
+ printf ' --strategies[=CORPUS]\n'
+ printf ' Search strategy benchmark.\n'
+ printf ' Default corpus is --strategies=%s\n\n' "${STRATEGIES_DEFAULT[*]}"
+
+ printf ' --jobs[=CORPUS]\n'
+ printf ' Parallelism benchmark.\n'
+ printf ' Default corpus is --jobs=%s\n\n' "${JOBS_DEFAULT[*]}"
+
+ printf ' --exec[=CORPUS]\n'
+ printf ' Process spawning benchmark.\n'
+ printf ' Default corpus is --exec=%s\n\n' "${EXEC_DEFAULT[*]}"
+
+ printf ' --sorted[=CORPUS]\n'
+ printf ' Sorted traversal benchmark.\n'
+ printf ' Default corpus is --sorted=%s\n\n' "${SORTED_DEFAULT[*]}"
+
+ printf ' --build=COMMIT\n'
+ printf ' Build this bfs commit and benchmark it. Specify multiple times to\n'
+ printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n'
+
+ printf ' --bfs[=COMMAND]\n'
+ printf ' Benchmark an existing build of bfs\n\n'
+
+ printf ' --find[=COMMAND]\n'
+ printf ' Compare against find\n\n'
+
+ printf ' --fd[=COMMAND]\n'
+ printf ' Compare against fd\n\n'
+
+ printf ' --no-clean\n'
+ printf ' Use any existing corpora as-is\n\n'
+
+ printf ' --help\n'
+ printf ' This message\n\n'
+}
+
+# Hack to export an array
+export_array() {
+ local str=$(declare -p "$1" | sed 's/ -a / -ga /')
+ unset "$1"
+ export "$1=$str"
+}
+
+# Hack to import an array
+import_array() {
+ local cmd="${!1}"
+ unset "$1"
+ eval "$cmd"
+}
+
+# Set up the benchmarks
+setup() {
+ ROOT=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")/..")
+ if ! [ "$PWD" -ef "$ROOT" ]; then
+ printf 'error: Please run this script from %s\n\n' "$ROOT" >&2
+ usage >&2
+ exit $EX_USAGE
+ fi
+
+ nproc=$(nproc)
+
+ # Options
+
+ CLEAN=1
+
+ BUILD=()
+ BFS=()
+ FIND=()
+ FD=()
+
+ COMPLETE=()
+ EARLY_QUIT=()
+ STAT=()
+ PRINT=()
+ STRATEGIES=()
+ JOBS=()
+ EXEC=()
+ SORTED=()
+
+ for arg; do
+ case "$arg" in
+ # Flags
+ --no-clean)
+ CLEAN=0
+ ;;
+ # bfs commits/tags to benchmark
+ --build=*)
+ BUILD+=("${arg#*=}")
+ BFS+=("bfs-${arg#*=}")
+ ;;
+ # Utilities to benchmark against
+ --bfs)
+ BFS+=(bfs)
+ ;;
+ --bfs=*)
+ BFS+=("${arg#*=}")
+ ;;
+ --find)
+ FIND+=(find)
+ ;;
+ --find=*)
+ FIND+=("${arg#*=}")
+ ;;
+ --fd)
+ FD+=(fd)
+ ;;
+ --fd=*)
+ FD+=("${arg#*=}")
+ ;;
+ # Benchmark groups
+ --complete)
+ COMPLETE=("${COMPLETE_DEFAULT[@]}")
+ ;;
+ --complete=*)
+ read -ra COMPLETE <<<"${arg#*=}"
+ ;;
+ --early-quit)
+ EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
+ ;;
+ --early-quit=*)
+ read -ra EARLY_QUIT <<<"${arg#*=}"
+ ;;
+ --stat)
+ STAT=("${STAT_DEFAULT[@]}")
+ ;;
+ --stat=*)
+ read -ra STAT <<<"${arg#*=}"
+ ;;
+ --print)
+ PRINT=("${PRINT_DEFAULT[@]}")
+ ;;
+ --print=*)
+ read -ra PRINT <<<"${arg#*=}"
+ ;;
+ --strategies)
+ STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
+ ;;
+ --strategies=*)
+ read -ra STRATEGIES <<<"${arg#*=}"
+ ;;
+ --jobs)
+ JOBS=("${JOBS_DEFAULT[@]}")
+ ;;
+ --jobs=*)
+ read -ra JOBS <<<"${arg#*=}"
+ ;;
+ --exec)
+ EXEC=("${EXEC_DEFAULT[@]}")
+ ;;
+ --exec=*)
+ read -ra EXEC <<<"${arg#*=}"
+ ;;
+ --sorted)
+ SORTED=("${SORTED_DEFAULT[@]}")
+ ;;
+ --sorted=*)
+ read -ra SORTED <<<"${arg#*=}"
+ ;;
+ --default)
+ COMPLETE=("${COMPLETE_DEFAULT[@]}")
+ EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
+ STAT=("${STAT_DEFAULT[@]}")
+ PRINT=("${PRINT_DEFAULT[@]}")
+ STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
+ JOBS=("${JOBS_DEFAULT[@]}")
+ EXEC=("${EXEC_DEFAULT[@]}")
+ SORTED=("${SORTED_DEFAULT[@]}")
+ ;;
+ --help)
+ usage
+ exit
+ ;;
+ *)
+ printf 'error: Unknown option %q\n\n' "$arg" >&2
+ usage >&2
+ exit $EX_USAGE
+ ;;
+ esac
+ done
+
+ if ((UID == 0)); then
+ max-freq
+ fi
+
+ echo "Building bfs ..."
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc" all
+
+ as-user mkdir -p bench/corpus
+
+ declare -A cloned=()
+ for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}" "${SORTED[@]}"; do
+ if ((cloned["$corpus"])); then
+ continue
+ fi
+ cloned["$corpus"]=1
+
+ dir="bench/corpus/$corpus"
+ if ((CLEAN)) || ! [ -e "$dir" ]; then
+ as-user ./bench/clone-tree.sh "${URLS[$corpus]}" "${TAGS[$corpus]}" "$dir"{,.git}
+ fi
+ done
+
+ if ((${#BUILD[@]} > 0)); then
+ echo "Creating bfs worktree ..."
+
+ worktree="bench/worktree"
+ as-user git worktree add -qd "$worktree"
+ defer as-user git worktree remove "$worktree"
+
+ bin="$(realpath -- "$SETUP_DIR")/bin"
+ as-user mkdir "$bin"
+
+ for commit in "${BUILD[@]}"; do
+ (
+ echo "Building bfs $commit ..."
+ cd "$worktree"
+ as-user git checkout -qd "$commit" --
+ if [ -e configure ]; then
+ as-user ./configure --enable-release
+ as-user make -s -j"$nproc"
+ else
+ as-user make -s -j"$nproc" release
+ fi
+ if [ -e ./bin/bfs ]; then
+ as-user cp ./bin/bfs "$bin/bfs-$commit"
+ else
+ as-user cp ./bfs "$bin/bfs-$commit"
+ fi
+ as-user make -s clean
+ )
+ done
+
+ export PATH="$bin:$PATH"
+ fi
+
+ export_array BFS
+ export_array FIND
+ export_array FD
+
+ export_array COMPLETE
+ export_array EARLY_QUIT
+ export_array STAT
+ export_array PRINT
+ export_array STRATEGIES
+ export_array JOBS
+ export_array EXEC
+ export_array SORTED
+
+ if ((UID == 0)); then
+ turbo-off
+ fi
+
+ sync
+}
+
+# Runs hyperfine and saves the output
+do-hyperfine() {
+ local tmp_md="$BENCH_DIR/.bench.md"
+ local md="$BENCH_DIR/bench.md"
+ local tmp_json="$BENCH_DIR/.bench.json"
+ local json="$BENCH_DIR/bench.json"
+
+ if (($# == 0)); then
+ printf 'Nothing to do\n\n' | tee -a "$md"
+ return 1
+ fi
+
+ hyperfine -w2 -M20 --export-markdown="$tmp_md" --export-json="$tmp_json" "$@" &>/dev/tty
+ cat "$tmp_md" >>"$md"
+ cat "$tmp_json" >>"$json"
+ rm "$tmp_md" "$tmp_json"
+
+ printf '\n' | tee -a "$md"
+}
+
+# Print the header for a benchmark group
+group() {
+ printf "## $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Print the header for a benchmark subgroup
+subgroup() {
+ printf "### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Print the header for a benchmark sub-subgroup
+subsubgroup() {
+ printf "#### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Benchmark the complete traversal of a directory tree
+# (without printing anything)
+bench-complete-corpus() {
+ total=$(./bin/bfs "$2" -printf '.' | wc -c)
+
+ subgroup "%s (%'d files)" "$1" "$total"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -false")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2 -false")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u '^$' $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All complete traversal benchmarks
+bench-complete() {
+ if (($#)); then
+ group "Complete traversal"
+
+ for corpus; do
+ bench-complete-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark quitting as soon as a file is seen
+bench-early-quit-corpus() {
+ dir="$2"
+ max_depth=$(./bin/bfs "$dir" -printf '%d\n' | sort -rn | head -n1)
+
+ subgroup '%s (depth %d)' "$1" "$max_depth"
+
+ # Save the list of unique filenames, along with their depth
+ UNIQ="$BENCH_DIR/uniq"
+ ./bin/bfs "$dir" -printf '%d %f\n' | sort -k2 | uniq -uf1 >"$UNIQ"
+
+ for ((i = 2; i <= max_depth; i *= 2)); do
+ subsubgroup 'Depth %d' "$i"
+
+ # Sample random uniquely-named files at depth $i
+ export FILES="$BENCH_DIR/uniq-$i"
+ sed -n "s/^$i //p" "$UNIQ" | shuf -n20 >"$FILES"
+ if ! [ -s "$FILES" ]; then
+ continue
+ fi
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $dir -name \$(shuf -n1 \$FILES) -print -quit")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $dir -name \$(shuf -n1 \$FILES) -print -quit")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -usg1 \$(shuf -n1 \$FILES) $dir")
+ done
+
+ do-hyperfine "${cmds[@]}"
+ done
+}
+
+# All early-quitting benchmarks
+bench-early-quit() {
+ if (($#)); then
+ group "Early termination"
+
+ for corpus; do
+ bench-early-quit-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark traversal with stat()
+bench-stat-corpus() {
+ total=$(./bin/bfs "$2" -printf '.' | wc -c)
+
+ subgroup "%s (%'d files)" "$1" "$total"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -size 1024G")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2 -size 1024G")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2 --size 1024Gi")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# stat() benchmarks
+bench-stat() {
+ if (($#)); then
+ group "Traversal with stat()"
+
+ for corpus; do
+ bench-stat-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark printing paths without colors
+bench-print-nocolor() {
+ subsubgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Benchmark printing paths with colors
+bench-print-color() {
+ subsubgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -color")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2 --color=always")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All printing benchmarks
+bench-print() {
+ if (($#)); then
+ group "Printing paths"
+
+ subgroup "Without colors"
+ for corpus; do
+ bench-print-nocolor "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+
+ subgroup "With colors"
+ for corpus; do
+ bench-print-color "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark search strategies
+bench-strategies-corpus() {
+ subgroup '%s' "$1"
+
+ if ((${#BFS[@]} == 1)); then
+ cmds=("$BFS -S "{bfs,dfs,ids,eds}" $2 -false")
+ do-hyperfine "${cmds[@]}"
+ else
+ for S in bfs dfs ids eds; do
+ subsubgroup '`-S %s`' "$S"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs -S $S $2 -false")
+ done
+ do-hyperfine "${cmds[@]}"
+ done
+ fi
+}
+
+# All search strategy benchmarks
+bench-strategies() {
+ if (($#)); then
+ group "Search strategies"
+
+ for corpus; do
+ bench-strategies-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark parallelism
+bench-jobs-corpus() {
+ subgroup '%s' "$1"
+
+ if ((${#BFS[@]} + ${#FD[@]} == 1)); then
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ if "$bfs" -j1 -quit &>/dev/null; then
+ cmds+=("$bfs -j"{1,2,3,4,6,8,12,16}" $2 -false")
+ else
+ cmds+=("$bfs $2 -false")
+ fi
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -j"{1,2,3,4,6,8,12,16}" -u '^$' $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+ else
+ for j in 1 2 3 4 6 8 12 16; do
+ subsubgroup '`-j%d`' $j
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ if "$bfs" -j1 -quit &>/dev/null; then
+ cmds+=("$bfs -j$j $2 -false")
+ elif ((j == 1)); then
+ cmds+=("$bfs $2 -false")
+ fi
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -j$j -u '^$' $2")
+ done
+
+ if ((${#cmds[@]})); then
+ do-hyperfine "${cmds[@]}"
+ fi
+ done
+ fi
+}
+
+# All parallelism benchmarks
+bench-jobs() {
+ if (($#)); then
+ group "Parallelism"
+
+ for corpus; do
+ bench-jobs-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# One file/process
+bench-exec-single() {
+ subsubgroup "One file per process"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -maxdepth 2 -exec true -- {} \;")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $1 --max-depth=2 -x true --")
+ # Without -j1, fd runs multiple processes in parallel, which is unfair
+ cmds+=("$fd -j1 -u --search-path $1 --max-depth=2 -x true --")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Many files/process
+bench-exec-multi() {
+ subsubgroup "Many files per process"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -exec true -- {} +")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $1 -X true --")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Many files, same dir
+bench-exec-chdir() {
+ if ((${#BFS[@]} + ${#FIND[@]} == 0)); then
+ return
+ fi
+
+ subsubgroup "Spawn in parent directory"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -maxdepth 3 -execdir true -- {} +")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Benchmark process spawning
+bench-exec-corpus() {
+ subgroup '%s' "$1"
+
+ bench-exec-single "$2"
+ bench-exec-multi "$2"
+ bench-exec-chdir "$2"
+}
+
+# All process spawning benchmarks
+bench-exec() {
+ if (($#)); then
+ group "Process spawning"
+
+ for corpus; do
+ bench-exec-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark sorted traversal
+bench-sorted-corpus() {
+ subgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs -s $2 -false")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All sorted traversal benchmarks
+bench-sorted() {
+ if (($#)); then
+ group "Sorted traversal"
+
+ for corpus; do
+ bench-sorted-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Print benchmarked versions
+bench-versions() {
+ subgroup "Versions"
+
+ local md="$BENCH_DIR/bench.md"
+
+ printf '```console\n' >>"$md"
+
+ {
+ for bfs in "${BFS[@]}"; do
+ printf '$ %s --version | head -n1\n' "$bfs"
+ "$bfs" --version | head -n1
+ done
+
+ for find in "${FIND[@]}"; do
+ printf '$ %s --version | head -n1\n' "$find"
+ "$find" --version | head -n1
+ done
+
+ for fd in "${FD[@]}"; do
+ printf '$ %s --version\n' "$fd"
+ "$fd" --version
+ done
+ } | tee -a "$md"
+
+ printf '```' >>"$md"
+}
+
+# Print benchmark details
+bench-details() {
+ group "Details"
+
+ bench-versions
+}
+
+# Run all the benchmarks
+bench() {
+ import_array BFS
+ import_array FIND
+ import_array FD
+
+ import_array COMPLETE
+ import_array EARLY_QUIT
+ import_array STAT
+ import_array PRINT
+ import_array STRATEGIES
+ import_array JOBS
+ import_array EXEC
+ import_array SORTED
+
+ bench-complete "${COMPLETE[@]}"
+ bench-early-quit "${EARLY_QUIT[@]}"
+ bench-stat "${STAT[@]}"
+ bench-print "${PRINT[@]}"
+ bench-strategies "${STRATEGIES[@]}"
+ bench-jobs "${JOBS[@]}"
+ bench-exec "${EXEC[@]}"
+ bench-sorted "${SORTED[@]}"
+ bench-details
+}
diff --git a/bench/clone-tree.sh b/bench/clone-tree.sh
new file mode 100755
index 0000000..744b5f4
--- /dev/null
+++ b/bench/clone-tree.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Creates a directory tree that matches a git repo, but with empty files. E.g.
+#
+# $ ./bench/clone-tree.sh "https://.../linux.git" v6.5 ./linux ./linux.git
+#
+# will create or update a shallow clone at ./linux.git, then create a directory
+# tree at ./linux with the same directory tree as the tag v6.5, except all files
+# will be empty.
+
+set -eu
+
+if (($# != 4)); then
+ printf 'Usage: %s https://url/of/repo.git <TAG> path/to/checkout path/to/repo.git\n' "$0" >&2
+ exit 1
+fi
+
+URL="$1"
+TAG="$2"
+DIR="$3"
+REPO="$4"
+
+BENCH=$(dirname -- "${BASH_SOURCE[0]}")
+BIN=$(realpath -- "$BENCH/../bin")
+BFS="$BIN/bfs"
+XTOUCH="$BIN/tests/xtouch"
+
+if [ "${NPROC-}" ]; then
+ # Use fewer cores in recursive calls
+ export NPROC=$(((NPROC + 1) / 2))
+else
+ export NPROC=$(nproc)
+fi
+
+JOBS=$((NPROC < 8 ? NPROC : 8))
+
+do-git() {
+ git -C "$REPO" "$@"
+}
+
+if ! [ -e "$REPO" ]; then
+ mkdir -p -- "$REPO"
+ do-git init -q --bare
+fi
+
+has-ref() {
+ do-git rev-list --quiet -1 --missing=allow-promisor "$1" &>/dev/null
+}
+
+sparse-fetch() {
+ do-git -c fetch.negotiationAlgorithm=noop fetch -q --filter=blob:none --depth=1 --no-tags --no-write-fetch-head --no-auto-gc "$@"
+}
+
+if ! has-ref "$TAG"; then
+ printf 'Fetching %s ...\n' "$TAG" >&2
+ do-git config remote.origin.url "$URL"
+ if ((${#TAG} >= 40)); then
+ sparse-fetch origin "$TAG"
+ else
+ sparse-fetch origin tag "$TAG"
+ fi
+fi
+
+# Delete a tree in parallel
+clean() {
+ local d=5
+ "$BFS" -f "$1" -mindepth $d -maxdepth $d -type d -print0 \
+ | xargs -0r -n1 -P$JOBS -- "$BFS" -j1 -mindepth 1 -delete -f
+ "$BFS" -f "$1" -delete
+}
+
+if [ -e "$DIR" ]; then
+ printf 'Cleaning old directory tree %s ...\n' "$DIR" >&2
+ TMP=$(mktemp -dp "$(dirname -- "$DIR")")
+ mv -- "$DIR" "$TMP"
+ clean "$TMP" &
+fi
+
+# List gitlinks (submodule references) in the tree
+ls-gitlinks() {
+ do-git ls-tree -zr "$TAG" \
+ | sed -zn 's/.* commit //p'
+}
+
+# Get the submodule ID for a path
+submodule-for-path() {
+ do-git config --blob "$TAG:.gitmodules" \
+ --name-only \
+ --fixed-value \
+ --get-regexp 'submodule\..**\.path' "$1" \
+ | sed -En 's/submodule\.(.*)\.path/\1/p'
+}
+
+# Get the URL for a submodule
+submodule-url() {
+ # - https://chrome-internal.googlesource.com/
+ # - not publicly accessible
+ # - https://chromium.googlesource.com/external/github.com/WebKit/webkit.git
+ # - is accessible, but the commit (59e9de61b7b3) isn't
+ # - https://android.googlesource.com/
+ # - is accessible, but you need an account
+
+ do-git config --blob "$TAG:.gitmodules" \
+ --get "submodule.$1.url" \
+ | sed -E \
+ -e '\|^https://chrome-internal.googlesource.com/|Q1' \
+ -e '\|^https://chromium.googlesource.com/external/github.com/WebKit/webkit.git|Q1' \
+ -e '\|^https://android.googlesource.com/|Q1'
+}
+
+# Recursively checkout submodules
+while read -rd '' SUBREF SUBDIR; do
+ SUBNAME=$(submodule-for-path "$SUBDIR")
+ SUBURL=$(submodule-url "$SUBNAME") || continue
+
+ if (($(jobs -pr | wc -w) >= JOBS)); then
+ wait -n
+ fi
+ "$0" "$SUBURL" "$SUBREF" "$DIR/$SUBDIR" "$REPO/modules/$SUBNAME" &
+done < <(ls-gitlinks)
+
+# Touch files in parallel
+xtouch() (
+ cd "$DIR"
+ if ((JOBS > 1)); then
+ xargs -0r -n4096 -P$JOBS -- "$XTOUCH" -p --
+ else
+ xargs -0r -- "$XTOUCH" -p --
+ fi
+)
+
+# Check out files
+printf 'Checking out %s ...\n' "$DIR" >&2
+mkdir -p -- "$DIR"
+do-git ls-tree -zr "$TAG"\
+ | sed -zn 's/.* blob .*\t//p' \
+ | xtouch
+
+# Wait for cleaning/submodules
+wait
diff --git a/bench/ioq.c b/bench/ioq.c
new file mode 100644
index 0000000..fb9edbc
--- /dev/null
+++ b/bench/ioq.c
@@ -0,0 +1,455 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "ioq.h"
+#include "sighook.h"
+#include "xtime.h"
+
+#include <errno.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+/** A latency sample. */
+struct lat {
+ /** The sampled latency. */
+ struct timespec time;
+ /** A random integer, for reservoir sampling. */
+ long key;
+};
+
+/** Number of latency samples to keep. */
+#define SAMPLES 1000
+/** Latency sampling period. */
+#define PERIOD 128
+
+/** Latency measurements. */
+struct lats {
+ /** Lowest observed latency. */
+ struct timespec min;
+ /** Highest observed latency. */
+ struct timespec max;
+ /** Total latency. */
+ struct timespec sum;
+ /** Number of measured requests. */
+ size_t count;
+
+ /** Priority queue for reservoir sampling. */
+ struct lat heap[SAMPLES];
+ /** Current size of the heap. */
+ size_t heap_size;
+};
+
+/** Initialize a latency reservoir. */
+static void lats_init(struct lats *lats) {
+ lats->min = (struct timespec) { .tv_sec = 1000 };
+ lats->max = (struct timespec) { 0 };
+ lats->sum = (struct timespec) { 0 };
+ lats->count = 0;
+ lats->heap_size = 0;
+}
+
+/** Binary heap parent. */
+static size_t heap_parent(size_t i) {
+ return (i - 1) / 2;
+}
+
+/** Binary heap left child. */
+static size_t heap_child(size_t i) {
+ return 2 * i + 1;
+}
+
+/** Binary heap smallest child. */
+static size_t heap_min_child(const struct lats *lats, size_t i) {
+ size_t j = heap_child(i);
+ size_t k = j + 1;
+ if (k < lats->heap_size && lats->heap[k].key < lats->heap[j].key) {
+ return k;
+ } else {
+ return j;
+ }
+}
+
+/** Check if the heap property is met. */
+static bool heap_check(const struct lat *parent, const struct lat *child) {
+ return parent->key <= child->key;
+}
+
+/** Reservoir sampling. */
+static void heap_push(struct lats *lats, const struct lat *lat) {
+ size_t i;
+
+ if (lats->heap_size < SAMPLES) {
+ // Heapify up
+ i = lats->heap_size++;
+ while (i > 0) {
+ size_t j = heap_parent(i);
+ if (heap_check(&lats->heap[j], lat)) {
+ break;
+ }
+ lats->heap[i] = lats->heap[j];
+ i = j;
+ }
+ } else if (lat->key > lats->heap[0].key) {
+ // Heapify down
+ i = 0;
+ while (true) {
+ size_t j = heap_min_child(lats, i);
+ if (j >= SAMPLES || heap_check(lat, &lats->heap[j])) {
+ break;
+ }
+ lats->heap[i] = lats->heap[j];
+ i = j;
+ }
+ } else {
+ // Reject
+ return;
+ }
+
+ lats->heap[i] = *lat;
+}
+
+/** Add a latency sample. */
+static void lats_push(struct lats *lats, const struct timespec *ts) {
+ timespec_min(&lats->min, ts);
+ timespec_max(&lats->max, ts);
+ timespec_add(&lats->sum, ts);
+ ++lats->count;
+
+ struct lat lat = {
+ .time = *ts,
+ .key = lrand48(),
+ };
+ heap_push(lats, &lat);
+}
+
+/** Merge two latency reservoirs. */
+static void lats_merge(struct lats *into, const struct lats *from) {
+ timespec_min(&into->min, &from->min);
+ timespec_max(&into->max, &from->max);
+ timespec_add(&into->sum, &from->sum);
+ into->count += from->count;
+
+ for (size_t i = 0; i < from->heap_size; ++i) {
+ heap_push(into, &from->heap[i]);
+ }
+}
+
+/** Latency qsort() comparator. */
+static int lat_cmp(const void *a, const void *b) {
+ const struct lat *la = a;
+ const struct lat *lb = b;
+ return timespec_cmp(&la->time, &lb->time);
+}
+
+/** Sort the latency reservoir. */
+static void lats_sort(struct lats *lats) {
+ qsort(lats->heap, lats->heap_size, sizeof(lats->heap[0]), lat_cmp);
+}
+
+/** Get the nth percentile. */
+static const struct timespec *lats_percentile(const struct lats *lats, int percent) {
+ size_t i = lats->heap_size * percent / 100;
+ return &lats->heap[i].time;
+}
+
+/** Which clock to use for benchmarking. */
+static clockid_t clockid = CLOCK_REALTIME;
+
+/** Get a current time measurement. */
+static void gettime(struct timespec *tp) {
+ int ret = clock_gettime(clockid, tp);
+ bfs_everify(ret == 0, "clock_gettime(%d)", (int)clockid);
+}
+
+/**
+ * Time measurements.
+ */
+struct times {
+ /** The start time. */
+ struct timespec start;
+
+ /** Total requests started. */
+ size_t pushed;
+ /** Total requests finished. */
+ size_t popped;
+
+ /** The start time for the currently tracked request. */
+ struct timespec req_start;
+ /** Whether a timed request is currently in flight. */
+ bool timing;
+
+ /** Latency measurements. */
+ struct lats lats;
+};
+
+/** Initialize a timer. */
+static void times_init(struct times *times) {
+ gettime(&times->start);
+ times->pushed = 0;
+ times->popped = 0;
+ bfs_assert(!times->timing);
+ lats_init(&times->lats);
+}
+
+/** Finish timing a request. */
+static void track_latency(struct times *times) {
+ struct timespec elapsed;
+ gettime(&elapsed);
+ timespec_sub(&elapsed, &times->req_start);
+ lats_push(&times->lats, &elapsed);
+
+ bfs_assert(times->timing);
+ times->timing = false;
+}
+
+/** Add times to the totals, and reset the lap times. */
+static void times_lap(struct times *total, struct times *lap) {
+ total->pushed += lap->pushed;
+ total->popped += lap->popped;
+ lats_merge(&total->lats, &lap->lats);
+
+ times_init(lap);
+}
+
+/** Print some times. */
+static void times_print(struct times *times, long seconds) {
+ struct timespec elapsed;
+ gettime(&elapsed);
+ timespec_sub(&elapsed, &times->start);
+
+ double fsec = timespec_ns(&elapsed) / 1.0e9;
+
+ if (seconds > 0) {
+ printf("%5ld", seconds);
+ } else if (elapsed.tv_nsec >= 10 * 1000 * 1000) {
+ printf("%5.2f", fsec);
+ } else {
+ printf("%5.0f", fsec);
+ }
+
+ double iops = times->popped / fsec;
+ double mean = timespec_ns(&times->lats.sum) / times->lats.count;
+ double min = timespec_ns(&times->lats.min);
+ double max = timespec_ns(&times->lats.max);
+
+ lats_sort(&times->lats);
+ double n50 = timespec_ns(lats_percentile(&times->lats, 50));
+ double n90 = timespec_ns(lats_percentile(&times->lats, 90));
+ double n99 = timespec_ns(lats_percentile(&times->lats, 99));
+
+ printf(" │ %'12.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f\n", iops, mean, min, n50, n90, n99, max);
+ fflush(stdout);
+}
+
+/** Push an ioq request. */
+static bool push(struct ioq *ioq, enum ioq_nop_type type, struct times *lap) {
+ void *ptr = NULL;
+
+ // Track latency for a small fraction of requests
+ if (!lap->timing && (lap->pushed + 1) % PERIOD == 0) {
+ ptr = lap;
+ gettime(&lap->req_start);
+ }
+
+ int ret = ioq_nop(ioq, type, ptr);
+ if (ret != 0) {
+ bfs_everify(errno == EAGAIN, "ioq_nop(%d)", (int)type);
+ return false;
+ }
+
+ ++lap->pushed;
+ if (ptr) {
+ lap->timing = true;
+ }
+ return true;
+}
+
+/** Pop an ioq request. */
+static bool pop(struct ioq *ioq, struct times *lap, bool block) {
+ struct ioq_ent *ent = ioq_pop(ioq, block);
+ if (!ent) {
+ return false;
+ }
+
+ if (ent->ptr) {
+ track_latency(lap);
+ }
+
+ ioq_free(ioq, ent);
+ ++lap->popped;
+ return true;
+}
+
+/** ^C flag. */
+static atomic bool quit = false;
+
+/** ^C hook. */
+static void ctrlc(int sig, siginfo_t *info, void *arg) {
+ store(&quit, true, relaxed);
+}
+
+int main(int argc, char *argv[]) {
+ // Use CLOCK_MONOTONIC if available
+#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0
+ if (sysoption(MONOTONIC_CLOCK) > 0) {
+ clockid = CLOCK_MONOTONIC;
+ }
+#endif
+
+ // Enable thousands separators
+ setlocale(LC_ALL, "");
+
+ // -d: queue depth
+ unsigned int depth = 4096;
+ // -j: threads
+ unsigned int threads = 0;
+ // -t: timeout
+ double timeout = 5.0;
+ // -L|-H: ioq_nop() type
+ enum ioq_nop_type type = IOQ_NOP_LIGHT;
+
+ const char *cmd = argc > 0 ? argv[0] : "ioq";
+ int c;
+ while (c = getopt(argc, argv, ":d:j:t:LH"), c != -1) {
+ switch (c) {
+ case 'd':
+ if (xstrtoui(optarg, NULL, 10, &depth) != 0) {
+ fprintf(stderr, "%s: Bad depth '%s': %s\n", cmd, optarg, errstr());
+ return EXIT_FAILURE;
+ }
+ break;
+ case 'j':
+ if (xstrtoui(optarg, NULL, 10, &threads) != 0) {
+ fprintf(stderr, "%s: Bad thread count '%s': %s\n", cmd, optarg, errstr());
+ return EXIT_FAILURE;
+ }
+ break;
+ case 't':
+ if (xstrtod(optarg, NULL, &timeout) != 0) {
+ fprintf(stderr, "%s: Bad timeout '%s': %s\n", cmd, optarg, errstr());
+ return EXIT_FAILURE;
+ }
+ break;
+ case 'L':
+ type = IOQ_NOP_LIGHT;
+ break;
+ case 'H':
+ type = IOQ_NOP_HEAVY;
+ break;
+ case ':':
+ fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ case '?':
+ fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (!threads) {
+ threads = nproc();
+ if (threads > 8) {
+ threads = 8;
+ }
+ }
+ if (threads < 2) {
+ threads = 2;
+ }
+ --threads;
+
+ // Listen for ^C to print the summary
+ struct sighook *hook = sighook(SIGINT, ctrlc, NULL, SH_CONTINUE | SH_ONESHOT);
+
+ printf("I/O queue benchmark (%s)\n\n", bfs_version);
+
+ printf("[-d] depth: %u\n", depth);
+ printf("[-j] threads: %u (including main)\n", threads + 1);
+ if (type == IOQ_NOP_HEAVY) {
+ printf("[-H] type: heavy (with syscalls)\n");
+ } else {
+ printf("[-L] type: light (no syscalls)\n");
+ }
+ printf("\n");
+
+ printf(" Time │ Throughput │ Latency │ min │ 50%% │ 90%% │ 99%% │ max\n");
+ printf(" (s) │ (IO/s) │ (ns/IO) │ │ │ │ │\n");
+ printf("â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•╪â•â•â•â•â•â•â•â•â•\n");
+ fflush(stdout);
+
+ struct ioq *ioq = ioq_create(depth, threads);
+ bfs_everify(ioq, "ioq_create(%u, %u)", depth, threads);
+
+ // Pre-allocate all the requests
+ while (ioq_capacity(ioq) > 0) {
+ int ret = ioq_nop(ioq, type, NULL);
+ bfs_everify(ret == 0, "ioq_nop(%d)", (int)type);
+ }
+ while (true) {
+ struct ioq_ent *ent = ioq_pop(ioq, true);
+ if (!ent) {
+ break;
+ }
+ ioq_free(ioq, ent);
+ }
+
+ struct times total, lap;
+ times_init(&total);
+ lap = total;
+
+ long seconds = 0;
+ while (!load(&quit, relaxed)) {
+ bool was_timing = lap.timing;
+
+ for (int i = 0; i < 16; ++i) {
+ bool block = ioq_capacity(ioq) == 0;
+ if (!pop(ioq, &lap, block)) {
+ break;
+ }
+ }
+
+ if (was_timing && !lap.timing) {
+ struct timespec elapsed;
+ gettime(&elapsed);
+ timespec_sub(&elapsed, &total.start);
+
+ if (elapsed.tv_sec > seconds) {
+ seconds = elapsed.tv_sec;
+ times_print(&lap, seconds);
+ times_lap(&total, &lap);
+ }
+
+ double ns = timespec_ns(&elapsed);
+ if (timeout > 0 && ns >= timeout * 1.0e9) {
+ break;
+ }
+ }
+
+ for (int i = 0; i < 8; ++i) {
+ if (!push(ioq, type, &lap)) {
+ break;
+ }
+ }
+ ioq_submit(ioq);
+ }
+
+ while (pop(ioq, &lap, true));
+ times_lap(&total, &lap);
+
+ if (load(&quit, relaxed)) {
+ printf("\r──^C──┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n");
+ } else {
+ printf("──────┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n");
+ }
+ times_print(&total, 0);
+
+ ioq_destroy(ioq);
+ sigunhook(hook);
+ return 0;
+}
diff --git a/bfs.h b/bfs.h
deleted file mode 100644
index f357807..0000000
--- a/bfs.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Constants about the bfs program itself.
- */
-
-#ifndef BFS_H
-#define BFS_H
-
-#ifndef BFS_VERSION
-# define BFS_VERSION "2.2.1"
-#endif
-
-#ifndef BFS_HOMEPAGE
-# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html"
-#endif
-
-#endif // BFS_H
diff --git a/bftw.c b/bftw.c
deleted file mode 100644
index 64b1120..0000000
--- a/bftw.c
+++ /dev/null
@@ -1,1557 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * The bftw() implementation consists of the following components:
- *
- * - struct bftw_file: A file that has been encountered during the traversal.
- * They have reference-counted links to their parents in the directory tree.
- *
- * - struct bftw_cache: Holds bftw_file's with open file descriptors, used for
- * openat() to minimize the amount of path re-traversals that need to happen.
- * Currently implemented as a priority queue based on depth and reference
- * count.
- *
- * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented
- * as a simple circular buffer.
- *
- * - struct bftw_state: Represents the current state of the traversal, allowing
- * various helper functions to take fewer parameters.
- */
-
-#include "bftw.h"
-#include "dir.h"
-#include "dstring.h"
-#include "mtab.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-/**
- * A file.
- */
-struct bftw_file {
- /** The parent directory, if any. */
- struct bftw_file *parent;
- /** The root under which this file was found. */
- struct bftw_file *root;
- /** The next file in the queue, if any. */
- struct bftw_file *next;
-
- /** This file's depth in the walk. */
- size_t depth;
- /** Reference count. */
- size_t refcount;
- /** Index in the bftw_cache priority queue. */
- size_t heap_index;
-
- /** An open descriptor to this file, or -1. */
- int fd;
-
- /** This file's type, if known. */
- enum bfs_type type;
- /** The device number, for cycle detection. */
- dev_t dev;
- /** The inode number, for cycle detection. */
- ino_t ino;
-
- /** The offset of this file in the full path. */
- size_t nameoff;
- /** The length of the file's name. */
- size_t namelen;
- /** The file's name. */
- char name[];
-};
-
-/**
- * A cache of open directories.
- */
-struct bftw_cache {
- /** A min-heap of open directories. */
- struct bftw_file **heap;
- /** Current heap size. */
- size_t size;
- /** Maximum heap size. */
- size_t capacity;
-};
-
-/** Initialize a cache. */
-static int bftw_cache_init(struct bftw_cache *cache, size_t capacity) {
- cache->heap = malloc(capacity*sizeof(*cache->heap));
- if (!cache->heap) {
- return -1;
- }
-
- cache->size = 0;
- cache->capacity = capacity;
- return 0;
-}
-
-/** Destroy a cache. */
-static void bftw_cache_destroy(struct bftw_cache *cache) {
- assert(cache->size == 0);
- free(cache->heap);
-}
-
-/** Check if two heap entries are in heap order. */
-static bool bftw_heap_check(const struct bftw_file *parent, const struct bftw_file *child) {
- if (parent->depth > child->depth) {
- return true;
- } else if (parent->depth < child->depth) {
- return false;
- } else {
- return parent->refcount <= child->refcount;
- }
-}
-
-/** Move a bftw_file to a particular place in the heap. */
-static void bftw_heap_move(struct bftw_cache *cache, struct bftw_file *file, size_t i) {
- cache->heap[i] = file;
- file->heap_index = i;
-}
-
-/** Bubble an entry up the heap. */
-static void bftw_heap_bubble_up(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- while (i > 0) {
- size_t pi = (i - 1)/2;
- struct bftw_file *parent = cache->heap[pi];
- if (bftw_heap_check(parent, file)) {
- break;
- }
-
- bftw_heap_move(cache, parent, i);
- i = pi;
- }
-
- bftw_heap_move(cache, file, i);
-}
-
-/** Bubble an entry down the heap. */
-static void bftw_heap_bubble_down(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- while (true) {
- size_t ci = 2*i + 1;
- if (ci >= cache->size) {
- break;
- }
-
- struct bftw_file *child = cache->heap[ci];
-
- size_t ri = ci + 1;
- if (ri < cache->size) {
- struct bftw_file *right = cache->heap[ri];
- if (!bftw_heap_check(child, right)) {
- ci = ri;
- child = right;
- }
- }
-
- if (bftw_heap_check(file, child)) {
- break;
- }
-
- bftw_heap_move(cache, child, i);
- i = ci;
- }
-
- bftw_heap_move(cache, file, i);
-}
-
-/** Bubble an entry up or down the heap. */
-static void bftw_heap_bubble(struct bftw_cache *cache, struct bftw_file *file) {
- size_t i = file->heap_index;
-
- if (i > 0) {
- size_t pi = (i - 1)/2;
- struct bftw_file *parent = cache->heap[pi];
- if (!bftw_heap_check(parent, file)) {
- bftw_heap_bubble_up(cache, file);
- return;
- }
- }
-
- bftw_heap_bubble_down(cache, file);
-}
-
-/** Increment a bftw_file's reference count. */
-static size_t bftw_file_incref(struct bftw_cache *cache, struct bftw_file *file) {
- size_t ret = ++file->refcount;
- if (file->fd >= 0) {
- bftw_heap_bubble_down(cache, file);
- }
- return ret;
-}
-
-/** Decrement a bftw_file's reference count. */
-static size_t bftw_file_decref(struct bftw_cache *cache, struct bftw_file *file) {
- size_t ret = --file->refcount;
- if (file->fd >= 0) {
- bftw_heap_bubble_up(cache, file);
- }
- return ret;
-}
-
-/** Add a bftw_file to the cache. */
-static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
- assert(cache->size < cache->capacity);
- assert(file->fd >= 0);
-
- size_t size = cache->size++;
- file->heap_index = size;
- bftw_heap_bubble_up(cache, file);
-}
-
-/** Remove a bftw_file from the cache. */
-static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
- assert(cache->size > 0);
-
- size_t size = --cache->size;
- size_t i = file->heap_index;
- if (i != size) {
- struct bftw_file *end = cache->heap[size];
- end->heap_index = i;
- bftw_heap_bubble(cache, end);
- }
-}
-
-/** Close a bftw_file. */
-static void bftw_file_close(struct bftw_cache *cache, struct bftw_file *file) {
- assert(file->fd >= 0);
-
- bftw_cache_remove(cache, file);
-
- close(file->fd);
- file->fd = -1;
-}
-
-/** Pop a directory from the cache. */
-static void bftw_cache_pop(struct bftw_cache *cache) {
- assert(cache->size > 0);
- bftw_file_close(cache, cache->heap[0]);
-}
-
-/**
- * Shrink the cache, to recover from EMFILE.
- *
- * @param cache
- * The cache in question.
- * @param saved
- * A bftw_file that must be preserved.
- * @return
- * 0 if successfully shrunk, otherwise -1.
- */
-static int bftw_cache_shrink(struct bftw_cache *cache, const struct bftw_file *saved) {
- int ret = -1;
- struct bftw_file *file = NULL;
-
- if (cache->size >= 1) {
- file = cache->heap[0];
- if (file == saved && cache->size >= 2) {
- file = cache->heap[1];
- }
- }
-
- if (file && file != saved) {
- bftw_file_close(cache, file);
- ret = 0;
- }
-
- cache->capacity = cache->size;
- return ret;
-}
-
-/** Compute the name offset of a child path. */
-static size_t bftw_child_nameoff(const struct bftw_file *parent) {
- size_t ret = parent->nameoff + parent->namelen;
- if (parent->name[parent->namelen - 1] != '/') {
- ++ret;
- }
- return ret;
-}
-
-/** Create a new bftw_file. */
-static struct bftw_file *bftw_file_new(struct bftw_cache *cache, struct bftw_file *parent, const char *name) {
- size_t namelen = strlen(name);
- size_t size = BFS_FLEX_SIZEOF(struct bftw_file, name, namelen + 1);
-
- struct bftw_file *file = malloc(size);
- if (!file) {
- return NULL;
- }
-
- file->parent = parent;
-
- if (parent) {
- file->root = parent->root;
- file->depth = parent->depth + 1;
- file->nameoff = bftw_child_nameoff(parent);
- bftw_file_incref(cache, parent);
- } else {
- file->root = file;
- file->depth = 0;
- file->nameoff = 0;
- }
-
- file->next = NULL;
-
- file->refcount = 1;
- file->fd = -1;
-
- file->type = BFS_UNKNOWN;
- file->dev = -1;
- file->ino = -1;
-
- file->namelen = namelen;
- memcpy(file->name, name, namelen + 1);
-
- return file;
-}
-
-/**
- * Open a bftw_file relative to another one.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The file to open.
- * @param base
- * The base directory for the relative path (may be NULL).
- * @param at_fd
- * The base file descriptor, AT_FDCWD if base == NULL.
- * @param at_path
- * The relative path to the file.
- * @return
- * The opened file descriptor, or negative on error.
- */
-static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, const struct bftw_file *base, const char *at_path) {
- assert(file->fd < 0);
-
- int at_fd = AT_FDCWD;
- if (base) {
- at_fd = base->fd;
- assert(at_fd >= 0);
- }
-
- int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY;
- int fd = openat(at_fd, at_path, flags);
-
- if (fd < 0 && errno == EMFILE) {
- if (bftw_cache_shrink(cache, base) == 0) {
- fd = openat(at_fd, at_path, flags);
- }
- }
-
- if (fd >= 0) {
- if (cache->size == cache->capacity) {
- bftw_cache_pop(cache);
- }
-
- file->fd = fd;
- bftw_cache_add(cache, file);
- }
-
- return fd;
-}
-
-/**
- * Open a bftw_file.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The file to open.
- * @param path
- * The full path to the file.
- * @return
- * The opened file descriptor, or negative on error.
- */
-static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
- // Find the nearest open ancestor
- struct bftw_file *base = file;
- do {
- base = base->parent;
- } while (base && base->fd < 0);
-
- const char *at_path = path;
- if (base) {
- at_path += bftw_child_nameoff(base);
- }
-
- int fd = bftw_file_openat(cache, file, base, at_path);
- if (fd >= 0 || errno != ENAMETOOLONG) {
- return fd;
- }
-
- // Handle ENAMETOOLONG by manually traversing the path component-by-component
-
- // Use the ->next linked list to temporarily hold the reversed parent
- // chain between base and file
- struct bftw_file *cur;
- for (cur = file; cur->parent != base; cur = cur->parent) {
- cur->parent->next = cur;
- }
-
- // Open the files in the chain one by one
- for (base = cur; base; base = base->next) {
- fd = bftw_file_openat(cache, base, base->parent, base->name);
- if (fd < 0 || base == file) {
- break;
- }
- }
-
- // Clear out the linked list
- for (struct bftw_file *next = cur->next; cur != file; cur = next, next = next->next) {
- cur->next = NULL;
- }
-
- return fd;
-}
-
-/**
- * Open a bftw_file as a directory.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The directory to open.
- * @param path
- * The full path to the directory.
- * @return
- * The opened directory, or NULL on error.
- */
-static struct bfs_dir *bftw_file_opendir(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
- int fd = bftw_file_open(cache, file, path);
- if (fd < 0) {
- return NULL;
- }
-
- return bfs_opendir(fd, NULL);
-}
-
-/** Free a bftw_file. */
-static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) {
- assert(file->refcount == 0);
-
- if (file->fd >= 0) {
- bftw_file_close(cache, file);
- }
-
- free(file);
-}
-
-/**
- * A queue of bftw_file's to examine.
- */
-struct bftw_queue {
- /** The head of the queue. */
- struct bftw_file *head;
- /** The insertion target. */
- struct bftw_file **target;
-};
-
-/** Initialize a bftw_queue. */
-static void bftw_queue_init(struct bftw_queue *queue) {
- queue->head = NULL;
- queue->target = &queue->head;
-}
-
-/** Add a file to a bftw_queue. */
-static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
- assert(file->next == NULL);
-
- file->next = *queue->target;
- *queue->target = file;
- queue->target = &file->next;
-}
-
-/** Pop the next file from the head of the queue. */
-static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) {
- struct bftw_file *file = queue->head;
- queue->head = file->next;
- file->next = NULL;
- if (queue->target == &file->next) {
- queue->target = &queue->head;
- }
- return file;
-}
-
-/** The split phase of mergesort. */
-static struct bftw_file **bftw_sort_split(struct bftw_file **head, struct bftw_file **tail) {
- struct bftw_file **tortoise = head, **hare = head;
-
- while (*hare != *tail) {
- tortoise = &(*tortoise)->next;
- hare = &(*hare)->next;
- if (*hare != *tail) {
- hare = &(*hare)->next;
- }
- }
-
- return tortoise;
-}
-
-/** The merge phase of mergesort. */
-static struct bftw_file **bftw_sort_merge(struct bftw_file **head, struct bftw_file **mid, struct bftw_file **tail) {
- struct bftw_file *left = *head, *right = *mid, *end = *tail;
- *mid = NULL;
- *tail = NULL;
-
- while (left || right) {
- struct bftw_file *next;
- if (left && (!right || strcoll(left->name, right->name) <= 0)) {
- next = left;
- left = left->next;
- } else {
- next = right;
- right = right->next;
- }
-
- *head = next;
- head = &next->next;
- }
-
- *head = end;
- return head;
-}
-
-/**
- * Sort a (sub-)list of files.
- *
- * @param head
- * The head of the (sub-)list to sort.
- * @param tail
- * The tail of the (sub-)list to sort.
- * @return
- * The new tail of the (sub-)list.
- */
-static struct bftw_file **bftw_sort_files(struct bftw_file **head, struct bftw_file **tail) {
- struct bftw_file **mid = bftw_sort_split(head, tail);
- if (*mid == *head || *mid == *tail) {
- return tail;
- }
-
- mid = bftw_sort_files(head, mid);
- tail = bftw_sort_files(mid, tail);
-
- return bftw_sort_merge(head, mid, tail);
-}
-
-/**
- * Holds the current state of the bftw() traversal.
- */
-struct bftw_state {
- /** bftw() callback. */
- bftw_callback *callback;
- /** bftw() callback data. */
- void *ptr;
- /** bftw() flags. */
- enum bftw_flags flags;
- /** Search strategy. */
- enum bftw_strategy strategy;
- /** The mount table. */
- const struct bfs_mtab *mtab;
-
- /** The appropriate errno value, if any. */
- int error;
-
- /** The cache of open directories. */
- struct bftw_cache cache;
- /** The queue of directories left to explore. */
- struct bftw_queue queue;
- /** The start of the current batch of files. */
- struct bftw_file **batch;
-
- /** The current path. */
- char *path;
- /** The current file. */
- struct bftw_file *file;
- /** The previous file. */
- struct bftw_file *previous;
-
- /** The currently open directory. */
- struct bfs_dir *dir;
- /** The current directory entry. */
- struct bfs_dirent *de;
- /** Storage for the directory entry. */
- struct bfs_dirent de_storage;
- /** Any error encountered while reading the directory. */
- int direrror;
-
- /** Extra data about the current file. */
- struct BFTW ftwbuf;
-};
-
-/**
- * Initialize the bftw() state.
- */
-static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) {
- state->callback = args->callback;
- state->ptr = args->ptr;
- state->flags = args->flags;
- state->strategy = args->strategy;
- state->mtab = args->mtab;
-
- state->error = 0;
-
- if (args->nopenfd < 2) {
- errno = EMFILE;
- goto err;
- }
-
- // Reserve 1 fd for the open bfs_dir
- if (bftw_cache_init(&state->cache, args->nopenfd - 1) != 0) {
- goto err;
- }
-
- bftw_queue_init(&state->queue);
- state->batch = NULL;
-
- state->path = dstralloc(0);
- if (!state->path) {
- goto err_cache;
- }
-
- state->file = NULL;
- state->previous = NULL;
-
- state->dir = NULL;
- state->de = NULL;
- state->direrror = 0;
-
- return 0;
-
-err_cache:
- bftw_cache_destroy(&state->cache);
-err:
- return -1;
-}
-
-/** Cached bfs_stat(). */
-static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flags flags) {
- if (!cache->buf) {
- if (cache->error) {
- errno = cache->error;
- } else if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, &cache->storage) == 0) {
- cache->buf = &cache->storage;
- } else {
- cache->error = errno;
- }
- }
-
- return cache->buf;
-}
-
-const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
- const struct bfs_stat *ret;
-
- if (flags & BFS_STAT_NOFOLLOW) {
- ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW);
- if (ret && !S_ISLNK(ret->mode) && !mutbuf->stat_cache.buf) {
- // Non-link, so share stat info
- mutbuf->stat_cache.buf = ret;
- }
- } else {
- ret = bftw_stat_impl(mutbuf, &mutbuf->stat_cache, BFS_STAT_FOLLOW);
- if (!ret && (flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(errno)) {
- ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW);
- }
- }
-
- return ret;
-}
-
-const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- if (flags & BFS_STAT_NOFOLLOW) {
- return ftwbuf->lstat_cache.buf;
- } else if (ftwbuf->stat_cache.buf) {
- return ftwbuf->stat_cache.buf;
- } else if ((flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(ftwbuf->stat_cache.error)) {
- return ftwbuf->lstat_cache.buf;
- } else {
- return NULL;
- }
-}
-
-enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- if (flags & BFS_STAT_NOFOLLOW) {
- if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- return ftwbuf->type;
- }
- } else if (flags & BFS_STAT_TRYFOLLOW) {
- if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) {
- return ftwbuf->type;
- }
- } else {
- if (ftwbuf->type != BFS_LNK) {
- return ftwbuf->type;
- } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) {
- return BFS_ERROR;
- }
- }
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
- if (statbuf) {
- return bfs_mode_to_type(statbuf->mode);
- } else {
- return BFS_ERROR;
- }
-}
-
-/**
- * Update the path for the current file.
- */
-static int bftw_update_path(struct bftw_state *state, const char *name) {
- const struct bftw_file *file = state->file;
- size_t length = file ? file->nameoff + file->namelen : 0;
-
- assert(dstrlen(state->path) >= length);
- dstresize(&state->path, length);
-
- if (name) {
- if (length > 0 && state->path[length - 1] != '/') {
- if (dstrapp(&state->path, '/') != 0) {
- return -1;
- }
- }
- if (dstrcat(&state->path, name) != 0) {
- return -1;
- }
- }
-
- return 0;
-}
-
-/** Check if a stat() call is needed for this visit. */
-static bool bftw_need_stat(const struct bftw_state *state) {
- if (state->flags & BFTW_STAT) {
- return true;
- }
-
- const struct BFTW *ftwbuf = &state->ftwbuf;
- if (ftwbuf->type == BFS_UNKNOWN) {
- return true;
- }
-
- if (ftwbuf->type == BFS_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- return true;
- }
-
- if (ftwbuf->type == BFS_DIR) {
- if (state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) {
- return true;
- }
-#if __linux__
- } else if (state->mtab) {
- // Linux fills in d_type from the underlying inode, even when
- // the directory entry is a bind mount point. In that case, we
- // need to stat() to get the correct type. We don't need to
- // check for directories because they can only be mounted over
- // by other directories.
- if (bfs_might_be_mount(state->mtab, ftwbuf->path)) {
- return true;
- }
-#endif
- }
-
- return false;
-}
-
-/** Initialize bftw_stat cache. */
-static void bftw_stat_init(struct bftw_stat *cache) {
- cache->buf = NULL;
- cache->error = 0;
-}
-
-/**
- * Open a file if necessary.
- *
- * @param file
- * The file to open.
- * @param path
- * The path to that file or one of its descendants.
- * @return
- * The opened file descriptor, or -1 on error.
- */
-static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
- int ret = file->fd;
-
- if (ret < 0) {
- char *copy = strndup(path, file->nameoff + file->namelen);
- if (!copy) {
- return -1;
- }
-
- ret = bftw_file_open(cache, file, copy);
- free(copy);
- }
-
- return ret;
-}
-
-/**
- * Initialize the buffers with data about the current path.
- */
-static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
- struct bftw_file *file = state->file;
- const struct bfs_dirent *de = state->de;
-
- struct BFTW *ftwbuf = &state->ftwbuf;
- ftwbuf->path = state->path;
- ftwbuf->root = file ? file->root->name : ftwbuf->path;
- ftwbuf->depth = 0;
- ftwbuf->visit = visit;
- ftwbuf->type = BFS_UNKNOWN;
- ftwbuf->error = state->direrror;
- ftwbuf->at_fd = AT_FDCWD;
- ftwbuf->at_path = ftwbuf->path;
- ftwbuf->stat_flags = BFS_STAT_NOFOLLOW;
- bftw_stat_init(&ftwbuf->lstat_cache);
- bftw_stat_init(&ftwbuf->stat_cache);
-
- struct bftw_file *parent = NULL;
- if (de) {
- parent = file;
- ftwbuf->depth = file->depth + 1;
- ftwbuf->type = de->type;
- ftwbuf->nameoff = bftw_child_nameoff(file);
- } else if (file) {
- parent = file->parent;
- ftwbuf->depth = file->depth;
- ftwbuf->type = file->type;
- ftwbuf->nameoff = file->nameoff;
- }
-
- if (parent) {
- // Try to ensure the immediate parent is open, to avoid ENAMETOOLONG
- if (bftw_ensure_open(&state->cache, parent, state->path) >= 0) {
- ftwbuf->at_fd = parent->fd;
- ftwbuf->at_path += ftwbuf->nameoff;
- } else {
- ftwbuf->error = errno;
- }
- }
-
- if (ftwbuf->depth == 0) {
- // Compute the name offset for root paths like "foo/bar"
- ftwbuf->nameoff = xbasename(ftwbuf->path) - ftwbuf->path;
- }
-
- if (ftwbuf->error != 0) {
- ftwbuf->type = BFS_ERROR;
- return;
- }
-
- int follow_flags = BFTW_FOLLOW_ALL;
- if (ftwbuf->depth == 0) {
- follow_flags |= BFTW_FOLLOW_ROOTS;
- }
- bool follow = state->flags & follow_flags;
- if (follow) {
- ftwbuf->stat_flags = BFS_STAT_TRYFOLLOW;
- }
-
- const struct bfs_stat *statbuf = NULL;
- if (bftw_need_stat(state)) {
- statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (statbuf) {
- ftwbuf->type = bfs_mode_to_type(statbuf->mode);
- } else {
- ftwbuf->type = BFS_ERROR;
- ftwbuf->error = errno;
- return;
- }
- }
-
- if (ftwbuf->type == BFS_DIR && (state->flags & BFTW_DETECT_CYCLES)) {
- for (const struct bftw_file *ancestor = parent; ancestor; ancestor = ancestor->parent) {
- if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) {
- ftwbuf->type = BFS_ERROR;
- ftwbuf->error = ELOOP;
- return;
- }
- }
- }
-}
-
-/** Check if the current file is a mount point. */
-static bool bftw_is_mount(struct bftw_state *state, const char *name) {
- const struct bftw_file *file = state->file;
- if (!file) {
- return false;
- }
-
- const struct bftw_file *parent = name ? file : file->parent;
- if (!parent) {
- return false;
- }
-
- const struct BFTW *ftwbuf = &state->ftwbuf;
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- return statbuf && statbuf->dev != parent->dev;
-}
-
-/** Fill file identity information from an ftwbuf. */
-static void bftw_fill_id(struct bftw_file *file, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
- if (!statbuf || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- statbuf = ftwbuf->lstat_cache.buf;
- }
- if (statbuf) {
- file->dev = statbuf->dev;
- file->ino = statbuf->ino;
- }
-}
-
-/**
- * Visit a path, invoking the callback.
- */
-static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, enum bftw_visit visit) {
- if (bftw_update_path(state, name) != 0) {
- state->error = errno;
- return BFTW_STOP;
- }
-
- const struct BFTW *ftwbuf = &state->ftwbuf;
- bftw_init_ftwbuf(state, visit);
-
- // Never give the callback BFS_ERROR unless BFTW_RECOVER is specified
- if (ftwbuf->type == BFS_ERROR && !(state->flags & BFTW_RECOVER)) {
- state->error = ftwbuf->error;
- return BFTW_STOP;
- }
-
- if ((state->flags & BFTW_SKIP_MOUNTS) && bftw_is_mount(state, name)) {
- return BFTW_PRUNE;
- }
-
- enum bftw_action ret = state->callback(ftwbuf, state->ptr);
- switch (ret) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- case BFTW_STOP:
- goto done;
- default:
- state->error = EINVAL;
- return BFTW_STOP;
- }
-
- if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) {
- ret = BFTW_PRUNE;
- goto done;
- }
-
- if ((state->flags & BFTW_PRUNE_MOUNTS) && bftw_is_mount(state, name)) {
- ret = BFTW_PRUNE;
- goto done;
- }
-
-done:
- if (state->file && !name) {
- bftw_fill_id(state->file, ftwbuf);
- }
-
- return ret;
-}
-
-/**
- * Push a new file onto the queue.
- */
-static int bftw_push(struct bftw_state *state, const char *name, bool fill_id) {
- struct bftw_file *parent = state->file;
- struct bftw_file *file = bftw_file_new(&state->cache, parent, name);
- if (!file) {
- state->error = errno;
- return -1;
- }
-
- if (state->de) {
- file->type = state->de->type;
- }
-
- if (fill_id) {
- bftw_fill_id(file, &state->ftwbuf);
- }
-
- bftw_queue_push(&state->queue, file);
-
- return 0;
-}
-
-/**
- * Build the path to the current file.
- */
-static int bftw_build_path(struct bftw_state *state) {
- const struct bftw_file *file = state->file;
-
- size_t pathlen = file->nameoff + file->namelen;
- if (dstresize(&state->path, pathlen) != 0) {
- state->error = errno;
- return -1;
- }
-
- // Try to find a common ancestor with the existing path
- const struct bftw_file *ancestor = state->previous;
- while (ancestor && ancestor->depth > file->depth) {
- ancestor = ancestor->parent;
- }
-
- // Build the path backwards
- while (file && file != ancestor) {
- if (file->nameoff > 0) {
- state->path[file->nameoff - 1] = '/';
- }
- memcpy(state->path + file->nameoff, file->name, file->namelen);
-
- if (ancestor && ancestor->depth == file->depth) {
- ancestor = ancestor->parent;
- }
- file = file->parent;
- }
-
- state->previous = state->file;
- return 0;
-}
-
-/**
- * Pop the next file from the queue.
- */
-static int bftw_pop(struct bftw_state *state) {
- if (!state->queue.head) {
- return 0;
- }
-
- state->file = bftw_queue_pop(&state->queue);
-
- if (bftw_build_path(state) != 0) {
- return -1;
- }
-
- return 1;
-}
-
-/**
- * Open the current directory.
- */
-static void bftw_opendir(struct bftw_state *state) {
- assert(!state->dir);
- assert(!state->de);
-
- state->direrror = 0;
-
- state->dir = bftw_file_opendir(&state->cache, state->file, state->path);
- if (!state->dir) {
- state->direrror = errno;
- }
-}
-
-/**
- * Read an entry from the current directory.
- */
-static int bftw_readdir(struct bftw_state *state) {
- if (!state->dir) {
- return -1;
- }
-
- int ret = bfs_readdir(state->dir, &state->de_storage);
- if (ret > 0) {
- state->de = &state->de_storage;
- } else if (ret == 0) {
- state->de = NULL;
- } else {
- state->de = NULL;
- state->direrror = errno;
- }
-
- return ret;
-}
-
-/**
- * Flags controlling which files get visited when done with a directory.
- */
-enum bftw_gc_flags {
- /** Don't visit anything. */
- BFTW_VISIT_NONE = 0,
- /** Visit the file itself. */
- BFTW_VISIT_FILE = 1 << 0,
- /** Visit the file's ancestors. */
- BFTW_VISIT_PARENTS = 1 << 1,
- /** Visit both the file and its ancestors. */
- BFTW_VISIT_ALL = BFTW_VISIT_FILE | BFTW_VISIT_PARENTS,
-};
-
-/**
- * Close the current directory.
- */
-static enum bftw_action bftw_closedir(struct bftw_state *state, enum bftw_gc_flags flags) {
- struct bftw_file *file = state->file;
- enum bftw_action ret = BFTW_CONTINUE;
-
- if (state->dir) {
- assert(file->fd >= 0);
-
- if (file->refcount > 1) {
- // Keep the fd around if any subdirectories exist
- file->fd = bfs_freedir(state->dir);
- } else {
- bfs_closedir(state->dir);
- file->fd = -1;
- }
-
- if (file->fd < 0) {
- bftw_cache_remove(&state->cache, file);
- }
- }
-
- state->de = NULL;
- state->dir = NULL;
-
- if (state->direrror != 0) {
- if (flags & BFTW_VISIT_FILE) {
- ret = bftw_visit(state, NULL, BFTW_PRE);
- } else {
- state->error = state->direrror;
- }
- state->direrror = 0;
- }
-
- return ret;
-}
-
-/**
- * Finalize and free a file we're done with.
- */
-static enum bftw_action bftw_gc_file(struct bftw_state *state, enum bftw_gc_flags flags) {
- enum bftw_action ret = BFTW_CONTINUE;
-
- if (!(state->flags & BFTW_POST_ORDER)) {
- flags = 0;
- }
- bool visit = flags & BFTW_VISIT_FILE;
-
- while (state->file) {
- if (bftw_file_decref(&state->cache, state->file) > 0) {
- state->file = NULL;
- break;
- }
-
- if (visit && bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) {
- ret = BFTW_STOP;
- flags &= ~BFTW_VISIT_PARENTS;
- }
- visit = flags & BFTW_VISIT_PARENTS;
-
- struct bftw_file *parent = state->file->parent;
- if (state->previous == state->file) {
- state->previous = parent;
- }
- bftw_file_free(&state->cache, state->file);
- state->file = parent;
- }
-
- return ret;
-}
-
-/**
- * Drain all the entries from a bftw_queue.
- */
-static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue) {
- while (queue->head) {
- state->file = bftw_queue_pop(queue);
- bftw_gc_file(state, BFTW_VISIT_NONE);
- }
-}
-
-/**
- * Dispose of the bftw() state.
- *
- * @return
- * The bftw() return value.
- */
-static int bftw_state_destroy(struct bftw_state *state) {
- dstrfree(state->path);
-
- bftw_closedir(state, BFTW_VISIT_NONE);
-
- bftw_gc_file(state, BFTW_VISIT_NONE);
- bftw_drain_queue(state, &state->queue);
-
- bftw_cache_destroy(&state->cache);
-
- errno = state->error;
- return state->error ? -1 : 0;
-}
-
-/** Start a batch of files. */
-static void bftw_batch_start(struct bftw_state *state) {
- if (state->strategy == BFTW_DFS) {
- state->queue.target = &state->queue.head;
- }
- state->batch = state->queue.target;
-}
-
-/** Finish adding a batch of files. */
-static void bftw_batch_finish(struct bftw_state *state) {
- if (state->flags & BFTW_SORT) {
- state->queue.target = bftw_sort_files(state->batch, state->queue.target);
- }
-}
-
-/**
- * Streaming mode: visit files as they are encountered.
- */
-static int bftw_stream(const struct bftw_args *args) {
- struct bftw_state state;
- if (bftw_state_init(&state, args) != 0) {
- return -1;
- }
-
- assert(!(state.flags & BFTW_SORT));
-
- bftw_batch_start(&state);
- for (size_t i = 0; i < args->npaths; ++i) {
- const char *path = args->paths[i];
-
- switch (bftw_visit(&state, path, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- continue;
- case BFTW_STOP:
- goto done;
- }
-
- if (bftw_push(&state, path, true) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- while (bftw_pop(&state) > 0) {
- bftw_opendir(&state);
-
- bftw_batch_start(&state);
- while (bftw_readdir(&state) > 0) {
- const char *name = state.de->name;
-
- switch (bftw_visit(&state, name, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- continue;
- case BFTW_STOP:
- goto done;
- }
-
- if (bftw_push(&state, name, true) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- if (bftw_closedir(&state, BFTW_VISIT_ALL) == BFTW_STOP) {
- goto done;
- }
- if (bftw_gc_file(&state, BFTW_VISIT_ALL) == BFTW_STOP) {
- goto done;
- }
- }
-
-done:
- return bftw_state_destroy(&state);
-}
-
-/**
- * Batching mode: queue up all children before visiting them.
- */
-static int bftw_batch(const struct bftw_args *args) {
- struct bftw_state state;
- if (bftw_state_init(&state, args) != 0) {
- return -1;
- }
-
- bftw_batch_start(&state);
- for (size_t i = 0; i < args->npaths; ++i) {
- if (bftw_push(&state, args->paths[i], false) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- while (bftw_pop(&state) > 0) {
- enum bftw_gc_flags gcflags = BFTW_VISIT_ALL;
-
- switch (bftw_visit(&state, NULL, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- gcflags &= ~BFTW_VISIT_FILE;
- goto next;
- case BFTW_STOP:
- goto done;
- }
-
- bftw_opendir(&state);
-
- bftw_batch_start(&state);
- while (bftw_readdir(&state) > 0) {
- if (bftw_push(&state, state.de->name, false) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- if (bftw_closedir(&state, gcflags) == BFTW_STOP) {
- goto done;
- }
-
- next:
- if (bftw_gc_file(&state, gcflags) == BFTW_STOP) {
- goto done;
- }
- }
-
-done:
- return bftw_state_destroy(&state);
-}
-
-/** Select bftw_stream() or bftw_batch() appropriately. */
-static int bftw_auto(const struct bftw_args *args) {
- if (args->flags & BFTW_SORT) {
- return bftw_batch(args);
- } else {
- return bftw_stream(args);
- }
-}
-
-/**
- * Iterative deepening search state.
- */
-struct bftw_ids_state {
- /** The wrapped callback. */
- bftw_callback *delegate;
- /** The wrapped callback arguments. */
- void *ptr;
- /** Which visit this search corresponds to. */
- enum bftw_visit visit;
- /** Whether to override the bftw_visit. */
- bool force_visit;
- /** The current minimum depth (inclusive). */
- size_t min_depth;
- /** The current maximum depth (exclusive). */
- size_t max_depth;
- /** The set of pruned paths. */
- struct trie pruned;
- /** An error code to report. */
- int error;
- /** Whether the bottom has been found. */
- bool bottom;
- /** Whether to quit the search. */
- bool quit;
-};
-
-/** Iterative deepening callback function. */
-static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr) {
- struct bftw_ids_state *state = ptr;
-
- if (state->force_visit) {
- struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
- mutbuf->visit = state->visit;
- }
-
- if (ftwbuf->type == BFS_ERROR) {
- if (ftwbuf->depth + 1 >= state->min_depth) {
- return state->delegate(ftwbuf, state->ptr);
- } else {
- return BFTW_PRUNE;
- }
- }
-
- if (ftwbuf->depth < state->min_depth) {
- if (trie_find_str(&state->pruned, ftwbuf->path)) {
- return BFTW_PRUNE;
- } else {
- return BFTW_CONTINUE;
- }
- } else if (state->visit == BFTW_POST) {
- if (trie_find_str(&state->pruned, ftwbuf->path)) {
- return BFTW_PRUNE;
- }
- }
-
- enum bftw_action ret = BFTW_CONTINUE;
- if (ftwbuf->visit == state->visit) {
- ret = state->delegate(ftwbuf, state->ptr);
- }
-
- switch (ret) {
- case BFTW_CONTINUE:
- if (ftwbuf->type == BFS_DIR && ftwbuf->depth + 1 >= state->max_depth) {
- state->bottom = false;
- ret = BFTW_PRUNE;
- }
- break;
- case BFTW_PRUNE:
- if (ftwbuf->type == BFS_DIR) {
- if (!trie_insert_str(&state->pruned, ftwbuf->path)) {
- state->error = errno;
- state->quit = true;
- ret = BFTW_STOP;
- }
- }
- break;
- case BFTW_STOP:
- state->quit = true;
- break;
- }
-
- return ret;
-}
-
-/** Initialize iterative deepening state. */
-static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *state, struct bftw_args *ids_args) {
- state->delegate = args->callback;
- state->ptr = args->ptr;
- state->visit = BFTW_PRE;
- state->force_visit = false;
- state->min_depth = 0;
- state->max_depth = 1;
- trie_init(&state->pruned);
- state->error = 0;
- state->bottom = false;
- state->quit = false;
-
- *ids_args = *args;
- ids_args->callback = bftw_ids_callback;
- ids_args->ptr = state;
- ids_args->flags &= ~BFTW_POST_ORDER;
- ids_args->strategy = BFTW_DFS;
-}
-
-/** Finish an iterative deepening search. */
-static int bftw_ids_finish(struct bftw_ids_state *state) {
- int ret = 0;
-
- if (state->error) {
- ret = -1;
- } else {
- state->error = errno;
- }
-
- trie_destroy(&state->pruned);
-
- errno = state->error;
- return ret;
-}
-
-/**
- * Iterative deepening bftw() wrapper.
- */
-static int bftw_ids(const struct bftw_args *args) {
- struct bftw_ids_state state;
- struct bftw_args ids_args;
- bftw_ids_init(args, &state, &ids_args);
-
- while (!state.quit && !state.bottom) {
- state.bottom = true;
-
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
- }
-
- ++state.min_depth;
- ++state.max_depth;
- }
-
- if (args->flags & BFTW_POST_ORDER) {
- state.visit = BFTW_POST;
- state.force_visit = true;
-
- while (!state.quit && state.min_depth > 0) {
- --state.max_depth;
- --state.min_depth;
-
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
- }
- }
- }
-
- return bftw_ids_finish(&state);
-}
-
-/**
- * Exponential deepening bftw() wrapper.
- */
-static int bftw_eds(const struct bftw_args *args) {
- struct bftw_ids_state state;
- struct bftw_args ids_args;
- bftw_ids_init(args, &state, &ids_args);
-
- while (!state.quit && !state.bottom) {
- state.bottom = true;
-
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
- }
-
- state.min_depth = state.max_depth;
- state.max_depth *= 2;
- }
-
- if (!state.quit && (args->flags & BFTW_POST_ORDER)) {
- state.visit = BFTW_POST;
- state.min_depth = 0;
- ids_args.flags |= BFTW_POST_ORDER;
-
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- }
- }
-
- return bftw_ids_finish(&state);
-}
-
-int bftw(const struct bftw_args *args) {
- switch (args->strategy) {
- case BFTW_BFS:
- return bftw_auto(args);
- case BFTW_DFS:
- return bftw_batch(args);
- case BFTW_IDS:
- return bftw_ids(args);
- case BFTW_EDS:
- return bftw_eds(args);
- }
-
- errno = EINVAL;
- return -1;
-}
diff --git a/build/cc.sh b/build/cc.sh
new file mode 100755
index 0000000..e1d2b0b
--- /dev/null
+++ b/build/cc.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Run the compiler and check if it succeeded. Usage:
+#
+# $ build/cc.sh [-q] path/to/file.c [-flags -Warnings ...]
+
+set -eu
+
+QUIET=
+if [ "$1" = "-q" ]; then
+ QUIET=y
+ shift
+fi
+
+# Source files can specify their own flags with lines like
+#
+# /// _CFLAGS += -Wmissing-variable-declarations
+#
+# which will be added to the makefile on success, or lines like
+#
+# /// -Werror
+#
+# which are just used for the current file.
+EXTRA_FLAGS=$(sed -n '\|^///|{s|^/// ||; s|[^=]*= ||; p;}' "$1")
+
+# Without -q, print the executed command for config.log
+if [ -z "$QUIET" ]; then
+ set -x
+fi
+
+$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $EXTRA_FLAGS $XLDLIBS
diff --git a/build/config.mk b/build/config.mk
new file mode 100644
index 0000000..663926c
--- /dev/null
+++ b/build/config.mk
@@ -0,0 +1,51 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that implements `./configure`
+
+include build/prelude.mk
+include build/exports.mk
+
+# All configuration steps
+config: gen/config.mk gen/config.h
+.PHONY: config
+
+# The main configuration file, which includes the others
+gen/config.mk: gen/vars.mk gen/flags.mk gen/pkgs.mk
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'include %s\n' $^ >>$@
+ ${VCAT} $@
+.PHONY: gen/config.mk
+
+# Saves the configurable variables
+gen/vars.mk::
+ @${MKDIR} ${@D}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf 'PREFIX := %s\n' "$$XPREFIX" >>$@
+ @printf 'MANDIR := %s\n' "$$XMANDIR" >>$@
+ @printf 'OS := %s\n' "$${OS:-$$(uname)}" >>$@
+ @printf 'CC := %s\n' "$$XCC" >>$@
+ @printf 'INSTALL := %s\n' "$$XINSTALL" >>$@
+ @printf 'MKDIR := %s\n' "$$XMKDIR" >>$@
+ @printf 'PKG_CONFIG := %s\n' "$$XPKG_CONFIG" >>$@
+ @printf 'RM := %s\n' "$$XRM" >>$@
+ @test -z "$$VERSION" || printf 'export VERSION=%s\n' "$$VERSION" >>$@
+ ${VCAT} $@
+
+# Sets the build flags. This depends on vars.mk and uses a recursive make so
+# that the default flags can depend on variables like ${OS}.
+gen/flags.mk: gen/vars.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/flags.mk $@
+.PHONY: gen/flags.mk
+
+# Auto-detect dependencies and their build flags
+gen/pkgs.mk: gen/flags.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/pkgs.mk $@
+.PHONY: gen/pkgs.mk
+
+# Compile-time feature detection
+gen/config.h: gen/pkgs.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@
+.PHONY: gen/config.h
diff --git a/build/define-if.sh b/build/define-if.sh
new file mode 100755
index 0000000..204cfa4
--- /dev/null
+++ b/build/define-if.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Output a C preprocessor definition based on whether a command succeeds
+
+set -eu
+
+MACRO=$(printf 'BFS_%s' "$1" | tr '/a-z-' '_A-Z_')
+shift
+
+if "$@"; then
+ printf '#define %s true\n' "$MACRO"
+else
+ printf '#define %s false\n' "$MACRO"
+ exit 1
+fi
diff --git a/build/embed.sh b/build/embed.sh
new file mode 100755
index 0000000..c0744f6
--- /dev/null
+++ b/build/embed.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Convert data into a C array like #embed
+
+set -eu
+
+{ cat; printf '\0'; } \
+ | od -An -tx1 \
+ | sed 's/[^ ][^ ]*/0x&,/g'
diff --git a/build/empty.c b/build/empty.c
new file mode 100644
index 0000000..4fa9a5b
--- /dev/null
+++ b/build/empty.c
@@ -0,0 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+int main(void) {
+ return 0;
+}
diff --git a/build/exports.mk b/build/exports.mk
new file mode 100644
index 0000000..913a1aa
--- /dev/null
+++ b/build/exports.mk
@@ -0,0 +1,20 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile fragment that exports variables used by configuration scripts
+
+export XPREFIX=${PREFIX}
+export XMANDIR=${MANDIR}
+
+export XCC=${CC}
+export XINSTALL=${INSTALL}
+export XMKDIR=${MKDIR}
+export XPKG_CONFIG=${PKG_CONFIG}
+export XRM=${RM}
+
+export XCPPFLAGS=${_CPPFLAGS}
+export XCFLAGS=${_CFLAGS}
+export XLDFLAGS=${_LDFLAGS}
+export XLDLIBS=${_LDLIBS}
+
+export XNOLIBS=${NOLIBS}
diff --git a/build/flags-if.sh b/build/flags-if.sh
new file mode 100755
index 0000000..81eb345
--- /dev/null
+++ b/build/flags-if.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Add flags to a makefile if a build succeeds
+
+set -eu
+
+build/cc.sh "$@" || exit 1
+
+# If the build succeeded, print any lines like
+#
+# /// _CFLAGS += -foo
+#
+# (unless they're already set)
+OLD_FLAGS="$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS $XLDLIBS"
+
+while IFS="" read -r line; do
+ case "$line" in
+ ///*=*)
+ flag="${line#*= }"
+ if [ "${OLD_FLAGS#*"$flag"}" = "$OLD_FLAGS" ]; then
+ printf '%s\n' "${line#/// }"
+ fi
+ ;;
+ esac
+done <"$1"
diff --git a/build/flags.mk b/build/flags.mk
new file mode 100644
index 0000000..3748a8a
--- /dev/null
+++ b/build/flags.mk
@@ -0,0 +1,136 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/flags.mk
+
+include build/prelude.mk
+include gen/vars.mk
+
+# Internal flags
+_CPPFLAGS := -Isrc -Igen -include src/prelude.h
+_CFLAGS := -std=c17
+_LDFLAGS :=
+_LDLIBS :=
+
+# Platform-specific system libraries
+LDLIBS,DragonFly := -lposix1e
+LDLIBS,FreeBSD := -lrt
+LDLIBS,Linux := -lrt
+LDLIBS,NetBSD := -lutil
+LDLIBS,QNX := -lregex -lsocket
+LDLIBS,SunOS := -lsec -lsocket -lnsl
+_LDLIBS += ${LDLIBS,${OS}}
+
+# Build profiles
+_ASAN := ${TRUTHY,${ASAN}}
+_LSAN := ${TRUTHY,${LSAN}}
+_MSAN := ${TRUTHY,${MSAN}}
+_TSAN := ${TRUTHY,${TSAN}}
+_UBSAN := ${TRUTHY,${UBSAN}}
+_GCOV := ${TRUTHY,${GCOV}}
+_LINT := ${TRUTHY,${LINT}}
+_RELEASE := ${TRUTHY,${RELEASE}}
+
+LTO ?= ${RELEASE}
+_LTO := ${TRUTHY,${LTO}}
+
+ASAN_CFLAGS,y := -fsanitize=address
+LSAN_CFLAGS,y := -fsanitize=leak
+MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
+TSAN_CFLAGS,y := -fsanitize=thread
+UBSAN_CFLAGS,y := -fsanitize=undefined
+
+_CFLAGS += ${ASAN_CFLAGS,${_ASAN}}
+_CFLAGS += ${LSAN_CFLAGS,${_LSAN}}
+_CFLAGS += ${MSAN_CFLAGS,${_MSAN}}
+_CFLAGS += ${TSAN_CFLAGS,${_TSAN}}
+_CFLAGS += ${UBSAN_CFLAGS,${_UBSAN}}
+
+SAN_CFLAGS,y := -fno-sanitize-recover=all
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+SAN := ${NOT,${INSANE}}
+_CFLAGS += ${SAN_CFLAGS,${SAN}}
+
+# MSAN and TSAN both need all code to be instrumented
+YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+NOLIBS ?= ${NOT,${YESLIBS}}
+
+# gcov only intercepts fork()/exec() with -std=gnu*
+GCOV_CFLAGS,y := -std=gnu17 --coverage
+_CFLAGS += ${GCOV_CFLAGS,${_GCOV}}
+
+LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
+LINT_CFLAGS,y := -Werror -O2
+
+_CPPFLAGS += ${LINT_CPPFLAGS,${_LINT}}
+_CFLAGS += ${LINT_CFLAGS,${_LINT}}
+
+RELEASE_CPPFLAGS,y := -DNDEBUG
+RELEASE_CFLAGS,y := -O3
+
+_CPPFLAGS += ${RELEASE_CPPFLAGS,${_RELEASE}}
+_CFLAGS += ${RELEASE_CFLAGS,${_RELEASE}}
+
+LTO_CFLAGS,y := -flto=auto
+_CFLAGS += ${LTO_CFLAGS,${_LTO}}
+
+# Configurable flags
+CFLAGS ?= -g -Wall
+
+# Add the configurable flags last so they can override ours
+_CPPFLAGS += ${CPPFLAGS} ${EXTRA_CPPFLAGS}
+_CFLAGS += ${CFLAGS} ${EXTRA_CFLAGS}
+_LDFLAGS += ${LDFLAGS} ${EXTRA_LDFLAGS}
+# (except LDLIBS, as earlier libs override later ones)
+_LDLIBS := ${LDLIBS} ${EXTRA_LDLIBS} ${_LDLIBS}
+
+include build/exports.mk
+
+# Conditionally-supported flags
+AUTO_FLAGS := \
+ gen/flags/Wformat.mk \
+ gen/flags/Wimplicit-fallthrough.mk \
+ gen/flags/Wimplicit.mk \
+ gen/flags/Wmissing-decls.mk \
+ gen/flags/Wmissing-var-decls.mk \
+ gen/flags/Wshadow.mk \
+ gen/flags/Wsign-compare.mk \
+ gen/flags/Wstrict-prototypes.mk \
+ gen/flags/Wundef-prefix.mk \
+ gen/flags/bind-now.mk \
+ gen/flags/deps.mk \
+ gen/flags/pthread.mk
+
+gen/flags.mk: ${AUTO_FLAGS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf '_CPPFLAGS := %s\n' "$$XCPPFLAGS" >>$@
+ @printf '_CFLAGS := %s\n' "$$XCFLAGS" >>$@
+ @printf '_LDFLAGS := %s\n' "$$XLDFLAGS" >>$@
+ @printf '_LDLIBS := %s\n' "$$XLDLIBS" >>$@
+ @printf 'NOLIBS := %s\n' "$$XNOLIBS" >>$@
+ @test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
+ @cat $^ >>$@
+ @cat ${^:%=%.log} >gen/flags.log
+ ${VCAT} $@
+.PHONY: gen/flags.mk
+
+# Check that the C compiler works at all
+cc::
+ @build/cc.sh -q build/empty.c -o gen/.cc.out; \
+ ret=$$?; \
+ build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \
+ exit $$ret
+
+# The short name of the config test
+SLUG = ${@:gen/%.mk=%}
+# The source file to build
+CSRC = build/${SLUG}.c
+# The hidden output file name
+OUT = ${SLUG:flags/%=gen/flags/.%.out}
+
+${AUTO_FLAGS}: cc
+ @${MKDIR} ${@D}
+ @build/flags-if.sh ${CSRC} -o ${OUT} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
+.PHONY: ${AUTO_FLAGS}
diff --git a/build/flags/Wformat.c b/build/flags/Wformat.c
new file mode 100644
index 0000000..287b209
--- /dev/null
+++ b/build/flags/Wformat.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wformat=2
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wimplicit-fallthrough.c b/build/flags/Wimplicit-fallthrough.c
new file mode 100644
index 0000000..c32058d
--- /dev/null
+++ b/build/flags/Wimplicit-fallthrough.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wimplicit-fallthrough
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wimplicit.c b/build/flags/Wimplicit.c
new file mode 100644
index 0000000..3ea2b90
--- /dev/null
+++ b/build/flags/Wimplicit.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Werror=implicit
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wmissing-decls.c b/build/flags/Wmissing-decls.c
new file mode 100644
index 0000000..5ef3e96
--- /dev/null
+++ b/build/flags/Wmissing-decls.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wmissing-declarations
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wmissing-var-decls.c b/build/flags/Wmissing-var-decls.c
new file mode 100644
index 0000000..5c20cc6
--- /dev/null
+++ b/build/flags/Wmissing-var-decls.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wmissing-variable-declarations
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wshadow.c b/build/flags/Wshadow.c
new file mode 100644
index 0000000..28f6ef3
--- /dev/null
+++ b/build/flags/Wshadow.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wshadow
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wsign-compare.c b/build/flags/Wsign-compare.c
new file mode 100644
index 0000000..f083083
--- /dev/null
+++ b/build/flags/Wsign-compare.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wsign-compare
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wstrict-prototypes.c b/build/flags/Wstrict-prototypes.c
new file mode 100644
index 0000000..9614bee
--- /dev/null
+++ b/build/flags/Wstrict-prototypes.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -Wstrict-prototypes
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/Wundef-prefix.c b/build/flags/Wundef-prefix.c
new file mode 100644
index 0000000..3eaf82b
--- /dev/null
+++ b/build/flags/Wundef-prefix.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CPPFLAGS += -Wundef-prefix=BFS_
+/// -Werror
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/bind-now.c b/build/flags/bind-now.c
new file mode 100644
index 0000000..08bb4f2
--- /dev/null
+++ b/build/flags/bind-now.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _LDFLAGS += -Wl,-z,now
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/deps.c b/build/flags/deps.c
new file mode 100644
index 0000000..1c8c309
--- /dev/null
+++ b/build/flags/deps.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CPPFLAGS += -MD -MP
+
+int main(void) {
+ return 0;
+}
diff --git a/build/flags/pthread.c b/build/flags/pthread.c
new file mode 100644
index 0000000..db09aa4
--- /dev/null
+++ b/build/flags/pthread.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -pthread
+
+int main(void) {
+ return 0;
+}
diff --git a/build/has/--st-birthtim.c b/build/has/--st-birthtim.c
new file mode 100644
index 0000000..4da621f
--- /dev/null
+++ b/build/has/--st-birthtim.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.__st_birthtim.tv_sec;
+}
diff --git a/build/has/_Fork.c b/build/has/_Fork.c
new file mode 100644
index 0000000..4d7fbd3
--- /dev/null
+++ b/build/has/_Fork.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <unistd.h>
+
+int main(void) {
+ return _Fork();
+}
diff --git a/build/has/acl-get-entry.c b/build/has/acl-get-entry.c
new file mode 100644
index 0000000..1e7f473
--- /dev/null
+++ b/build/has/acl-get-entry.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ acl_entry_t entry;
+ return acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+}
diff --git a/build/has/acl-get-file.c b/build/has/acl-get-file.c
new file mode 100644
index 0000000..0b76ee2
--- /dev/null
+++ b/build/has/acl-get-file.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ return acl == (acl_t)NULL;
+}
diff --git a/build/has/acl-get-tag-type.c b/build/has/acl-get-tag-type.c
new file mode 100644
index 0000000..67b7d37
--- /dev/null
+++ b/build/has/acl-get-tag-type.c
@@ -0,0 +1,13 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_entry_t entry;
+ memset(&entry, 0, sizeof(entry));
+ acl_tag_t tag;
+ return acl_get_tag_type(entry, &tag);
+}
diff --git a/build/has/acl-is-trivial-np.c b/build/has/acl-is-trivial-np.c
new file mode 100644
index 0000000..9ca9fc7
--- /dev/null
+++ b/build/has/acl-is-trivial-np.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_fd(3);
+ int trivial;
+ acl_is_trivial_np(acl, &trivial);
+ return 0;
+}
diff --git a/build/has/acl-trivial.c b/build/has/acl-trivial.c
new file mode 100644
index 0000000..7efc838
--- /dev/null
+++ b/build/has/acl-trivial.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ return acl_trivial(".");
+}
diff --git a/build/has/builtin-riscv-pause.c b/build/has/builtin-riscv-pause.c
new file mode 100644
index 0000000..24b0675
--- /dev/null
+++ b/build/has/builtin-riscv-pause.c
@@ -0,0 +1,7 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+int main(void) {
+ __builtin_riscv_pause();
+ return 0;
+}
diff --git a/build/has/confstr.c b/build/has/confstr.c
new file mode 100644
index 0000000..58280b4
--- /dev/null
+++ b/build/has/confstr.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <unistd.h>
+
+int main(void) {
+ confstr(_CS_PATH, NULL, 0);
+ return 0;
+}
diff --git a/build/has/dprintf.c b/build/has/dprintf.c
new file mode 100644
index 0000000..c206fa3
--- /dev/null
+++ b/build/has/dprintf.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdio.h>
+
+int main(void) {
+ return dprintf(1, "%s\n", "Hello world!");
+}
diff --git a/build/has/extattr-get-file.c b/build/has/extattr-get-file.c
new file mode 100644
index 0000000..ac9cf96
--- /dev/null
+++ b/build/has/extattr-get-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_file("file", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-get-link.c b/build/has/extattr-get-link.c
new file mode 100644
index 0000000..c35be5b
--- /dev/null
+++ b/build/has/extattr-get-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_get_link("link", EXTATTR_NAMESPACE_USER, "xattr", NULL, 0);
+}
diff --git a/build/has/extattr-list-file.c b/build/has/extattr-list-file.c
new file mode 100644
index 0000000..e68a8bb
--- /dev/null
+++ b/build/has/extattr-list-file.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_file("file", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/build/has/extattr-list-link.c b/build/has/extattr-list-link.c
new file mode 100644
index 0000000..49f0ec2
--- /dev/null
+++ b/build/has/extattr-list-link.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+
+int main(void) {
+ return extattr_list_link("link", EXTATTR_NAMESPACE_USER, NULL, 0);
+}
diff --git a/build/has/fdclosedir.c b/build/has/fdclosedir.c
new file mode 100644
index 0000000..f4ad1f5
--- /dev/null
+++ b/build/has/fdclosedir.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ return fdclosedir(opendir("."));
+}
diff --git a/build/has/getdents.c b/build/has/getdents.c
new file mode 100644
index 0000000..579898f
--- /dev/null
+++ b/build/has/getdents.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return getdents(3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getdents64-syscall.c b/build/has/getdents64-syscall.c
new file mode 100644
index 0000000..7642d93
--- /dev/null
+++ b/build/has/getdents64-syscall.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+int main(void) {
+ char buf[1024];
+ return syscall(SYS_getdents64, 3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getdents64.c b/build/has/getdents64.c
new file mode 100644
index 0000000..d8e8062
--- /dev/null
+++ b/build/has/getdents64.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return getdents64(3, (void *)buf, sizeof(buf));
+}
diff --git a/build/has/getmntent-1.c b/build/has/getmntent-1.c
new file mode 100644
index 0000000..9854dcd
--- /dev/null
+++ b/build/has/getmntent-1.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <mntent.h>
+#include <stdio.h>
+
+int main(void) {
+ return !getmntent(stdin);
+}
diff --git a/build/has/getmntent-2.c b/build/has/getmntent-2.c
new file mode 100644
index 0000000..71f0220
--- /dev/null
+++ b/build/has/getmntent-2.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdio.h>
+#include <sys/mnttab.h>
+
+int main(void) {
+ struct mnttab mnt;
+ return getmntent(stdin, &mnt);
+}
diff --git a/build/has/getmntinfo.c b/build/has/getmntinfo.c
new file mode 100644
index 0000000..90ef5fb
--- /dev/null
+++ b/build/has/getmntinfo.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/mount.h>
+
+int main(void) {
+ return getmntinfo(NULL, MNT_WAIT);
+}
diff --git a/build/has/getprogname-gnu.c b/build/has/getprogname-gnu.c
new file mode 100644
index 0000000..6b97c5e
--- /dev/null
+++ b/build/has/getprogname-gnu.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+
+int main(void) {
+ const char *str = program_invocation_short_name;
+ return str[0];
+}
diff --git a/build/has/getprogname.c b/build/has/getprogname.c
new file mode 100644
index 0000000..83dc8e8
--- /dev/null
+++ b/build/has/getprogname.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+
+int main(void) {
+ const char *str = getprogname();
+ return str[0];
+}
diff --git a/build/has/io-uring-max-workers.c b/build/has/io-uring-max-workers.c
new file mode 100644
index 0000000..34ab5b7
--- /dev/null
+++ b/build/has/io-uring-max-workers.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <liburing.h>
+
+int main(void) {
+ struct io_uring ring;
+ io_uring_queue_init(1, &ring, 0);
+ unsigned int values[] = {0, 0};
+ return io_uring_register_iowq_max_workers(&ring, values);
+}
diff --git a/build/has/pipe2.c b/build/has/pipe2.c
new file mode 100644
index 0000000..4cb43b5
--- /dev/null
+++ b/build/has/pipe2.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <unistd.h>
+
+int main(void) {
+ int fds[2];
+ return pipe2(fds, O_CLOEXEC);
+}
diff --git a/build/has/posix-getdents.c b/build/has/posix-getdents.c
new file mode 100644
index 0000000..f74bbe5
--- /dev/null
+++ b/build/has/posix-getdents.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ char buf[1024];
+ return posix_getdents(3, (void *)buf, sizeof(buf), 0);
+}
diff --git a/build/has/posix-spawn-addfchdir-np.c b/build/has/posix-spawn-addfchdir-np.c
new file mode 100644
index 0000000..b870a53
--- /dev/null
+++ b/build/has/posix-spawn-addfchdir-np.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <spawn.h>
+
+int main(void) {
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_addfchdir_np(&actions, 3);
+ return 0;
+}
diff --git a/build/has/posix-spawn-addfchdir.c b/build/has/posix-spawn-addfchdir.c
new file mode 100644
index 0000000..c52ff81
--- /dev/null
+++ b/build/has/posix-spawn-addfchdir.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <spawn.h>
+
+int main(void) {
+ posix_spawn_file_actions_t actions;
+ posix_spawn_file_actions_init(&actions);
+ posix_spawn_file_actions_addfchdir(&actions, 3);
+ return 0;
+}
diff --git a/build/has/pragma-nounroll.c b/build/has/pragma-nounroll.c
new file mode 100644
index 0000000..2bdae14
--- /dev/null
+++ b/build/has/pragma-nounroll.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// -Werror
+
+int main(void) {
+#pragma nounroll
+ for (int i = 0; i < 100; ++i);
+ return 0;
+}
diff --git a/build/has/pthread-set-name-np.c b/build/has/pthread-set-name-np.c
new file mode 100644
index 0000000..324aab9
--- /dev/null
+++ b/build/has/pthread-set-name-np.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <pthread.h>
+#include <pthread_np.h>
+
+int main(void) {
+ pthread_set_name_np(pthread_self(), "name");
+ return 0;
+}
diff --git a/build/has/pthread-setname-np.c b/build/has/pthread-setname-np.c
new file mode 100644
index 0000000..a3b94c1
--- /dev/null
+++ b/build/has/pthread-setname-np.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <pthread.h>
+
+int main(void) {
+ return pthread_setname_np(pthread_self(), "name");
+}
diff --git a/build/has/sched-getaffinity.c b/build/has/sched-getaffinity.c
new file mode 100644
index 0000000..6f8fd98
--- /dev/null
+++ b/build/has/sched-getaffinity.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sched.h>
+
+int main(void) {
+ cpu_set_t set;
+ return sched_getaffinity(0, sizeof(set), &set);
+}
diff --git a/build/has/st-acmtim.c b/build/has/st-acmtim.c
new file mode 100644
index 0000000..d687ab0
--- /dev/null
+++ b/build/has/st-acmtim.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ unsigned int a = sb.st_atim.tv_sec;
+ unsigned int c = sb.st_ctim.tv_sec;
+ unsigned int m = sb.st_mtim.tv_sec;
+ return a + c + m;
+}
diff --git a/build/has/st-acmtimespec.c b/build/has/st-acmtimespec.c
new file mode 100644
index 0000000..f747bc0
--- /dev/null
+++ b/build/has/st-acmtimespec.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ unsigned int a = sb.st_atimespec.tv_sec;
+ unsigned int c = sb.st_ctimespec.tv_sec;
+ unsigned int m = sb.st_mtimespec.tv_sec;
+ return a + c + m;
+}
diff --git a/build/has/st-birthtim.c b/build/has/st-birthtim.c
new file mode 100644
index 0000000..4964571
--- /dev/null
+++ b/build/has/st-birthtim.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_birthtim.tv_sec;
+}
diff --git a/build/has/st-birthtimespec.c b/build/has/st-birthtimespec.c
new file mode 100644
index 0000000..91a613f
--- /dev/null
+++ b/build/has/st-birthtimespec.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_birthtimespec.tv_sec;
+}
diff --git a/build/has/st-flags.c b/build/has/st-flags.c
new file mode 100644
index 0000000..b1d0c32
--- /dev/null
+++ b/build/has/st-flags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/stat.h>
+
+int main(void) {
+ struct stat sb = {0};
+ return sb.st_flags;
+}
diff --git a/build/has/statx-syscall.c b/build/has/statx-syscall.c
new file mode 100644
index 0000000..87ec869
--- /dev/null
+++ b/build/has/statx-syscall.c
@@ -0,0 +1,13 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <linux/stat.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+int main(void) {
+ struct statx sb;
+ syscall(SYS_statx, AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb);
+ return 0;
+}
diff --git a/build/has/statx.c b/build/has/statx.c
new file mode 100644
index 0000000..65f1674
--- /dev/null
+++ b/build/has/statx.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+int main(void) {
+ struct statx sb;
+ statx(AT_FDCWD, ".", 0, STATX_BASIC_STATS, &sb);
+ return 0;
+}
diff --git a/build/has/strerror-l.c b/build/has/strerror-l.c
new file mode 100644
index 0000000..3dcc4d7
--- /dev/null
+++ b/build/has/strerror-l.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <locale.h>
+#include <string.h>
+
+int main(void) {
+ locale_t locale = duplocale(LC_GLOBAL_LOCALE);
+ return !strerror_l(ENOMEM, locale);
+}
diff --git a/build/has/strerror-r-gnu.c b/build/has/strerror-r-gnu.c
new file mode 100644
index 0000000..26ca0ee
--- /dev/null
+++ b/build/has/strerror-r-gnu.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <string.h>
+
+int main(void) {
+ char buf[256];
+ // Check that strerror_r() returns a pointer
+ return *strerror_r(ENOMEM, buf, sizeof(buf));
+}
diff --git a/build/has/strerror-r-posix.c b/build/has/strerror-r-posix.c
new file mode 100644
index 0000000..41b2d30
--- /dev/null
+++ b/build/has/strerror-r-posix.c
@@ -0,0 +1,11 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <errno.h>
+#include <string.h>
+
+int main(void) {
+ char buf[256];
+ // Check that strerror_r() returns an integer
+ return 2 * strerror_r(ENOMEM, buf, sizeof(buf));
+}
diff --git a/build/has/string-to-flags.c b/build/has/string-to-flags.c
new file mode 100644
index 0000000..027d72c
--- /dev/null
+++ b/build/has/string-to-flags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <util.h>
+
+int main(void) {
+ return string_to_flags(NULL, NULL, NULL);
+}
diff --git a/build/has/strtofflags.c b/build/has/strtofflags.c
new file mode 100644
index 0000000..73ecbcb
--- /dev/null
+++ b/build/has/strtofflags.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+#include <unistd.h>
+
+int main(void) {
+ return strtofflags(NULL, NULL, NULL);
+}
diff --git a/build/has/tcgetwinsize.c b/build/has/tcgetwinsize.c
new file mode 100644
index 0000000..d25d12b
--- /dev/null
+++ b/build/has/tcgetwinsize.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <termios.h>
+
+int main(void) {
+ struct winsize ws;
+ return tcgetwinsize(0, &ws);
+}
diff --git a/build/has/tcsetwinsize.c b/build/has/tcsetwinsize.c
new file mode 100644
index 0000000..6717415
--- /dev/null
+++ b/build/has/tcsetwinsize.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <termios.h>
+
+int main(void) {
+ const struct winsize ws = {0};
+ return tcsetwinsize(0, &ws);
+}
diff --git a/build/has/timegm.c b/build/has/timegm.c
new file mode 100644
index 0000000..6e2d155
--- /dev/null
+++ b/build/has/timegm.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ struct tm tm = {0};
+ return (int)timegm(&tm);
+}
diff --git a/build/has/timer-create.c b/build/has/timer-create.c
new file mode 100644
index 0000000..d5354c3
--- /dev/null
+++ b/build/has/timer-create.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ timer_t timer;
+ return timer_create(CLOCK_REALTIME, NULL, &timer);
+}
diff --git a/build/has/tm-gmtoff.c b/build/has/tm-gmtoff.c
new file mode 100644
index 0000000..543df48
--- /dev/null
+++ b/build/has/tm-gmtoff.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <time.h>
+
+int main(void) {
+ struct tm tm = {0};
+ return tm.tm_gmtoff;
+}
diff --git a/build/has/uselocale.c b/build/has/uselocale.c
new file mode 100644
index 0000000..a712ff8
--- /dev/null
+++ b/build/has/uselocale.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <locale.h>
+
+int main(void) {
+ locale_t locale = uselocale((locale_t)0);
+ return locale == LC_GLOBAL_LOCALE;
+}
diff --git a/build/header.mk b/build/header.mk
new file mode 100644
index 0000000..f15829a
--- /dev/null
+++ b/build/header.mk
@@ -0,0 +1,93 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/config.h
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+include gen/pkgs.mk
+include build/exports.mk
+
+# All header fragments we generate
+HEADERS := \
+ gen/has/--st-birthtim.h \
+ gen/has/_Fork.h \
+ gen/has/acl-get-entry.h \
+ gen/has/acl-get-file.h \
+ gen/has/acl-get-tag-type.h \
+ gen/has/acl-is-trivial-np.h \
+ gen/has/acl-trivial.h \
+ gen/has/builtin-riscv-pause.h \
+ gen/has/confstr.h \
+ gen/has/dprintf.h \
+ gen/has/extattr-get-file.h \
+ gen/has/extattr-get-link.h \
+ gen/has/extattr-list-file.h \
+ gen/has/extattr-list-link.h \
+ gen/has/fdclosedir.h \
+ gen/has/getdents.h \
+ gen/has/getdents64-syscall.h \
+ gen/has/getdents64.h \
+ gen/has/getmntent-1.h \
+ gen/has/getmntent-2.h \
+ gen/has/getmntinfo.h \
+ gen/has/getprogname-gnu.h \
+ gen/has/getprogname.h \
+ gen/has/io-uring-max-workers.h \
+ gen/has/pipe2.h \
+ gen/has/pragma-nounroll.h \
+ gen/has/posix-getdents.h \
+ gen/has/posix-spawn-addfchdir-np.h \
+ gen/has/posix-spawn-addfchdir.h \
+ gen/has/pthread-set-name-np.h \
+ gen/has/pthread-setname-np.h \
+ gen/has/sched-getaffinity.h \
+ gen/has/st-acmtim.h \
+ gen/has/st-acmtimespec.h \
+ gen/has/st-birthtim.h \
+ gen/has/st-birthtimespec.h \
+ gen/has/st-flags.h \
+ gen/has/statx-syscall.h \
+ gen/has/statx.h \
+ gen/has/strerror-l.h \
+ gen/has/strerror-r-gnu.h \
+ gen/has/strerror-r-posix.h \
+ gen/has/string-to-flags.h \
+ gen/has/strtofflags.h \
+ gen/has/tcgetwinsize.h \
+ gen/has/tcsetwinsize.h \
+ gen/has/timegm.h \
+ gen/has/timer-create.h \
+ gen/has/tm-gmtoff.h \
+ gen/has/uselocale.h
+
+# Previously generated by pkgs.mk
+PKG_HEADERS := ${ALL_PKGS:%=gen/with/%.h}
+
+gen/config.h: ${PKG_HEADERS} ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '// %s\n' "$@" >$@
+ @printf '#ifndef BFS_CONFIG_H\n' >>$@
+ @printf '#define BFS_CONFIG_H\n' >>$@
+ @cat $^ >>$@
+ @printf '#endif // BFS_CONFIG_H\n' >>$@
+ @cat gen/flags.log ${^:%=%.log} >gen/config.log
+ ${VCAT} $@
+ @printf '%s' "$$CONFFLAGS" | build/embed.sh >gen/confflags.i
+ @printf '%s' "$$XCC" | build/embed.sh >gen/cc.i
+ @printf '%s' "$$XCPPFLAGS" | build/embed.sh >gen/cppflags.i
+ @printf '%s' "$$XCFLAGS" | build/embed.sh >gen/cflags.i
+ @printf '%s' "$$XLDFLAGS" | build/embed.sh >gen/ldflags.i
+ @printf '%s' "$$XLDLIBS" | build/embed.sh >gen/ldlibs.i
+.PHONY: gen/config.h
+
+# The short name of the config test
+SLUG = ${@:gen/%.h=%}
+# The hidden output file name
+OUT = ${SLUG:has/%=gen/has/.%.out}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
diff --git a/build/msg-if.sh b/build/msg-if.sh
new file mode 100755
index 0000000..afb478c
--- /dev/null
+++ b/build/msg-if.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a success/failure indicator from a makefile:
+#
+# $ ./configure
+# [ CC ] with/liburing.c ✘
+# [ CC ] with/oniguruma.c ✔
+
+set -eu
+
+MSG="$1"
+shift
+
+if [ -z "${NO_COLOR:-}" ] && [ -t 1 ]; then
+ Y='\033[1;32m✔\033[0m'
+ N='\033[1;31m✘\033[0m'
+else
+ Y='✔'
+ N='✘'
+fi
+
+if "$@"; then
+ YN="$Y"
+else
+ YN="$N"
+fi
+
+build/msg.sh "$(printf "%-37s $YN" "$MSG")"
diff --git a/build/msg.sh b/build/msg.sh
new file mode 100755
index 0000000..2249125
--- /dev/null
+++ b/build/msg.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print a message from a makefile:
+#
+# $ make -s
+# $ make
+# [ CC ] src/main.c
+# $ make V=1
+# cc -Isrc -Igen -D...
+
+set -eu
+
+# Get the $MAKEFLAGS from the top-level make invocation
+MFLAGS="${XMAKEFLAGS-${MAKEFLAGS-}}"
+
+# Check if make should be quiet (make -s)
+is_quiet() {
+ # GNU make puts single-letter flags in the first word of $MAKEFLAGS,
+ # without a leading dash
+ case "${MFLAGS%% *}" in
+ -*) : ;;
+ *s*) return 0 ;;
+ esac
+
+ # BSD make puts each flag separately like -r -s -j 48
+ for flag in $MFLAGS; do
+ case "$flag" in
+ # Ignore things like --jobserver-auth
+ --*) continue ;;
+ # Skip variable assignments
+ *=*) break ;;
+ -*s*) return 0 ;;
+ esac
+ done
+
+ return 1
+}
+
+# Check if make should be loud (make V=1)
+is_loud() {
+ test "$XV"
+}
+
+MSG="$1"
+shift
+
+if ! is_quiet && ! is_loud; then
+ printf '%s\n' "$MSG"
+fi
+
+if [ $# -eq 0 ]; then
+ exit
+fi
+
+if is_loud; then
+ printf '%s\n' "$*"
+fi
+
+exec "$@"
diff --git a/build/pkgconf.sh b/build/pkgconf.sh
new file mode 100755
index 0000000..decf706
--- /dev/null
+++ b/build/pkgconf.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# pkg-config wrapper with hardcoded fallbacks
+
+set -eu
+
+MODE=
+case "${1:-}" in
+ --*)
+ MODE="$1"
+ shift
+esac
+
+if [ $# -lt 1 ]; then
+ exit
+fi
+
+case "$XNOLIBS" in
+ y|1)
+ exit 1
+esac
+
+if [ -z "$MODE" ]; then
+ # Check whether the libraries exist at all
+ for LIB; do
+ # Check ${WITH_$LIB}
+ WITH_LIB="WITH_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')"
+ eval "WITH=\"\${$WITH_LIB:-}\""
+ case "$WITH" in
+ y|1) continue ;;
+ n|0) exit 1 ;;
+ esac
+
+ XCFLAGS="$XCFLAGS $("$0" --cflags "$LIB")" || exit 1
+ XLDFLAGS="$XLDFLAGS $("$0" --ldflags "$LIB")" || exit 1
+ XLDLIBS="$("$0" --ldlibs "$LIB") $XLDLIBS" || exit 1
+ build/cc.sh "build/with/$LIB.c" -o "gen/with/.$LIB.out" || exit 1
+ done
+fi
+
+# Defer to pkg-config if possible
+if command -v "${XPKG_CONFIG:-}" >/dev/null 2>&1; then
+ case "$MODE" in
+ --cflags)
+ "$XPKG_CONFIG" --cflags "$@"
+ ;;
+ --ldflags)
+ "$XPKG_CONFIG" --libs-only-L --libs-only-other "$@"
+ ;;
+ --ldlibs)
+ "$XPKG_CONFIG" --libs-only-l "$@"
+ ;;
+ esac
+
+ exit
+fi
+
+# pkg-config unavailable, emulate it ourselves
+CFLAGS=""
+LDFLAGS=""
+LDLIBS=""
+
+for LIB; do
+ case "$LIB" in
+ libacl)
+ LDLIB=-lacl
+ ;;
+ libcap)
+ LDLIB=-lcap
+ ;;
+ libselinux)
+ LDLIB=-lselinux
+ ;;
+ liburing)
+ LDLIB=-luring
+ ;;
+ oniguruma)
+ LDLIB=-lonig
+ ;;
+ *)
+ printf 'error: Unknown package %s\n' "$LIB" >&2
+ exit 1
+ ;;
+ esac
+
+ LDLIBS="$LDLIBS$LDLIB "
+done
+
+case "$MODE" in
+ --ldlibs)
+ printf '%s\n' "$LDLIBS"
+ ;;
+esac
diff --git a/build/pkgs.mk b/build/pkgs.mk
new file mode 100644
index 0000000..f692739
--- /dev/null
+++ b/build/pkgs.mk
@@ -0,0 +1,33 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/pkgs.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/flags.mk
+include build/exports.mk
+
+HEADERS := ${ALL_PKGS:%=gen/with/%.h}
+
+gen/pkgs.mk: ${HEADERS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @gen() { \
+ printf 'PKGS := %s\n' "$$*"; \
+ printf '_CFLAGS += %s\n' "$$(build/pkgconf.sh --cflags "$$@")"; \
+ printf '_LDFLAGS += %s\n' "$$(build/pkgconf.sh --ldflags "$$@")"; \
+ printf '_LDLIBS := %s $${_LDLIBS}\n' "$$(build/pkgconf.sh --ldlibs "$$@")"; \
+ }; \
+ gen $$(grep -l ' true$$' $^ | sed 's|.*/\(.*\)\.h|\1|') >>$@
+ ${VCAT} $@
+
+.PHONY: gen/pkgs.mk
+
+# Convert gen/with/foo.h to foo
+PKG = ${@:gen/with/%.h=%}
+
+${HEADERS}::
+ @${MKDIR} ${@D}
+ @build/define-if.sh with/${PKG} build/pkgconf.sh ${PKG} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] with/${PKG}.c" test $$? -eq 0;
diff --git a/build/prelude.mk b/build/prelude.mk
new file mode 100644
index 0000000..6250d73
--- /dev/null
+++ b/build/prelude.mk
@@ -0,0 +1,68 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Common makefile utilities. Compatible with both GNU make and most BSD makes.
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# We don't use any suffix rules
+.SUFFIXES:
+
+# GNU make has $^ for the full list of targets, while BSD make has $> (and the
+# long-form ${.ALLSRC}). We use the GNU version, bringing it to BSD like this:
+^ ?= $>
+
+# Installation paths
+DESTDIR ?=
+PREFIX ?= /usr
+MANDIR ?= ${PREFIX}/share/man
+
+# Configurable executables
+CC ?= cc
+INSTALL ?= install
+MKDIR ?= mkdir -p
+PKG_CONFIG ?= pkg-config
+RM ?= rm -f
+
+# GNU and BSD make have incompatible syntax for conditionals, but we can do a
+# lot with just nested variable expansion. We use "y" as the canonical
+# truthy value, and "" (the empty string) as the canonical falsey value.
+#
+# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this:
+#
+# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y
+# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y
+# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty]
+# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty]
+# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [empty]
+#
+# Inspired by https://github.com/wahern/autoguess
+TRUTHY,y := y
+TRUTHY,1 := y
+
+# Boolean operators are also implemented with nested expansion
+NOT, := y
+
+# Normalize ${V} to either "y" or ""
+export XV=${TRUTHY,${V}}
+
+# Suppress output unless V=1
+Q, := @
+Q := ${Q,${XV}}
+
+# Show full commands with `make V=1`, otherwise short summaries
+MSG = @build/msg.sh
+
+# cat a file if V=1
+VCAT,y := @cat
+VCAT, := @:
+VCAT := ${VCAT,${XV}}
+
+# All external dependencies
+ALL_PKGS := \
+ libacl \
+ libcap \
+ libselinux \
+ liburing \
+ oniguruma
diff --git a/build/version.sh b/build/version.sh
new file mode 100755
index 0000000..ec0663a
--- /dev/null
+++ b/build/version.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Print the version number
+
+set -eu
+
+DIR="$(dirname -- "$0")/.."
+
+if [ "${VERSION-}" ]; then
+ printf '%s' "$VERSION"
+elif [ -e "$DIR/.git" ] && command -v git >/dev/null 2>&1; then
+ git -C "$DIR" describe --always --dirty
+else
+ echo "4.0.8"
+fi
diff --git a/build/with/libacl.c b/build/with/libacl.c
new file mode 100644
index 0000000..de1fe50
--- /dev/null
+++ b/build/with/libacl.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ acl_free(0);
+ return 0;
+}
diff --git a/build/with/libcap.c b/build/with/libcap.c
new file mode 100644
index 0000000..58e832c
--- /dev/null
+++ b/build/with/libcap.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/capability.h>
+
+int main(void) {
+ cap_free(0);
+ return 0;
+}
diff --git a/build/with/libselinux.c b/build/with/libselinux.c
new file mode 100644
index 0000000..bca409d
--- /dev/null
+++ b/build/with/libselinux.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <selinux/selinux.h>
+
+int main(void) {
+ freecon(0);
+ return 0;
+}
diff --git a/build/with/liburing.c b/build/with/liburing.c
new file mode 100644
index 0000000..bea499a
--- /dev/null
+++ b/build/with/liburing.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <liburing.h>
+
+int main(void) {
+ io_uring_free_probe(0);
+ return 0;
+}
diff --git a/build/with/oniguruma.c b/build/with/oniguruma.c
new file mode 100644
index 0000000..cb17596
--- /dev/null
+++ b/build/with/oniguruma.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <oniguruma.h>
+
+int main(void) {
+ onig_free(0);
+ return 0;
+}
diff --git a/color.c b/color.c
deleted file mode 100644
index 509e646..0000000
--- a/color.c
+++ /dev/null
@@ -1,1121 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "color.h"
-#include "bftw.h"
-#include "dir.h"
-#include "dstring.h"
-#include "expr.h"
-#include "fsade.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include <unistd.h>
-
-/**
- * The parsed form of LS_COLORS.
- */
-struct colors {
- char *reset;
- char *leftcode;
- char *rightcode;
- char *endcode;
- char *clear_to_eol;
-
- char *bold;
- char *gray;
- char *red;
- char *green;
- char *yellow;
- char *blue;
- char *magenta;
- char *cyan;
- char *white;
-
- char *warning;
- char *error;
-
- char *normal;
-
- char *file;
- char *multi_hard;
- char *executable;
- char *capable;
- char *setgid;
- char *setuid;
-
- char *directory;
- char *sticky;
- char *other_writable;
- char *sticky_other_writable;
-
- char *link;
- char *orphan;
- char *missing;
- bool link_as_target;
-
- char *blockdev;
- char *chardev;
- char *door;
- char *pipe;
- char *socket;
-
- /** A mapping from color names (fi, di, ln, etc.) to struct fields. */
- struct trie names;
-
- /** A mapping from file extensions to colors. */
- struct trie ext_colors;
-};
-
-/** Initialize a color in the table. */
-static int init_color(struct colors *colors, const char *name, const char *value, char **field) {
- if (value) {
- *field = dstrdup(value);
- if (!*field) {
- return -1;
- }
- } else {
- *field = NULL;
- }
-
- struct trie_leaf *leaf = trie_insert_str(&colors->names, name);
- if (leaf) {
- leaf->value = field;
- return 0;
- } else {
- return -1;
- }
-}
-
-/** Get a color from the table. */
-static char **get_color(const struct colors *colors, const char *name) {
- const struct trie_leaf *leaf = trie_find_str(&colors->names, name);
- if (leaf) {
- return (char **)leaf->value;
- } else {
- return NULL;
- }
-}
-
-/** Set the value of a color. */
-static int set_color(struct colors *colors, const char *name, char *value) {
- char **color = get_color(colors, name);
- if (color) {
- dstrfree(*color);
- *color = value;
- return 0;
- } else {
- return -1;
- }
-}
-
-/**
- * Transform a file extension for fast lookups, by reversing and lowercasing it.
- */
-static void extxfrm(char *ext) {
- size_t len = strlen(ext);
- for (size_t i = 0; i < len - i; ++i) {
- char a = ext[i];
- char b = ext[len - i - 1];
-
- // What's internationalization? Doesn't matter, this is what
- // GNU ls does. Luckily, since there's no standard C way to
- // casefold. Not using tolower() here since it respects the
- // current locale, which GNU ls doesn't do.
- if (a >= 'A' && a <= 'Z') {
- a += 'a' - 'A';
- }
- if (b >= 'A' && b <= 'Z') {
- b += 'a' - 'A';
- }
-
- ext[i] = b;
- ext[len - i - 1] = a;
- }
-}
-
-/**
- * Set the color for an extension.
- */
-static int set_ext_color(struct colors *colors, char *key, const char *value) {
- extxfrm(key);
-
- // A later *.x should override any earlier *.x, *.y.x, etc.
- struct trie_leaf *match;
- while ((match = trie_find_postfix(&colors->ext_colors, key))) {
- dstrfree(match->value);
- trie_remove(&colors->ext_colors, match);
- }
-
- struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key);
- if (leaf) {
- leaf->value = (char *)value;
- return 0;
- } else {
- return -1;
- }
-}
-
-/**
- * Find a color by an extension.
- */
-static const char *get_ext_color(const struct colors *colors, const char *filename) {
- char *xfrm = strdup(filename);
- if (!xfrm) {
- return NULL;
- }
- extxfrm(xfrm);
-
- const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm);
- free(xfrm);
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
-}
-
-/**
- * Parse a chunk of LS_COLORS that may have escape sequences. The supported
- * escapes are:
- *
- * \a, \b, \f, \n, \r, \t, \v:
- * As in C
- * \e:
- * ESC (\033)
- * \?:
- * DEL (\177)
- * \_:
- * ' ' (space)
- * \NNN:
- * Octal
- * \xNN:
- * Hex
- * ^C:
- * Control character.
- *
- * See man dir_colors.
- *
- * @param value
- * The value to parse.
- * @param end
- * The character that marks the end of the chunk.
- * @param[out] next
- * Will be set to the next chunk.
- * @return
- * The parsed chunk as a dstring.
- */
-static char *unescape(const char *value, char end, const char **next) {
- if (!value) {
- goto fail;
- }
-
- char *str = dstralloc(0);
- if (!str) {
- goto fail_str;
- }
-
- const char *i;
- for (i = value; *i && *i != end; ++i) {
- unsigned char c = 0;
-
- switch (*i) {
- case '\\':
- switch (*++i) {
- case 'a':
- c = '\a';
- break;
- case 'b':
- c = '\b';
- break;
- case 'e':
- c = '\033';
- break;
- case 'f':
- c = '\f';
- break;
- case 'n':
- c = '\n';
- break;
- case 'r':
- c = '\r';
- break;
- case 't':
- c = '\t';
- break;
- case 'v':
- c = '\v';
- break;
- case '?':
- c = '\177';
- break;
- case '_':
- c = ' ';
- break;
-
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- while (i[1] >= '0' && i[1] <= '7') {
- c <<= 3;
- c |= *i++ - '0';
- }
- c <<= 3;
- c |= *i - '0';
- break;
-
- case 'X':
- case 'x':
- while (true) {
- if (i[1] >= '0' && i[1] <= '9') {
- c <<= 4;
- c |= i[1] - '0';
- } else if (i[1] >= 'A' && i[1] <= 'F') {
- c <<= 4;
- c |= i[1] - 'A' + 0xA;
- } else if (i[1] >= 'a' && i[1] <= 'f') {
- c <<= 4;
- c |= i[1] - 'a' + 0xA;
- } else {
- break;
- }
- ++i;
- }
- break;
-
- case '\0':
- goto fail_str;
-
- default:
- c = *i;
- break;
- }
- break;
-
- case '^':
- switch (*++i) {
- case '?':
- c = '\177';
- break;
- case '\0':
- goto fail_str;
- default:
- // CTRL masks bits 6 and 7
- c = *i & 0x1F;
- break;
- }
- break;
-
- default:
- c = *i;
- break;
- }
-
- if (dstrapp(&str, c) != 0) {
- goto fail_str;
- }
- }
-
- if (*i) {
- *next = i + 1;
- } else {
- *next = NULL;
- }
-
- return str;
-
-fail_str:
- dstrfree(str);
-fail:
- *next = NULL;
- return NULL;
-}
-
-struct colors *parse_colors(const char *ls_colors) {
- struct colors *colors = malloc(sizeof(struct colors));
- if (!colors) {
- return NULL;
- }
-
- trie_init(&colors->names);
- trie_init(&colors->ext_colors);
-
- int ret = 0;
-
- // From man console_codes
-
- ret |= init_color(colors, "rs", "0", &colors->reset);
- ret |= init_color(colors, "lc", "\033[", &colors->leftcode);
- ret |= init_color(colors, "rc", "m", &colors->rightcode);
- ret |= init_color(colors, "ec", NULL, &colors->endcode);
- ret |= init_color(colors, "cl", "\033[K", &colors->clear_to_eol);
-
- ret |= init_color(colors, "bld", "01;39", &colors->bold);
- ret |= init_color(colors, "gry", "01;30", &colors->gray);
- ret |= init_color(colors, "red", "01;31", &colors->red);
- ret |= init_color(colors, "grn", "01;32", &colors->green);
- ret |= init_color(colors, "ylw", "01;33", &colors->yellow);
- ret |= init_color(colors, "blu", "01;34", &colors->blue);
- ret |= init_color(colors, "mag", "01;35", &colors->magenta);
- ret |= init_color(colors, "cyn", "01;36", &colors->cyan);
- ret |= init_color(colors, "wht", "01;37", &colors->white);
-
- ret |= init_color(colors, "wr", "01;33", &colors->warning);
- ret |= init_color(colors, "er", "01;31", &colors->error);
-
- // Defaults from man dir_colors
-
- ret |= init_color(colors, "no", NULL, &colors->normal);
-
- ret |= init_color(colors, "fi", NULL, &colors->file);
- ret |= init_color(colors, "mh", NULL, &colors->multi_hard);
- ret |= init_color(colors, "ex", "01;32", &colors->executable);
- ret |= init_color(colors, "ca", "30;41", &colors->capable);
- ret |= init_color(colors, "sg", "30;43", &colors->setgid);
- ret |= init_color(colors, "su", "37;41", &colors->setuid);
-
- ret |= init_color(colors, "di", "01;34", &colors->directory);
- ret |= init_color(colors, "st", "37;44", &colors->sticky);
- ret |= init_color(colors, "ow", "34;42", &colors->other_writable);
- ret |= init_color(colors, "tw", "30;42", &colors->sticky_other_writable);
-
- ret |= init_color(colors, "ln", "01;36", &colors->link);
- ret |= init_color(colors, "or", NULL, &colors->orphan);
- ret |= init_color(colors, "mi", NULL, &colors->missing);
- colors->link_as_target = false;
-
- ret |= init_color(colors, "bd", "01;33", &colors->blockdev);
- ret |= init_color(colors, "cd", "01;33", &colors->chardev);
- ret |= init_color(colors, "do", "01;35", &colors->door);
- ret |= init_color(colors, "pi", "33", &colors->pipe);
- ret |= init_color(colors, "so", "01;35", &colors->socket);
-
- if (ret) {
- free_colors(colors);
- return NULL;
- }
-
- for (const char *chunk = ls_colors, *next; chunk; chunk = next) {
- if (chunk[0] == '*') {
- char *key = unescape(chunk + 1, '=', &next);
- if (!key) {
- continue;
- }
-
- char *value = unescape(next, ':', &next);
- if (value) {
- if (set_ext_color(colors, key, value) != 0) {
- dstrfree(value);
- }
- }
-
- dstrfree(key);
- } else {
- const char *equals = strchr(chunk, '=');
- if (!equals) {
- break;
- }
-
- char *value = unescape(equals + 1, ':', &next);
- if (!value) {
- continue;
- }
-
- char *key = strndup(chunk, equals - chunk);
- if (!key) {
- dstrfree(value);
- continue;
- }
-
- // All-zero values should be treated like NULL, to fall
- // back on any other relevant coloring for that file
- if (strspn(value, "0") == strlen(value)
- && strcmp(key, "rs") != 0
- && strcmp(key, "lc") != 0
- && strcmp(key, "rc") != 0
- && strcmp(key, "ec") != 0) {
- dstrfree(value);
- value = NULL;
- }
-
- if (set_color(colors, key, value) != 0) {
- dstrfree(value);
- }
- free(key);
- }
- }
-
- if (colors->link && strcmp(colors->link, "target") == 0) {
- colors->link_as_target = true;
- dstrfree(colors->link);
- colors->link = NULL;
- }
-
- return colors;
-}
-
-void free_colors(struct colors *colors) {
- if (colors) {
- struct trie_leaf *leaf;
- while ((leaf = trie_first_leaf(&colors->ext_colors))) {
- dstrfree(leaf->value);
- trie_remove(&colors->ext_colors, leaf);
- }
- trie_destroy(&colors->ext_colors);
-
- while ((leaf = trie_first_leaf(&colors->names))) {
- char **field = leaf->value;
- dstrfree(*field);
- trie_remove(&colors->names, leaf);
- }
- trie_destroy(&colors->names);
-
- free(colors);
- }
-}
-
-static CFILE *cfalloc(void) {
- CFILE *cfile = malloc(sizeof(*cfile));
- if (!cfile) {
- return NULL;
- }
-
- cfile->buffer = dstralloc(128);
- if (!cfile->buffer) {
- free(cfile);
- return NULL;
- }
-
- cfile->file = NULL;
- cfile->colors = NULL;
- cfile->close = false;
-
- return cfile;
-}
-
-CFILE *cfopen(const char *path, const struct colors *colors) {
- CFILE *cfile = cfalloc();
- if (!cfile) {
- return NULL;
- }
-
- cfile->file = fopen(path, "wb");
- if (!cfile->file) {
- cfclose(cfile);
- return NULL;
- }
- cfile->close = true;
-
- if (isatty(fileno(cfile->file))) {
- cfile->colors = colors;
- } else {
- cfile->colors = NULL;
- }
-
- return cfile;
-}
-
-CFILE *cfdup(FILE *file, const struct colors *colors) {
- CFILE *cfile = cfalloc();
- if (!cfile) {
- return NULL;
- }
-
- cfile->close = false;
- cfile->file = file;
-
- if (isatty(fileno(file))) {
- cfile->colors = colors;
- } else {
- cfile->colors = NULL;
- }
-
- return cfile;
-}
-
-int cfclose(CFILE *cfile) {
- int ret = 0;
-
- if (cfile) {
- dstrfree(cfile->buffer);
-
- if (cfile->close) {
- ret = fclose(cfile->file);
- }
-
- free(cfile);
- }
-
- return ret;
-}
-
-/** Check if a symlink is broken. */
-static bool is_link_broken(const struct BFTW *ftwbuf) {
- if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) {
- return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0;
- } else {
- return true;
- }
-}
-
-/** Get the color for a file. */
-static const char *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- enum bfs_type type = bftw_type(ftwbuf, flags);
- if (type == BFS_ERROR) {
- goto error;
- }
-
- const struct bfs_stat *statbuf = NULL;
- const char *color = NULL;
-
- switch (type) {
- case BFS_REG:
- if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) {
- statbuf = bftw_stat(ftwbuf, flags);
- if (!statbuf) {
- goto error;
- }
- }
-
- if (colors->setuid && (statbuf->mode & 04000)) {
- color = colors->setuid;
- } else if (colors->setgid && (statbuf->mode & 02000)) {
- color = colors->setgid;
- } else if (colors->capable && bfs_check_capabilities(ftwbuf) > 0) {
- color = colors->capable;
- } else if (colors->executable && (statbuf->mode & 00111)) {
- color = colors->executable;
- } else if (colors->multi_hard && statbuf->nlink > 1) {
- color = colors->multi_hard;
- }
-
- if (!color) {
- color = get_ext_color(colors, filename);
- }
-
- if (!color) {
- color = colors->file;
- }
-
- break;
-
- case BFS_DIR:
- if (colors->sticky_other_writable || colors->other_writable || colors->sticky) {
- statbuf = bftw_stat(ftwbuf, flags);
- if (!statbuf) {
- goto error;
- }
- }
-
- if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) {
- color = colors->sticky_other_writable;
- } else if (colors->other_writable && (statbuf->mode & 00002)) {
- color = colors->other_writable;
- } else if (colors->sticky && (statbuf->mode & 01000)) {
- color = colors->sticky;
- } else {
- color = colors->directory;
- }
-
- break;
-
- case BFS_LNK:
- if (colors->orphan && is_link_broken(ftwbuf)) {
- color = colors->orphan;
- } else {
- color = colors->link;
- }
- break;
-
- case BFS_BLK:
- color = colors->blockdev;
- break;
- case BFS_CHR:
- color = colors->chardev;
- break;
- case BFS_FIFO:
- color = colors->pipe;
- break;
- case BFS_SOCK:
- color = colors->socket;
- break;
- case BFS_DOOR:
- color = colors->door;
- break;
-
- default:
- break;
- }
-
- if (!color) {
- color = colors->normal;
- }
-
- return color;
-
-error:
- if (colors->missing) {
- return colors->missing;
- } else {
- return colors->orphan;
- }
-}
-
-/** Print an ANSI escape sequence. */
-static int print_esc(CFILE *cfile, const char *esc) {
- const struct colors *colors = cfile->colors;
-
- if (dstrdcat(&cfile->buffer, colors->leftcode) != 0) {
- return -1;
- }
- if (dstrdcat(&cfile->buffer, esc) != 0) {
- return -1;
- }
- if (dstrdcat(&cfile->buffer, colors->rightcode) != 0) {
- return -1;
- }
-
- return 0;
-}
-
-/** Reset after an ANSI escape sequence. */
-static int print_reset(CFILE *cfile) {
- const struct colors *colors = cfile->colors;
-
- if (colors->endcode) {
- return dstrdcat(&cfile->buffer, colors->endcode);
- } else {
- return print_esc(cfile, colors->reset);
- }
-}
-
-/** Print a string with an optional color. */
-static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t len) {
- if (esc) {
- if (print_esc(cfile, esc) != 0) {
- return -1;
- }
- }
- if (dstrncat(&cfile->buffer, str, len) != 0) {
- return -1;
- }
- if (esc) {
- if (print_reset(cfile) != 0) {
- return -1;
- }
- }
-
- return 0;
-}
-
-/** Find the offset of the first broken path component. */
-static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) {
- ssize_t ret = max;
-
- if (bftw_type(ftwbuf, flags) != BFS_ERROR) {
- goto out;
- }
-
- char *at_path;
- int at_fd;
- if (path == ftwbuf->path) {
- if (ftwbuf->depth == 0) {
- at_fd = AT_FDCWD;
- at_path = dstrndup(path, max);
- } else {
- // The parent must have existed to get here
- goto out;
- }
- } else {
- // We're in print_link_target(), so resolve relative to the link's parent directory
- at_fd = ftwbuf->at_fd;
- if (at_fd == AT_FDCWD && path[0] != '/') {
- at_path = dstrndup(ftwbuf->path, ftwbuf->nameoff);
- if (at_path && dstrncat(&at_path, path, max) != 0) {
- ret = -1;
- goto out_path;
- }
- } else {
- at_path = dstrndup(path, max);
- }
- }
-
- if (!at_path) {
- ret = -1;
- goto out;
- }
-
- while (ret > 0) {
- if (xfaccessat(at_fd, at_path, F_OK) == 0) {
- break;
- }
-
- size_t len = dstrlen(at_path);
- while (ret && at_path[len - 1] == '/') {
- --len, --ret;
- }
- while (ret && at_path[len - 1] != '/') {
- --len, --ret;
- }
-
- dstresize(&at_path, len);
- }
-
-out_path:
- dstrfree(at_path);
-out:
- return ret;
-}
-
-/** Print the directories leading up to a file. */
-static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t nameoff) {
- const struct colors *colors = cfile->colors;
-
- ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff);
- if (broken < 0) {
- return -1;
- }
-
- if (broken > 0) {
- if (print_colored(cfile, colors->directory, path, broken) != 0) {
- return -1;
- }
- }
-
- if ((size_t)broken < nameoff) {
- const char *color = colors->missing;
- if (!color) {
- color = colors->orphan;
- }
- if (print_colored(cfile, color, path + broken, nameoff - broken) != 0) {
- return -1;
- }
- }
-
- return 0;
-}
-
-/** Print a path with colors. */
-static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- size_t nameoff;
- if (path == ftwbuf->path) {
- nameoff = ftwbuf->nameoff;
- } else {
- nameoff = xbasename(path) - path;
- }
-
- print_dirs_colored(cfile, path, ftwbuf, flags, nameoff);
-
- const char *filename = path + nameoff;
- const char *color = file_color(cfile->colors, filename, ftwbuf, flags);
- return print_colored(cfile, color, filename, strlen(filename));
-}
-
-/** Print the path to a file with the appropriate colors. */
-static int print_path(CFILE *cfile, const struct BFTW *ftwbuf) {
- const struct colors *colors = cfile->colors;
- if (!colors) {
- return dstrcat(&cfile->buffer, ftwbuf->path);
- }
-
- enum bfs_stat_flags flags = ftwbuf->stat_flags;
- if (colors && colors->link_as_target && ftwbuf->type == BFS_LNK) {
- flags = BFS_STAT_TRYFOLLOW;
- }
-
- return print_path_colored(cfile, ftwbuf->path, ftwbuf, flags);
-}
-
-/** Print a link target with the appropriate colors. */
-static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
- size_t len = statbuf ? statbuf->size : 0;
-
- char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
- if (!target) {
- return -1;
- }
-
- int ret;
- if (cfile->colors) {
- ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW);
- } else {
- ret = dstrcat(&cfile->buffer, target);
- }
-
- free(target);
- return ret;
-}
-
-/** Format some colored output to the buffer. */
-BFS_FORMATTER(2, 3)
-static int cbuff(CFILE *cfile, const char *format, ...);
-
-/** Dump a parsed expression tree, for debugging. */
-static int print_expr(CFILE *cfile, const struct expr *expr, bool verbose) {
- if (dstrcat(&cfile->buffer, "(") != 0) {
- return -1;
- }
-
- if (expr->lhs || expr->rhs) {
- if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) {
- return -1;
- }
- } else {
- if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) {
- return -1;
- }
- }
-
- for (size_t i = 1; i < expr->argc; ++i) {
- if (cbuff(cfile, " ${bld}%s${rs}", expr->argv[i]) < 0) {
- return -1;
- }
- }
-
- if (verbose) {
- double rate = 0.0, time = 0.0;
- if (expr->evaluations) {
- rate = 100.0*expr->successes/expr->evaluations;
- time = (1.0e9*expr->elapsed.tv_sec + expr->elapsed.tv_nsec)/expr->evaluations;
- }
- if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]",
- expr->successes, expr->evaluations, rate, time)) {
- return -1;
- }
- }
-
- if (expr->lhs) {
- if (dstrcat(&cfile->buffer, " ") != 0) {
- return -1;
- }
- if (print_expr(cfile, expr->lhs, verbose) != 0) {
- return -1;
- }
- }
-
- if (expr->rhs) {
- if (dstrcat(&cfile->buffer, " ") != 0) {
- return -1;
- }
- if (print_expr(cfile, expr->rhs, verbose) != 0) {
- return -1;
- }
- }
-
- if (dstrcat(&cfile->buffer, ")") != 0) {
- return -1;
- }
-
- return 0;
-}
-
-static int cvbuff(CFILE *cfile, const char *format, va_list args) {
- const struct colors *colors = cfile->colors;
- int error = errno;
-
- for (const char *i = format; *i; ++i) {
- size_t verbatim = strcspn(i, "%$");
- if (dstrncat(&cfile->buffer, i, verbatim) != 0) {
- return -1;
- }
-
- i += verbatim;
- switch (*i) {
- case '%':
- switch (*++i) {
- case '%':
- if (dstrapp(&cfile->buffer, '%') != 0) {
- return -1;
- }
- break;
-
- case 'c':
- if (dstrapp(&cfile->buffer, va_arg(args, int)) != 0) {
- return -1;
- }
- break;
-
- case 'd':
- if (dstrcatf(&cfile->buffer, "%d", va_arg(args, int)) != 0) {
- return -1;
- }
- break;
-
- case 'g':
- if (dstrcatf(&cfile->buffer, "%g", va_arg(args, double)) != 0) {
- return -1;
- }
- break;
-
- case 's':
- if (dstrcat(&cfile->buffer, va_arg(args, const char *)) != 0) {
- return -1;
- }
- break;
-
- case 'z':
- ++i;
- if (*i != 'u') {
- goto invalid;
- }
- if (dstrcatf(&cfile->buffer, "%zu", va_arg(args, size_t)) != 0) {
- return -1;
- }
- break;
-
- case 'm':
- if (dstrcat(&cfile->buffer, strerror(error)) != 0) {
- return -1;
- }
- break;
-
- case 'p':
- switch (*++i) {
- case 'P':
- if (print_path(cfile, va_arg(args, const struct BFTW *)) != 0) {
- return -1;
- }
- break;
-
- case 'L':
- if (print_link_target(cfile, va_arg(args, const struct BFTW *)) != 0) {
- return -1;
- }
- break;
-
- case 'e':
- if (print_expr(cfile, va_arg(args, const struct expr *), false) != 0) {
- return -1;
- }
- break;
- case 'E':
- if (print_expr(cfile, va_arg(args, const struct expr *), true) != 0) {
- return -1;
- }
- break;
-
- default:
- goto invalid;
- }
-
- break;
-
- default:
- goto invalid;
- }
- break;
-
- case '$':
- switch (*++i) {
- case '$':
- if (dstrapp(&cfile->buffer, '$') != 0) {
- return -1;
- }
- break;
-
- case '{': {
- ++i;
- const char *end = strchr(i, '}');
- if (!end) {
- goto invalid;
- }
- if (!colors) {
- i = end;
- break;
- }
-
- size_t len = end - i;
- char name[len + 1];
- memcpy(name, i, len);
- name[len] = '\0';
-
- char **esc = get_color(colors, name);
- if (!esc) {
- goto invalid;
- }
- if (*esc) {
- if (print_esc(cfile, *esc) != 0) {
- return -1;
- }
- }
-
- i = end;
- break;
- }
-
- default:
- goto invalid;
- }
- break;
-
- default:
- return 0;
- }
- }
-
- return 0;
-
-invalid:
- assert(!"Invalid format string");
- errno = EINVAL;
- return -1;
-}
-
-static int cbuff(CFILE *cfile, const char *format, ...) {
- va_list args;
- va_start(args, format);
- int ret = cvbuff(cfile, format, args);
- va_end(args);
- return ret;
-}
-
-int cvfprintf(CFILE *cfile, const char *format, va_list args) {
- assert(dstrlen(cfile->buffer) == 0);
-
- int ret = -1;
- if (cvbuff(cfile, format, args) == 0) {
- size_t len = dstrlen(cfile->buffer);
- if (fwrite(cfile->buffer, 1, len, cfile->file) == len) {
- ret = 0;
- }
- }
-
- dstresize(&cfile->buffer, 0);
- return ret;
-}
-
-int cfprintf(CFILE *cfile, const char *format, ...) {
- va_list args;
- va_start(args, format);
- int ret = cvfprintf(cfile, format, args);
- va_end(args);
- return ret;
-}
diff --git a/color.h b/color.h
deleted file mode 100644
index 55e89ff..0000000
--- a/color.h
+++ /dev/null
@@ -1,127 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Utilities for colored output on ANSI terminals.
- */
-
-#ifndef BFS_COLOR_H
-#define BFS_COLOR_H
-
-#include "util.h"
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-
-/**
- * A lookup table for colors.
- */
-struct colors;
-
-/**
- * Parse a color table.
- *
- * @param ls_colors
- * A color table in the LS_COLORS environment variable format.
- * @return The parsed color table.
- */
-struct colors *parse_colors(const char *ls_colors);
-
-/**
- * Free a color table.
- *
- * @param colors
- * The color table to free.
- */
-void free_colors(struct colors *colors);
-
-/**
- * A file/stream with associated colors.
- */
-typedef struct CFILE {
- /** The underlying file/stream. */
- FILE *file;
- /** The color table to use, if any. */
- const struct colors *colors;
- /** A buffer for colored formatting. */
- char *buffer;
- /** Whether to close the underlying stream. */
- bool close;
-} CFILE;
-
-/**
- * Open a file for colored output.
- *
- * @param path
- * The path to the file to open.
- * @param colors
- * The color table to use if file is a TTY.
- * @return A colored file stream.
- */
-CFILE *cfopen(const char *path, const struct colors *colors);
-
-/**
- * Make a colored copy of an open file.
- *
- * @param file
- * The underlying file.
- * @param colors
- * The color table to use if file is a TTY.
- * @return A colored wrapper around file.
- */
-CFILE *cfdup(FILE *file, const struct colors *colors);
-
-/**
- * Close a colored file.
- *
- * @param cfile
- * The colored file to close.
- * @return 0 on success, -1 on failure.
- */
-int cfclose(CFILE *cfile);
-
-/**
- * Colored, formatted output.
- *
- * @param cfile
- * The colored stream to print to.
- * @param format
- * A printf()-style format string, supporting these format specifiers:
- *
- * %c: A single character
- * %d: An integer
- * %g: A double
- * %s: A string
- * %zu: A size_t
- * %m: strerror(errno)
- * %pP: A colored file path, from a const struct BFTW * argument
- * %pL: A colored link target, from a const struct BFTW * argument
- * %pe: Dump a const struct expr *, for debugging.
- * %pE: Dump a const struct expr * in verbose form, for debugging.
- * %%: A literal '%'
- * ${cc}: Change the color to 'cc'
- * $$: A literal '$'
- * @return 0 on success, -1 on failure.
- */
-BFS_FORMATTER(2, 3)
-int cfprintf(CFILE *cfile, const char *format, ...);
-
-/**
- * cfprintf() variant that takes a va_list.
- */
-int cvfprintf(CFILE *cfile, const char *format, va_list args);
-
-#endif // BFS_COLOR_H
diff --git a/completions/bfs.bash b/completions/bfs.bash
index f734ab1..0dd39f4 100644
--- a/completions/bfs.bash
+++ b/completions/bfs.bash
@@ -1,21 +1,8 @@
-# bash completion script for bfs
+# Copyright © Benjamin Mundt <benMundt@ibm.com>
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
-############################################################################
-# bfs #
-# Copyright (C) 2020 Benjamin Mundt <benMundt@ibm.com> #
-# Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# bash completion script for bfs
_bfs() {
local cur prev words cword
@@ -31,6 +18,7 @@ _bfs() {
-fstype
-gid
-group
+ -j
-ok
-okdir
-regextype
@@ -44,12 +32,14 @@ _bfs() {
# (e.g. because they are numeric, glob, regexp, time, etc.)
local nocomp=(
-{a,B,c,m}{min,since,time}
+ -context
-ilname
-iname
-inum
-ipath
-iregex
-iwholename
+ -limit
-links
-lname
-maxdepth
@@ -107,10 +97,9 @@ _bfs() {
-depth
-follow
-ignore_readdir_race
- -maxdepth
- -mindepth
-mount
-nocolor
+ -noerror
-noignore_readdir_race
-noleaf
-nowarn
diff --git a/completions/bfs.fish b/completions/bfs.fish
new file mode 100644
index 0000000..7182bee
--- /dev/null
+++ b/completions/bfs.fish
@@ -0,0 +1,148 @@
+# Completions for the 'bfs' command
+
+set -l debug_flag_comp 'help\t"Print help message" cost\t"Show cost estimates" exec\t"Print executed command details" opt\t"Print optimization details" rates\t"Print predicate success rates" search\t"Trace the filesystem traversal" stat\t"Trace all stat() calls" tree\t"Print the parse tree" all\t"All debug flags at once"'
+set -l optimization_comp '0\t"Disable all optimizations" 1\t"Basic logical simplifications" 2\t"-O1, plus dead code elimination and data flow analysis" 3\t"-02, plus re-order expressions to reduce expected cost" 4\t"All optimizations, including aggressive optimizations" fast\t"Same as -O4"'
+set -l strategy_comp 'bfs\t"Breadth-first search" dfs\t"Depth-first search" ids\t"Iterative deepening search" eds\t"Exponential deepening search"'
+set -l regex_type_comp 'help\t"Print help message" posix-basic\t"POSIX basic regular expressions" posix-extended\t"POSIX extended regular expressions" ed\t"Like ed" emacs\t"Like emacs" grep\t"Like grep" sed\t"Like sed"'
+set -l type_comp 'b\t"Block device" c\t"Character device" d\t"Directory" l\t"Symbolic link" p\t"Pipe" f\t"Regular file" s\t"Socket" w\t"Whiteout" D\t"Door"'
+
+# Flags
+
+complete -c bfs -o H -d "Follow symbolic links only on the command line"
+complete -c bfs -o L -o follow -d "Follow all symbolic links"
+complete -c bfs -o P -d "Never follow symbolic links"
+complete -c bfs -o E -d "Use extended regular expressions"
+complete -c bfs -o X -d "Filter out files with non-xargs(1)-safe names"
+complete -c bfs -o d -o depth -d "Search in post-order"
+complete -c bfs -o s -d "Visit directory entries in sorted order"
+complete -c bfs -o x -o xdev -d "Don't descend into other mount points"
+complete -c bfs -o f -d "Treat specified path as a path to search" -a "(__fish_complete_directories)" -x
+complete -c bfs -o D -d "Turn on a debugging flag" -a $debug_flag_comp -x
+complete -c bfs -s O -d "Enable specified optimization level" -a $optimization_comp -x
+complete -c bfs -o S -d "Choose the search strategy" -a $strategy_comp -x
+complete -c bfs -s j -d "Use this many threads" -x
+
+# Operators
+
+complete -c bfs -o not -d "Negate result of expression"
+complete -c bfs -o a -o and -d "Result is only true if both previous and next expression are true"
+complete -c bfs -o o -o or -d "Result is true if either previous or next expression are true"
+
+# Special forms
+
+complete -c bfs -o exclude -d "Exclude all paths matching the expression from the search" -x
+
+# Options
+
+complete -c bfs -o color -d "Turn colors on"
+complete -c bfs -o nocolor -d "Turn colors off"
+complete -c bfs -o daystart -d "Measure time relative to the start of today"
+complete -c bfs -o files0-from -d "Treat the NUL-separated paths in specified file as starting points for the search" -F
+complete -c bfs -o ignore_readdir_race -d "Don't report an error if the file tree is modified during the search"
+complete -c bfs -o noignore_readdir_race -d "Report an error if the file tree is modified during the search"
+complete -c bfs -o maxdepth -d "Ignore files deeper than specified number" -x
+complete -c bfs -o mindepth -d "Ignore files shallower than specified number" -x
+complete -c bfs -o mount -d "Exclude mount points"
+complete -c bfs -o noerror -d "Ignore any errors that occur during traversal"
+complete -c bfs -o nohidden -d "Exclude hidden files and directories"
+complete -c bfs -o noleaf -d "Ignored; for compatibility with GNU find"
+complete -c bfs -o regextype -d "Use specified flavored regex" -a $regex_type_comp -x
+complete -c bfs -o status -d "Display a status bar while searching"
+complete -c bfs -o unique -d "Skip any files that have already been seen"
+complete -c bfs -o warn -d "Turn on warnings about the command line"
+complete -c bfs -o nowarn -d "Turn off warnings about the command line"
+
+# Tests
+
+complete -c bfs -o acl -d "Find files with a non-trivial Access Control List"
+complete -c bfs -o amin -d "Find files accessed specified number of minutes ago" -x
+complete -c bfs -o Bmin -d "Find files birthed specified number of minutes ago" -x
+complete -c bfs -o cmin -d "Find files changed specified number of minutes ago" -x
+complete -c bfs -o mmin -d "Find files modified specified number of minutes ago" -x
+complete -c bfs -o anewer -d "Find files accessed more recently than specified file was modified" -F
+complete -c bfs -o Bnewer -d "Find files birthed more recently than specified file was modified" -F
+complete -c bfs -o cnewer -d "Find files changed more recently than specified file was modified" -F
+complete -c bfs -o mnewer -d "Find files modified more recently than specified file was modified" -F
+complete -c bfs -o asince -d "Find files accessed more recently than specified time" -x
+complete -c bfs -o Bsince -d "Find files birthed more recently than specified time" -x
+complete -c bfs -o csince -d "Find files changed more recently than specified time" -x
+complete -c bfs -o msince -d "Find files modified more recently than specified time" -x
+complete -c bfs -o atime -d "Find files accessed specified number of days ago" -x
+complete -c bfs -o Btime -d "Find files birthed specified number of days ago" -x
+complete -c bfs -o ctime -d "Find files changed specified number of days ago" -x
+complete -c bfs -o mtime -d "Find files modified specified number of days ago" -x
+complete -c bfs -o capable -d "Find files with capabilities set"
+complete -c bfs -o context -d "Find files by SELinux context" -x
+complete -c bfs -o depth -d "Find files with specified number of depth" -x
+complete -c bfs -o empty -d "Find empty files/directories"
+complete -c bfs -o executable -d "Find files the current user can execute"
+complete -c bfs -o readable -d "Find files the current user can read"
+complete -c bfs -o writable -d "Find files the current user can write"
+complete -c bfs -o false -d "Always false"
+complete -c bfs -o true -d "Always true"
+complete -c bfs -o fstype -d "Find files on file systems with the given type" -a "(__fish_print_filesystems)" -x
+complete -c bfs -o gid -d "Find files owned by group ID" -a "(__fish_complete_group_ids)" -x
+complete -c bfs -o uid -d "Find files owned by user ID" -a "(__fish_complete_user_ids)" -x
+complete -c bfs -o group -d "Find files owned by the group" -a "(__fish_complete_groups)" -x
+complete -c bfs -o user -d "Find files owned by the user" -a "(__fish_complete_users)" -x
+complete -c bfs -o hidden -d "Find hidden files"
+complete -c bfs -o ilname -d "Case-insensitive versions of -lname" -x
+complete -c bfs -o iname -d "Case-insensitive versions of -name" -x
+complete -c bfs -o ipath -d "Case-insensitive versions of -path" -x
+complete -c bfs -o iregex -d "Case-insensitive versions of -regex" -x
+complete -c bfs -o iwholename -d "Case-insensitive versions of -wholename" -x
+complete -c bfs -o inum -d "Find files with specified inode number" -x
+complete -c bfs -o links -d "Find files with specified number of hard links" -x
+complete -c bfs -o lname -d "Find symbolic links whose target matches specified glob" -x
+complete -c bfs -o name -d "Find files whose name matches specified glob" -x
+complete -c bfs -o newer -d "Find files newer than specified file" -F
+
+# handle -newer{a,B,c,m}{a,B,c,m} FILE
+for x in {a,B,c,m}
+ for y in {a,B,c,m}
+ complete -c bfs -o newer$x$y -d "Find files whose $x""time is newer than the $y""time of specified file" -F
+ end
+end
+
+# handle -newer{a,B,c,m}t TIMESTAMP
+for x in {a,B,c,m}
+ complete -c bfs -o newer$x"t" -d "Find files whose $x""time is newer than specified timestamp" -x
+end
+
+complete -c bfs -o nogroup -d "Find files owned by nonexistent groups"
+complete -c bfs -o nouser -d "Find files owned by nonexistent users"
+complete -c bfs -o path -o wholename -d "Find files whose entire path matches specified glob" -x
+complete -c bfs -o perm -d "Find files with a matching mode" -x
+complete -c bfs -o regex -d "Find files whose entire path matches the regular expression" -x
+complete -c bfs -o samefile -d "Find hard links to specified file" -F
+complete -c bfs -o since -d "Find files modified since specified time" -x
+complete -c bfs -o size -d "Find files with the given size" -x
+complete -c bfs -o sparse -d "Find files that occupy fewer disk blocks than expected"
+complete -c bfs -o type -d "Find files of the given type" -a $type_comp -x
+complete -c bfs -o used -d "Find files last accessed specified number of days after they were changed" -x
+complete -c bfs -o xattr -d "Find files with extended attributes"
+complete -c bfs -o xattrname -d "Find files with the specified extended attribute name" -x
+complete -c bfs -o xtype -d "Find files of the given type, following links when -type would not, and vice versa" -a $type_comp -x
+
+# Actions
+
+complete -c bfs -o rm -o delete -d "Delete any found files"
+complete -c bfs -o exec -d "Execute a command" -r
+complete -c bfs -o ok -d "Prompt the user whether to execute a command" -r
+complete -c bfs -o execdir -d "Like -exec, but run the command in the same directory as the found file(s)" -r
+complete -c bfs -o okdir -d "Like -ok, but run the command in the same directory as the found file(s)" -r
+complete -c bfs -o exit -d "Exit immediately with the given status" -x
+complete -c bfs -o fls -d "Like -ls, but write to specified file" -F
+complete -c bfs -o fprint -d "Like -print, but write to specified file" -F
+complete -c bfs -o fprint0 -d "Like -print0, but write to specified file" -F
+complete -c bfs -o fprintf -d "Like -printf, but write to specified file" -F
+complete -c bfs -o limit -d "Limit the number of results" -x
+complete -c bfs -o ls -d "List files like ls -dils"
+complete -c bfs -o print -d "Print the path to the found file"
+complete -c bfs -o print0 -d "Like -print, but use the null character as a separator rather than newlines"
+complete -c bfs -o printf -d "Print according to a format string" -x
+complete -c bfs -o printx -d "Like -print, but escape whitespace and quotation characters"
+complete -c bfs -o prune -d "Don't descend into this directory"
+complete -c bfs -o quit -d "Quit immediately"
+complete -c bfs -o version -l version -d "Print version information"
+complete -c bfs -o help -l help -d "Print usage information"
diff --git a/completions/bfs.zsh b/completions/bfs.zsh
new file mode 100644
index 0000000..6b46f83
--- /dev/null
+++ b/completions/bfs.zsh
@@ -0,0 +1,176 @@
+#compdef bfs
+# Based on standard zsh find completion and bfs bash completion.
+
+local curcontext="$curcontext" state_descr variant default ret=1
+local -a state line args alts disp smatch
+
+args=(
+ # Flags
+ '(-depth)-d[search in post-order (descendents first)]'
+ '-D[print diagnostics]:debug option:(cost exec opt rates search stat time tree all help)'
+ '-E[use extended regular expressions with -regex/-iregex]'
+ '-f[specify file hierarchy to traverse]:path:_directories'
+ '-O+[enable query optimisation]:level:(0 1 2 3 4 fast)'
+ '-s[traverse directories in sorted order]'
+ '-X[warn if filename contains characters special to xargs]'
+ "-x[don't span filesystems]"
+ '(-H -L)-P[never follow symlinks]'
+ '(-H -P)-L[follow symlinks]'
+ '(-L -P)-H[only follow symlinks when resolving command-line arguments]'
+ "-S[select search method]:value:(bfs dfs ids eds)"
+ '-f[treat path as path to search]:path:_files -/'
+ '-j+[use this many threads]:threads:'
+
+ # Operators
+ '*-and'
+ '*-not'
+ '*-or'
+ '*-a' '*-o'
+
+ # Special forms
+ '*-exclude[exclude paths matching EXPRESSION from search]'
+
+ # Options
+ '(-nocolor)-color[turn on colors]'
+ '(-color)-nocolor[turn off colors]'
+ '*-daystart[measure times relative to start of today]'
+ '(-d)*-depth[search in post-order (descendents first)]'
+ '-files0-from[search NUL separated paths from FILE]:file:_files'
+ '*-follow[follow all symbolic links (same as -L)]'
+ '*-ignore_readdir_race[report an error if bfs detects file tree is modified during search]'
+ '*-noignore_readdir_race[do not report an error if bfs detects file tree is modified during search]'
+ '*-maxdepth[ignore files deeper than N]:maximum search depth'
+ '*-mindepth[ignore files shallower than N]:minimum search depth'
+ "*-mount[exclude mount points]"
+ '*-noerror[ignore any errors that occur during traversal]'
+ '*-nohidden[exclude hidden files]'
+ '*-noleaf[ignored, for compatibility with GNU find]'
+ '-regextype[type of regex to use, default posix-basic]:regexp syntax:(help posix-basic posix-extended ed emacs grep sed)'
+ '*-status[display a status bar while searching]'
+ '-unique[skip any files that have already been seen]'
+ '*-warn[turn on warnings about the command line]'
+ '*-nowarn[turn off warnings about the command line]'
+ "*-xdev[don't descend into other mount points]"
+
+ # Tests
+ '*-acl[find files with a non-trivial Access Control List]'
+ '*-amin[find files accessed N minutes ago]:access time (minutes):'
+ '*-anewer[find files accessed more recently than FILE was modified]:file to compare (access time):_files'
+ '*-asince[find files accessed more recently than TIME]:time:'
+ '*-atime[find files accessed N days ago]:access time (days):->times'
+
+ '*-Bmin[find files birthed N minutes ago]:birth time (minutes):'
+ '*-Bnewer[find files birthed more recently than FILE was modified]:file to compare (birth time):_files'
+ '*-Bsince[find files birthed more recently than TIME]:time:'
+ '*-Btime[find files birthed N days ago]:birth time (days):->times'
+
+ '*-cmin[find files changed N minutes ago]:inode change time (minutes):'
+ '*-cnewer[find files changed more recently than FILE was modified]:file to compare (inode change time):_files'
+ '*-csince[find files changed more recently than TIME]:time:'
+ '*-ctime[find files changed N days ago]:inode change time (days):->times'
+
+ '*-mmin[find files modified N minutes ago]:modification time (minutes):'
+ '*-mnewer[find files modified more recently than FILE was modified]:file to compare (modification time):_files'
+ '*-msince[find files modified more recently than TIME]:time:'
+ '*-mtime[find files modified N days ago]:modification time (days):->times'
+
+ '*-capable[find files with POSIX.1e capabilities set]'
+ '*-context[find files by SELinux context]:pattern'
+ # -depth without parameters exist above. I don't know how to handle this gracefully
+ '*-empty[find empty files/directories]'
+ '*-executable[find files the current user can execute]'
+ '*-readable[find files the current user can read]'
+ '*-writable[find files the current user can write]'
+ '*-false[always false]'
+ '*-true[always true]'
+ '*-fstype[find files on file systems with the given type]:file system type:_file_systems'
+
+ '*-gid[find files owned by group ID N]:numeric group ID:'
+ '*-group[find files owned by group NAME]:group:_groups'
+ '*-uid[find files owned by user ID N]:numeric user ID'
+ '*-user[find files owned by user NAME]:user:_users'
+ '*-hidden[find hidden files (those beginning with .)]'
+
+ '*-ilname[find symbolic links whose target matches GLOB (case insensitive)]:link pattern to search (case insensitive):'
+ '*-iname[find files whose name matches GLOB (case insensitive)]:name pattern to match (case insensitive):'
+ '*-inum[find files with inode number N]:inode number:'
+ '*-ipath[find files whose entire path matches GLOB (case insensitive)]:path pattern to search (case insensitive):'
+ '*-iregex[find files whose entire path matches REGEX (case insensitive)]:regular expression to search (case insensitive):'
+ '*-iwholename[find files whose entire path matches GLOB (case insensitive)]:full path pattern to search (case insensitive):'
+
+ '*-links[find files with N hard links]:number of links:'
+ '*-lname[find symbolic links whose target matches GLOB]:link pattern to search'
+ '*-name[find files whose name matches GLOB]:name pattern'
+ '*-newer[find files newer than FILE]:file to compare (modification time):_files'
+ '*-newer'{a,B,c,m}{a,B,c,m}'[find files where timestamp 1 is newer than timestamp 2 of reference FILE]:reference file:_files'
+ '*-newer'{a,B,c,m}t'[find files where timestamp is newer than timestamp given as parameter]:timestamp:'
+ '*-nogroup[find files with nonexistent owning group]'
+ '*-nouser[find files with nonexistent owning user]'
+ '*-path[find files whose entire path matches GLOB]:path pattern to search:'
+ '*-wholename[find files whose entire path matches GLOB]:full path pattern to search:'
+
+ '*-perm[find files with a matching mode]: :_file_modes'
+ '*-regex[find files whose entire path matches REGEX]:regular expression to search:'
+ '*-samefile[find hard links to FILE]:file to compare inode:_files'
+ '*-since[files modified since TIME]:time:'
+ '*-size[find files with the given size]:file size (blocks):'
+ '*-sparse[find files that occupy fewer disk blocks than expected]'
+ '*-type[find files of the given type]:file type:((b\:block\ device c\:character\ device d\:directory p\:named\ pipe f\:normal\ file l\:symbolic\ link s\:socket w\:whiteout D\:Door))'
+ '*-used[find files last accessed N days after they were changed]:access after inode change (days)'
+ '*-xattr[find files with extended attributes]'
+ '*-xattrname[find files with extended attribute NAME]:name:'
+ '*-xtype[find files of the given type following links when -type would not, and vice versa]:file type:((b\:block\ device c\:character\ device d\:directory p\:named\ pipe f\:normal\ file l\:symbolic\ link s\:socket w\:whiteout D\:Door))'
+
+ # Actions
+ '*-delete[delete any found files (-implies -depth)]'
+ '*-rm[delete any found files (-implies -depth)]'
+
+ '*-exec[execute a command]:program: _command_names -e:*(\;|+)::program arguments: _normal'
+ '*-execdir[execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal'
+ '*-ok[prompt the user whether to execute a command]:program: _command_names -e:*(\;|+)::program arguments: _normal'
+ '*-okdir[prompt the user whether to execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal'
+
+ '-exit[exit with status if found, default 0]'
+ '*-fls[list files like ls -dils, but write to FILE instead of standard output]:output file:_files'
+ '*-fprint[print the path to the found file, but write to FILE instead of standard output]:output file:_files'
+ '*-fprint0[print the path to the found file using null character as separator, but write to FILE instead of standard output]:output file:_files'
+ '*-fprintf[print according to format string, but write to FILE instead of standard output]:output file:_files:output format'
+
+ '*-limit[quit after N results]:maximum result count'
+ '*-ls[list files like ls -dils]'
+ '*-print[print the path to the found file]'
+ '*-print0[print the path to the found file using null character as separator]'
+ '*-printf[print according to format string]:output format'
+ '*-printx[like -print but escapes whitespace and quotation marks]'
+ "*-prune[don't descend into this directory]"
+
+ '*-quit[quit immediately]'
+ '(- *)-help[print usage information]'
+ '(-)--help[print usage information]'
+ '(- *)-version[print version information]'
+ '(-)--version[print version information]'
+
+ '(--help --version)*:other:{_alternative "directories:directory:_files -/" "logic:logic:(, ! \( \) )"}'
+)
+
+_arguments -C $args && ret=0
+
+if [[ $state = times ]]; then
+ if ! compset -P '[+-]' || [[ -prefix '[0-9]' ]]; then
+ compstate[list]+=' packed'
+ if zstyle -t ":completion:${curcontext}:senses" verbose; then
+ zstyle -s ":completion:${curcontext}:senses" list-separator sep || sep=--
+ default=" [default exactly]"
+ disp=( "- $sep before" "+ $sep since" )
+ smatch=( - + )
+ else
+ disp=( before exactly since )
+ smatch=( - '' + )
+ fi
+ alts=( "senses:sense${default}:compadd -V times -S '' -d disp -a smatch" )
+ fi
+ alts+=( "times:${state_descr}:_dates -f d" )
+ _alternative $alts && ret=0
+fi
+
+return ret
diff --git a/configure b/configure
new file mode 100755
index 0000000..7f0bd04
--- /dev/null
+++ b/configure
@@ -0,0 +1,237 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# bfs build configuration script
+
+set -eu
+
+# Get the relative path to the source tree based on how the script was run
+DIR=$(dirname -- "$0")
+
+# Print the help message
+help() {
+ cat <<EOF
+Usage:
+
+ \$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
+ \$ $MAKE -j$(_nproc)
+
+Variables set in the environment or on the command line will be picked up:
+
+ MAKE
+ The make implementation to use
+ CC
+ The C compiler to use
+
+ CPPFLAGS="-I... -D..."
+ CFLAGS="-W... -f..."
+ LDFLAGS="-L... -Wl,..."
+ Preprocessor/compiler/linker flags
+
+ LDLIBS="-l... -l..."
+ Dynamic libraries to link
+
+ EXTRA_{CPPFLAGS,CFLAGS,LDFLAGS,LDLIBS}
+ Adds to the default flags, instead of replacing them
+
+The default flags result in a plain debug build. Other build profiles include:
+
+ --enable-release
+ Enable optimizations, disable assertions
+ --enable-{asan,lsan,msan,tsan,ubsan}
+ Enable sanitizers
+ --enable-gcov
+ Enable code coverage instrumentation
+
+External dependencies are auto-detected by default, but you can build --with or
+--without them explicitly:
+
+ --with-libacl --without-libacl
+ --with-libcap --without-libcap
+ --with-libselinux --without-libselinux
+ --with-liburing --without-liburing
+ --with-oniguruma --without-oniguruma
+
+Packaging:
+
+ --prefix=/path
+ Set the installation prefix (default: /usr)
+ --mandir=/path
+ Set the man page directory (default: \$PREFIX/share/man)
+ --version=X.Y.Z
+ Set the version string (default: $("$DIR/build/version.sh"))
+
+This script is a thin wrapper around a makefile-based configuration system.
+Any other arguments will be passed directly to the $MAKE invocation, e.g.
+
+ \$ $0 -j$(_nproc) V=1
+EOF
+}
+
+# Report a warning
+warn() {
+ fmt="$1"
+ shift
+ printf "%s: warning: $fmt\\n" "$0" "$@" >&2
+}
+
+# Report an argument parsing error
+invalid() {
+ printf '%s: error: Unrecognized option "%s"\n\n' "$0" "$1" >&2
+ printf 'Run %s --help for more information.\n' "$0" >&2
+ exit 1
+}
+
+# Get the number of cores to use
+_nproc() {
+ {
+ nproc \
+ || sysctl -n hw.ncpu \
+ || getconf _NPROCESSORS_ONLN \
+ || echo 1
+ } 2>/dev/null
+}
+
+# Save the ./configure command line for bfs --version
+export CONFFLAGS=""
+
+# Default to `make`
+MAKE="${MAKE-make}"
+
+# Parse the command-line arguments
+for arg; do
+ shift
+
+ # Only add --options to CONFFLAGS, so we don't print FLAG=values twice in bfs --version
+ case "$arg" in
+ -*)
+ CONFFLAGS="${CONFFLAGS}${CONFFLAGS:+ }${arg}"
+ ;;
+ esac
+
+ # --[(enable|disable|with|without)-]$name[=$value]
+ value="${arg#*=}"
+ name="${arg%%=*}"
+ name="${name#--}"
+ case "$arg" in
+ --enable-*|--disable-*|--with-*|--without-*)
+ name="${name#*-}"
+ ;;
+ esac
+ NAME=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_')
+
+ # y/n modality
+ case "$arg" in
+ --enable-*|--with-*)
+ case "$arg" in
+ *=y|*=yes) yn=y ;;
+ *=n|*=no) yn=n ;;
+ *=*) invalid "$arg" ;;
+ *) yn=y ;;
+ esac
+ ;;
+ --disable-*|--without-*)
+ case "$arg" in
+ *=*) invalid "arg" ;;
+ *) yn=n ;;
+ esac
+ ;;
+ esac
+
+ # Fix up --enable-lib* to --with-lib*
+ case "$arg" in
+ --enable-*|--disable-*)
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ old="$arg"
+ case "$arg" in
+ --enable-*) arg="--with-${arg#--*-}" ;;
+ --disable-*) arg="--without-${arg#--*-}" ;;
+ esac
+ warn 'Treating "%s" like "%s"' "$old" "$arg"
+ ;;
+ esac
+ ;;
+ esac
+
+ case "$arg" in
+ -h|--help)
+ help
+ exit 0
+ ;;
+
+ --enable-*|--disable-*)
+ case "$name" in
+ release|lto|asan|lsan|msan|tsan|ubsan|lint|gcov)
+ set -- "$@" "$NAME=$yn"
+ ;;
+ *)
+ invalid "$arg"
+ ;;
+ esac
+ ;;
+
+ --with-*|--without-*)
+ case "$name" in
+ libacl|libcap|libselinux|liburing|oniguruma)
+ set -- "$@" "WITH_$NAME=$yn"
+ ;;
+ *)
+ invalid "$arg"
+ ;;
+ esac
+ ;;
+
+ --prefix=*|--mandir=*|--version=*)
+ set -- "$@" "$NAME=$value"
+ ;;
+
+ --infodir=*|--build=*|--host=*|--target=*)
+ warn 'Ignoring option "%s"' "$arg"
+ ;;
+
+ MAKE=*)
+ MAKE="$value"
+ ;;
+
+ # Warn about MAKE variables that have documented configure flags
+ RELEASE=*|LTO=*|ASAN=*|LSAN=*|MSAN=*|TSAN=*|UBSAN=*|LINT=*|GCOV=*)
+ name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-')
+ warn '"%s" is deprecated; use --enable-%s' "$arg" "$name"
+ set -- "$@" "$arg"
+ ;;
+
+ PREFIX=*|MANDIR=*|VERSION=*)
+ name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-')
+ warn '"%s" is deprecated; use --%s=%s' "$arg" "$name" "$value"
+ set -- "$@" "$arg"
+ ;;
+
+ WITH_*=*)
+ name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-')
+ warn '"%s" is deprecated; use --%s' "$arg" "$name"
+ set -- "$@" "$arg"
+ ;;
+
+ # make flag (-j2) or variable (CC=clang)
+ -*|*=*)
+ set -- "$@" "$arg"
+ ;;
+
+ *)
+ invalid "$arg"
+ ;;
+ esac
+done
+
+# Set up symbolic links for out-of-tree builds
+for f in Makefile bench build completions docs src tests; do
+ test -e "$f" || ln -s "$DIR/$f" "$f"
+done
+
+# Set MAKEFLAGS to -j$(_nproc) if it's unset
+export MAKEFLAGS="${MAKEFLAGS--j$(_nproc)}"
+
+$MAKE -rf build/config.mk "$@"
diff --git a/ctx.c b/ctx.c
deleted file mode 100644
index 450d87e..0000000
--- a/ctx.c
+++ /dev/null
@@ -1,284 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "ctx.h"
-#include "color.h"
-#include "darray.h"
-#include "diag.h"
-#include "expr.h"
-#include "mtab.h"
-#include "pwcache.h"
-#include "stat.h"
-#include "trie.h"
-#include <assert.h>
-#include <errno.h>
-#include <limits.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-const char *debug_flag_name(enum debug_flags flag) {
- switch (flag) {
- case DEBUG_COST:
- return "cost";
- case DEBUG_EXEC:
- return "exec";
- case DEBUG_OPT:
- return "opt";
- case DEBUG_RATES:
- return "rates";
- case DEBUG_SEARCH:
- return "search";
- case DEBUG_STAT:
- return "stat";
- case DEBUG_TREE:
- return "tree";
-
- case DEBUG_ALL:
- break;
- }
-
- assert(!"Unrecognized debug flag");
- return "???";
-}
-
-struct bfs_ctx *bfs_ctx_new(void) {
- struct bfs_ctx *ctx = malloc(sizeof(*ctx));
- if (!ctx) {
- return NULL;
- }
-
- ctx->argv = NULL;
- ctx->paths = NULL;
- ctx->expr = NULL;
- ctx->exclude = NULL;
-
- ctx->mindepth = 0;
- ctx->maxdepth = INT_MAX;
- ctx->flags = BFTW_RECOVER;
- ctx->strategy = BFTW_BFS;
- ctx->optlevel = 3;
- ctx->debug = 0;
- ctx->ignore_races = false;
- ctx->posixly_correct = false;
- ctx->status = false;
- ctx->unique = false;
- ctx->warn = false;
- ctx->xargs_safe = false;
-
- ctx->colors = NULL;
- ctx->colors_error = 0;
- ctx->cout = NULL;
- ctx->cerr = NULL;
-
- ctx->users = NULL;
- ctx->users_error = 0;
- ctx->groups = NULL;
- ctx->groups_error = 0;
-
- ctx->mtab = NULL;
- ctx->mtab_error = 0;
-
- trie_init(&ctx->files);
- ctx->nfiles = 0;
-
- struct rlimit rl;
- if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
- ctx->nofile_soft = rl.rlim_cur;
- ctx->nofile_hard = rl.rlim_max;
- } else {
- ctx->nofile_soft = 1024;
- ctx->nofile_hard = RLIM_INFINITY;
- }
-
- return ctx;
-}
-
-const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx) {
- struct bfs_ctx *mut = (struct bfs_ctx *)ctx;
-
- if (mut->users_error) {
- errno = mut->users_error;
- } else if (!mut->users) {
- mut->users = bfs_users_parse();
- if (!mut->users) {
- mut->users_error = errno;
- }
- }
-
- return mut->users;
-}
-
-const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx) {
- struct bfs_ctx *mut = (struct bfs_ctx *)ctx;
-
- if (mut->groups_error) {
- errno = mut->groups_error;
- } else if (!mut->groups) {
- mut->groups = bfs_groups_parse();
- if (!mut->groups) {
- mut->groups_error = errno;
- }
- }
-
- return mut->groups;
-}
-
-const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx) {
- struct bfs_ctx *mut = (struct bfs_ctx *)ctx;
-
- if (mut->mtab_error) {
- errno = mut->mtab_error;
- } else if (!mut->mtab) {
- mut->mtab = bfs_mtab_parse();
- if (!mut->mtab) {
- mut->mtab_error = errno;
- }
- }
-
- return mut->mtab;
-}
-
-/**
- * An open file tracked by the bfs context.
- */
-struct bfs_ctx_file {
- /** The file itself. */
- CFILE *cfile;
- /** The path to the file (for diagnostics). */
- const char *path;
-};
-
-struct CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
- struct bfs_stat sb;
- if (bfs_stat(fileno(cfile->file), NULL, 0, &sb) != 0) {
- return NULL;
- }
-
- bfs_file_id id;
- bfs_stat_id(&sb, &id);
-
- struct trie_leaf *leaf = trie_insert_mem(&ctx->files, id, sizeof(id));
- if (!leaf) {
- return NULL;
- }
-
- struct bfs_ctx_file *ctx_file = leaf->value;
- if (ctx_file) {
- ctx_file->path = path;
- return ctx_file->cfile;
- }
-
- leaf->value = ctx_file = malloc(sizeof(*ctx_file));
- if (!ctx_file) {
- trie_remove(&ctx->files, leaf);
- return NULL;
- }
-
- ctx_file->cfile = cfile;
- ctx_file->path = path;
- ++ctx->nfiles;
- return cfile;
-}
-
-/** Close a file tracked by the bfs context. */
-static int bfs_ctx_close(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) {
- CFILE *cfile = ctx_file->cfile;
-
- if (cfile == ctx->cout) {
- // Will be checked later
- return 0;
- } else if (cfile == ctx->cerr && !ctx_file->path) {
- // Writes to stderr are allowed to fail silently, unless the same file was used by
- // -fprint, -fls, etc.
- return 0;
- }
-
- int ret = 0, error = 0;
- if (ferror(cfile->file)) {
- ret = -1;
- error = EIO;
- }
-
- if (cfile == ctx->cerr) {
- if (fflush(cfile->file) != 0) {
- ret = -1;
- error = errno;
- }
- } else {
- if (cfclose(cfile) != 0) {
- ret = -1;
- error = errno;
- }
- }
-
- errno = error;
- return ret;
-}
-
-int bfs_ctx_free(struct bfs_ctx *ctx) {
- int ret = 0;
-
- if (ctx) {
- CFILE *cout = ctx->cout;
- CFILE *cerr = ctx->cerr;
-
- free_expr(ctx->expr);
- free_expr(ctx->exclude);
-
- bfs_mtab_free(ctx->mtab);
-
- bfs_groups_free(ctx->groups);
- bfs_users_free(ctx->users);
-
- struct trie_leaf *leaf;
- while ((leaf = trie_first_leaf(&ctx->files))) {
- struct bfs_ctx_file *ctx_file = leaf->value;
-
- if (bfs_ctx_close(ctx, ctx_file) != 0) {
- if (cerr) {
- bfs_error(ctx, "'%s': %m.\n", ctx_file->path);
- }
- ret = -1;
- }
-
- free(ctx_file);
- trie_remove(&ctx->files, leaf);
- }
- trie_destroy(&ctx->files);
-
- if (cout && fflush(cout->file) != 0) {
- if (cerr) {
- bfs_error(ctx, "standard output: %m.\n");
- }
- ret = -1;
- }
-
- cfclose(cout);
- cfclose(cerr);
-
- free_colors(ctx->colors);
-
- for (size_t i = 0; i < darray_length(ctx->paths); ++i) {
- free((char *)ctx->paths[i]);
- }
- darray_free(ctx->paths);
-
- free(ctx->argv);
- free(ctx);
- }
-
- return ret;
-}
diff --git a/darray.c b/darray.c
deleted file mode 100644
index 846d825..0000000
--- a/darray.c
+++ /dev/null
@@ -1,103 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "darray.h"
-#include <stdlib.h>
-#include <string.h>
-
-/**
- * The darray header.
- */
-struct darray {
- /** The current capacity of the array, as a count of elements. */
- size_t capacity;
- /** The current length of the array. */
- size_t length;
-
- // The array elements are stored after this header in memory. Not using
- // a flexible array member to avoid worrying about strict aliasing. We
- // assume that 2*sizeof(size_t) keeps any memory allocation suitably
- // aligned for the element type.
-};
-
-/** Get the header for a darray. */
-static struct darray *darray_header(const void *da) {
- return (struct darray *)da - 1;
-}
-
-/** Get the array from a darray header. */
-static char *darray_data(struct darray *header) {
- return (char *)(header + 1);
-}
-
-size_t darray_length(const void *da) {
- if (da) {
- return darray_header(da)->length;
- } else {
- return 0;
- }
-}
-
-void *darray_push(void *da, const void *item, size_t size) {
- struct darray *header;
- if (da) {
- header = darray_header(da);
- } else {
- header = malloc(sizeof(*header) + size);
- if (!header) {
- return NULL;
- }
- header->capacity = 1;
- header->length = 0;
- }
-
- size_t capacity = header->capacity;
- size_t i = header->length++;
- if (i >= capacity) {
- capacity *= 2;
- header = realloc(header, sizeof(*header) + capacity*size);
- if (!header) {
- // This failure will be detected by darray_check()
- return da;
- }
- header->capacity = capacity;
- }
-
- char *data = darray_data(header);
- memcpy(data + i*size, item, size);
- return data;
-}
-
-int darray_check(void *da) {
- if (!da) {
- return -1;
- }
-
- struct darray *header = darray_header(da);
- if (header->length <= header->capacity) {
- return 0;
- } else {
- // realloc() failed in darray_push(), so reset the length and report the failure
- header->length = header->capacity;
- return -1;
- }
-}
-
-void darray_free(void *da) {
- if (da) {
- free(darray_header(da));
- }
-}
diff --git a/darray.h b/darray.h
deleted file mode 100644
index 6ab8199..0000000
--- a/darray.h
+++ /dev/null
@@ -1,110 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A dynamic array library.
- *
- * darrays are represented by a simple pointer to the array element type, like
- * any other array. Behind the scenes, the capacity and current length of the
- * array are stored along with it. NULL is a valid way to initialize an empty
- * darray:
- *
- * int *darray = NULL;
- *
- * To append an element to a darray, use the DARRAY_PUSH macro:
- *
- * int e = 42;
- * if (DARRAY_PUSH(&darray, &e) != 0) {
- * // Report the error...
- * }
- *
- * The length can be retrieved by darray_length(). Iterating over the array
- * works like normal arrays:
- *
- * for (size_t i = 0; i < darray_length(darray); ++i) {
- * printf("%d\n", darray[i]);
- * }
- *
- * To free a darray, use darray_free():
- *
- * darray_free(darray);
- */
-
-#ifndef BFS_DARRAY_H
-#define BFS_DARRAY_H
-
-#include <stddef.h>
-
-/**
- * Get the length of a darray.
- *
- * @param da
- * The array in question.
- * @return
- * The length of the array.
- */
-size_t darray_length(const void *da);
-
-/**
- * @internal Use DARRAY_PUSH().
- *
- * Push an element into a darray.
- *
- * @param da
- * The array to append to.
- * @param item
- * The item to append.
- * @param size
- * The size of the item.
- * @return
- * The (new) location of the array.
- */
-void *darray_push(void *da, const void *item, size_t size);
-
-/**
- * @internal Use DARRAY_PUSH().
- *
- * Check if the last darray_push() call failed.
- *
- * @param da
- * The darray to check.
- * @return
- * 0 on success, -1 on failure.
- */
-int darray_check(void *da);
-
-/**
- * Free a darray.
- *
- * @param da
- * The darray to free.
- */
-void darray_free(void *da);
-
-/**
- * Push an item into a darray.
- *
- * @param da
- * The array to append to.
- * @param item
- * A pointer to the item to append.
- * @return
- * 0 on success, -1 on failure.
- */
-#define DARRAY_PUSH(da, item) \
- (darray_check(*(da) = darray_push(*(da), (item), sizeof(**(da) = *(item)))))
-
-#endif // BFS_DARRAY_H
diff --git a/diag.c b/diag.c
deleted file mode 100644
index 4b54c0a..0000000
--- a/diag.c
+++ /dev/null
@@ -1,104 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "diag.h"
-#include "ctx.h"
-#include "color.h"
-#include "util.h"
-#include <errno.h>
-#include <stdarg.h>
-
-void bfs_perror(const struct bfs_ctx *ctx, const char *str) {
- bfs_error(ctx, "%s: %m.\n", str);
-}
-
-void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bfs_verror(ctx, format, args);
- va_end(args);
-}
-
-bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bool ret = bfs_vwarning(ctx, format, args);
- va_end(args);
- return ret;
-}
-
-bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bool ret = bfs_vdebug(ctx, flag, format, args);
- va_end(args);
- return ret;
-}
-
-void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args) {
- int error = errno;
-
- bfs_error_prefix(ctx);
-
- errno = error;
- cvfprintf(ctx->cerr, format, args);
-}
-
-bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) {
- int error = errno;
-
- if (bfs_warning_prefix(ctx)) {
- errno = error;
- cvfprintf(ctx->cerr, format, args);
- return true;
- } else {
- return false;
- }
-}
-
-bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args) {
- int error = errno;
-
- if (bfs_debug_prefix(ctx, flag)) {
- errno = error;
- cvfprintf(ctx->cerr, format, args);
- return true;
- } else {
- return false;
- }
-}
-
-void bfs_error_prefix(const struct bfs_ctx *ctx) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${er}error:${rs} ", xbasename(ctx->argv[0]));
-}
-
-bool bfs_warning_prefix(const struct bfs_ctx *ctx) {
- if (ctx->warn) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${wr}warning:${rs} ", xbasename(ctx->argv[0]));
- return true;
- } else {
- return false;
- }
-}
-
-bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) {
- if (ctx->debug & flag) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", xbasename(ctx->argv[0]), debug_flag_name(flag));
- return true;
- } else {
- return false;
- }
-}
diff --git a/diag.h b/diag.h
deleted file mode 100644
index aa5e1c7..0000000
--- a/diag.h
+++ /dev/null
@@ -1,86 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Formatters for diagnostic messages.
- */
-
-#ifndef BFS_DIAG_H
-#define BFS_DIAG_H
-
-#include "ctx.h"
-#include "util.h"
-#include <stdarg.h>
-#include <stdbool.h>
-
-/**
- * Like perror(), but decorated like bfs_error().
- */
-void bfs_perror(const struct bfs_ctx *ctx, const char *str);
-
-/**
- * Shorthand for printing error messages.
- */
-BFS_FORMATTER(2, 3)
-void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
-
-/**
- * Shorthand for printing warning messages.
- *
- * @return Whether a warning was printed.
- */
-BFS_FORMATTER(2, 3)
-bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
-
-/**
- * Shorthand for printing debug messages.
- *
- * @return Whether a debug message was printed.
- */
-BFS_FORMATTER(3, 4)
-bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...);
-
-/**
- * bfs_error() variant that takes a va_list.
- */
-void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args);
-
-/**
- * bfs_warning() variant that takes a va_list.
- */
-bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args);
-
-/**
- * bfs_debug() variant that takes a va_list.
- */
-bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args);
-
-/**
- * Print the error message prefix.
- */
-void bfs_error_prefix(const struct bfs_ctx *ctx);
-
-/**
- * Print the warning message prefix.
- */
-bool bfs_warning_prefix(const struct bfs_ctx *ctx);
-
-/**
- * Print the debug message prefix.
- */
-bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag);
-
-#endif // BFS_DIAG_H
diff --git a/dir.c b/dir.c
deleted file mode 100644
index e0a7307..0000000
--- a/dir.c
+++ /dev/null
@@ -1,303 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "dir.h"
-#include "util.h"
-#include <dirent.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-#if __linux__
-# include <sys/syscall.h>
-#endif // __linux__
-
-enum bfs_type bfs_mode_to_type(mode_t mode) {
- switch (mode & S_IFMT) {
-#ifdef S_IFBLK
- case S_IFBLK:
- return BFS_BLK;
-#endif
-#ifdef S_IFCHR
- case S_IFCHR:
- return BFS_CHR;
-#endif
-#ifdef S_IFDIR
- case S_IFDIR:
- return BFS_DIR;
-#endif
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return BFS_DOOR;
-#endif
-#ifdef S_IFIFO
- case S_IFIFO:
- return BFS_FIFO;
-#endif
-#ifdef S_IFLNK
- case S_IFLNK:
- return BFS_LNK;
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return BFS_PORT;
-#endif
-#ifdef S_IFREG
- case S_IFREG:
- return BFS_REG;
-#endif
-#ifdef S_IFSOCK
- case S_IFSOCK:
- return BFS_SOCK;
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return BFS_WHT;
-#endif
-
- default:
- return BFS_UNKNOWN;
- }
-}
-
-#if __linux__
-/**
- * This is not defined in the kernel headers for some reason, callers have to
- * define it themselves.
- */
-struct linux_dirent64 {
- ino64_t d_ino;
- off64_t d_off;
- unsigned short d_reclen;
- unsigned char d_type;
- char d_name[];
-};
-
-// Make the whole allocation 64k
-#define BUF_SIZE ((64 << 10) - 8)
-#endif
-
-struct bfs_dir {
-#if __linux__
- int fd;
- unsigned short pos;
- unsigned short size;
-#else
- DIR *dir;
- struct dirent *de;
-#endif
-};
-
-struct bfs_dir *bfs_opendir(int at_fd, const char *at_path) {
-#if __linux__
- struct bfs_dir *dir = malloc(sizeof(*dir) + BUF_SIZE);
-#else
- struct bfs_dir *dir = malloc(sizeof(*dir));
-#endif
- if (!dir) {
- return NULL;
- }
-
- int fd;
- if (at_path) {
- fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
- } else if (at_fd >= 0) {
- fd = at_fd;
- } else {
- free(dir);
- errno = EBADF;
- return NULL;
- }
-
- if (fd < 0) {
- free(dir);
- return NULL;
- }
-
-#if __linux__
- dir->fd = fd;
- dir->pos = 0;
- dir->size = 0;
-#else
- dir->dir = fdopendir(fd);
- if (!dir->dir) {
- int error = errno;
- close(fd);
- free(dir);
- errno = error;
- return NULL;
- }
-
- dir->de = NULL;
-#endif // __linux__
-
- return dir;
-}
-
-int bfs_dirfd(const struct bfs_dir *dir) {
-#if __linux__
- return dir->fd;
-#else
- return dirfd(dir->dir);
-#endif
-}
-
-/** Convert a dirent type to a bfs_type. */
-static enum bfs_type translate_type(int d_type) {
- switch (d_type) {
-#ifdef DT_BLK
- case DT_BLK:
- return BFS_BLK;
-#endif
-#ifdef DT_CHR
- case DT_CHR:
- return BFS_CHR;
-#endif
-#ifdef DT_DIR
- case DT_DIR:
- return BFS_DIR;
-#endif
-#ifdef DT_DOOR
- case DT_DOOR:
- return BFS_DOOR;
-#endif
-#ifdef DT_FIFO
- case DT_FIFO:
- return BFS_FIFO;
-#endif
-#ifdef DT_LNK
- case DT_LNK:
- return BFS_LNK;
-#endif
-#ifdef DT_PORT
- case DT_PORT:
- return BFS_PORT;
-#endif
-#ifdef DT_REG
- case DT_REG:
- return BFS_REG;
-#endif
-#ifdef DT_SOCK
- case DT_SOCK:
- return BFS_SOCK;
-#endif
-#ifdef DT_WHT
- case DT_WHT:
- return BFS_WHT;
-#endif
- }
-
- return BFS_UNKNOWN;
-}
-
-#if !__linux__
-/** Get the type from a struct dirent if it exists, and convert it. */
-static enum bfs_type dirent_type(const struct dirent *de) {
-#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN)
- return translate_type(de->d_type);
-#else
- return BFS_UNKNOWN;
-#endif
-}
-#endif
-
-/** Check if a name is . or .. */
-static bool is_dot(const char *name) {
- return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
-}
-
-int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) {
- while (true) {
-#if __linux__
- char *buf = (char *)(dir + 1);
-
- if (dir->pos >= dir->size) {
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // Make sure msan knows the buffer is initialized
- memset(buf, 0, BUF_SIZE);
-#endif
-
- ssize_t size = syscall(__NR_getdents64, dir->fd, buf, BUF_SIZE);
- if (size <= 0) {
- return size;
- }
- dir->pos = 0;
- dir->size = size;
- }
-
- const struct linux_dirent64 *lde = (void *)(buf + dir->pos);
- dir->pos += lde->d_reclen;
-
- if (is_dot(lde->d_name)) {
- continue;
- }
-
- if (de) {
- de->type = translate_type(lde->d_type);
- de->name = lde->d_name;
- }
-
- return 1;
-#else // !__linux__
- errno = 0;
- dir->de = readdir(dir->dir);
- if (dir->de) {
- if (is_dot(dir->de->d_name)) {
- continue;
- }
- if (de) {
- de->type = dirent_type(dir->de);
- de->name = dir->de->d_name;
- }
- return 1;
- } else if (errno != 0) {
- return -1;
- } else {
- return 0;
- }
-#endif // !__linux__
- }
-}
-
-int bfs_closedir(struct bfs_dir *dir) {
-#if __linux__
- int ret = close(dir->fd);
-#else
- int ret = closedir(dir->dir);
-#endif
- free(dir);
- return ret;
-}
-
-int bfs_freedir(struct bfs_dir *dir) {
-#if __linux__
- int ret = dir->fd;
- free(dir);
- return ret;
-#elif __FreeBSD__
- int ret = fdclosedir(dir->dir);
- free(dir);
- return ret;
-#else
- int ret = dup_cloexec(dirfd(dir->dir));
- bfs_closedir(dir);
- return ret;
-#endif
-}
diff --git a/dir.h b/dir.h
deleted file mode 100644
index 69344c6..0000000
--- a/dir.h
+++ /dev/null
@@ -1,124 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Directories and their contents.
- */
-
-#ifndef BFS_DIR_H
-#define BFS_DIR_H
-
-#include <sys/types.h>
-
-/**
- * A directory.
- */
-struct bfs_dir;
-
-/**
- * File types.
- */
-enum bfs_type {
- /** An error occurred for this file. */
- BFS_ERROR = -1,
- /** Unknown type. */
- BFS_UNKNOWN,
- /** Block device. */
- BFS_BLK,
- /** Character device. */
- BFS_CHR,
- /** Directory. */
- BFS_DIR,
- /** Solaris door. */
- BFS_DOOR,
- /** Pipe. */
- BFS_FIFO,
- /** Symbolic link. */
- BFS_LNK,
- /** Solaris event port. */
- BFS_PORT,
- /** Regular file. */
- BFS_REG,
- /** Socket. */
- BFS_SOCK,
- /** BSD whiteout. */
- BFS_WHT,
-};
-
-/**
- * Convert a bfs_stat() mode to a bfs_type.
- */
-enum bfs_type bfs_mode_to_type(mode_t mode);
-
-/**
- * A directory entry.
- */
-struct bfs_dirent {
- /** The type of this file (possibly unknown). */
- enum bfs_type type;
- /** The name of this file. */
- const char *name;
-};
-
-/**
- * Open a directory.
- *
- * @param at_fd
- * The base directory for path resolution.
- * @param at_path
- * The path of the directory to open, relative to at_fd. Pass NULL to
- * open at_fd itself.
- * @return
- * The opened directory, or NULL on failure.
- */
-struct bfs_dir *bfs_opendir(int at_fd, const char *at_path);
-
-/**
- * Get the file descriptor for a directory.
- */
-int bfs_dirfd(const struct bfs_dir *dir);
-
-/**
- * Read a directory entry.
- *
- * @param dir
- * The directory to read.
- * @param[out] dirent
- * The directory entry to populate.
- * @return
- * 1 on success, 0 on EOF, or -1 on failure.
- */
-int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de);
-
-/**
- * Close a directory.
- *
- * @return
- * 0 on success, -1 on failure.
- */
-int bfs_closedir(struct bfs_dir *dir);
-
-/**
- * Free a directory, keeping an open file descriptor to it.
- *
- * @param dir
- * The directory to free.
- * @return
- * The file descriptor on success, or -1 on failure.
- */
-int bfs_freedir(struct bfs_dir *dir);
-
-#endif // BFS_DIR_H
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
new file mode 100644
index 0000000..69a997c
--- /dev/null
+++ b/docs/BUILDING.md
@@ -0,0 +1,201 @@
+Building `bfs`
+==============
+
+A simple invocation of
+
+ $ ./configure
+ $ make
+
+should build `bfs` successfully.
+
+
+Configuration
+-------------
+
+```console
+$ ./configure --help
+Usage:
+
+ $ ./configure [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
+ $ make
+
+...
+```
+
+### Variables
+
+Variables set in the environment or on the command line will be picked up:
+These variables specify binaries to run during the configuration and build process:
+
+<pre>
+<b>MAKE</b>=<i>make</i>
+ <a href="https://en.wikipedia.org/wiki/Make_(software)">make</a> implementation
+<b>CC</b>=<i>cc</i>
+ C compiler
+<b>INSTALL</b>=<i>install</i>
+ Copy files during <i>make install</i>
+<b>MKDIR</b>="<i>mkdir -p</i>"
+ Create directories
+<b>PKG_CONFIG</b>=<i>pkg-config</i>
+ Detect external libraries and required build flags
+<b>RM</b>="<i>rm -f</i>"
+ Delete files
+</pre>
+
+These flags will be used by the build process:
+
+<pre>
+<b>CPPFLAGS</b>="<i>-I... -D...</i>"
+<b>CFLAGS</b>="<i>-W... -f...</i>"
+<b>LDFLAGS</b>="<i>-L... -Wl,...</i>"
+ Preprocessor/compiler/linker flags
+
+<b>LDLIBS</b>="<i>-l... -l...</i>"
+ Dynamic libraries to link
+
+<b>EXTRA_</b>{<b>CPPFLAGS</b>,<b>CFLAGS</b>,<b>LDFLAGS</b>,<b>LDLIBS</b>}="<i>...</i>"
+ Adds to the default flags, instead of replacing them
+</pre>
+
+### Build profiles
+
+The default flags result in a plain debug build.
+Other build profiles can be enabled:
+
+<pre>
+--enable-release
+ Enable optimizations, disable assertions
+
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizer">asan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode">lsan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/MemorySanitizer">msan</a>
+--enable-<a href="https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual">tsan</a>
+--enable-<a href="https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html">ubsan</a>
+ Enable sanitizers
+
+--enable-<a href="https://gcc.gnu.org/onlinedocs/gcc/gcov/introduction-to-gcov.html">gcov</a>
+ Enable code coverage instrumentation
+</pre>
+
+You can combine multiple profiles (e.g. `./configure --enable-asan --enable-ubsan`), but not all of them will work together.
+
+### Dependencies
+
+`bfs` depends on some system libraries for some of its features.
+External dependencies are auto-detected by default, but you can build `--with` or `--without` them explicitly:
+
+<pre>
+--with-<a href="https://savannah.nongnu.org/projects/acl">libacl</a> --without-libacl
+--with-<a href="https://sites.google.com/site/fullycapable/">libcap</a> --without-libcap
+--with-<a href="https://github.com/SELinuxProject/selinux">libselinux</a> --without-libselinux
+--with-<a href="https://github.com/axboe/liburing">liburing</a> --without-liburing
+--with-<a href="https://github.com/kkos/oniguruma">oniguruma</a> --without-oniguruma
+</pre>
+
+[`pkg-config`] is used, if available, to detect these libraries and any additional build flags they may require.
+If this is undesirable, disable it by setting `PKG_CONFIG` to the empty string (`./configure PKG_CONFIG=""`).
+
+[`pkg-config`]: https://www.freedesktop.org/wiki/Software/pkg-config/
+
+### Out-of-tree builds
+
+You can set up an out-of-tree build by running the `configure` script from another directory, for example:
+
+ $ mkdir out
+ $ cd out
+ $ ../configure
+ $ make
+
+
+Building
+--------
+
+### Targets
+
+The [`Makefile`](/Makefile) supports several different build targets:
+
+<pre>
+make
+ The default target; builds just the <i>bfs</i> binary
+make <b>all</b>
+ Builds everything, including the tests (but doesn't run them)
+
+make <b>check</b>
+ Builds everything, and runs all tests
+make <b>unit-tests</b>
+ Builds and runs the unit tests
+make <b>integration-tests</b>
+ Builds and runs the integration tests
+make <b>distcheck</b>
+ Builds and runs the tests in multiple different configurations
+
+make <b>install</b>
+ Installs bfs globally
+make <b>uninstall</b>
+ Uninstalls bfs
+
+make <b>clean</b>
+ Deletes all built files
+make <b>distclean</b>
+ Also deletes files generated by ./configure
+</pre>
+
+
+Troubleshooting
+---------------
+
+If the build fails or behaves unexpectedly, start by enabling verbose mode:
+
+ $ ./configure V=1
+ $ make V=1
+
+This will print the generated configuration and the exact commands that are executed.
+
+You can also check the file `gen/config.log`, which contains any errors from commands run during the configuration phase.
+
+
+Testing
+-------
+
+`bfs` comes with an extensive test suite which can be run with
+
+ $ make check
+
+The test harness is implemented in the file [`tests/tests.sh`](/tests/tests.sh).
+Individual test cases are found in `tests/*/*.sh`.
+Most of them are *snapshot tests* which compare `bfs`'s output to a known-good copy saved under the matching `tests/*/*.out`.
+
+You can pass the name of a particular test case (or a few) to run just those tests.
+For example:
+
+ $ ./tests/tests.sh posix/basic
+
+If you need to update the reference snapshot, pass `--update`.
+It can be handy to generate the snapshot with a different `find` implementation to ensure the output is correct, for example:
+
+ $ ./tests/tests.sh posix/basic --bfs=find --update
+
+But keep in mind, other `find` implementations may not be correct.
+To my knowledge, no other implementation passes even the POSIX-compatible subset of the tests:
+
+ $ ./tests/tests.sh --bfs=find --sudo --posix
+ ...
+ [PASS] 104 / 119
+ [SKIP] 1 / 119
+ [FAIL] 14 / 119
+
+Run
+
+ $ ./tests/tests.sh --help
+
+for more details.
+
+### Validation
+
+A more thorough testsuite is run by the [CI](https://github.com/tavianator/bfs/actions) and to validate releases.
+It builds `bfs` in multiple configurations to test for latent bugs, memory leaks, 32-bit compatibility, etc.
+You can run it yourself with
+
+ $ make distcheck
+
+Some of these tests require `sudo`, and will prompt for your password if necessary.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..56f53b4
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,1238 @@
+4.*
+===
+
+4.0.8
+-----
+
+**June 20, 2025**
+
+### Bug fixes
+
+- Fixed an invalid optimization that transformed
+
+ $ bfs -user you -or -user me
+
+ into just
+
+ $ bfs -user you
+
+ The bug was originally introduced in bfs 2.0 (October 14, 2020).
+ ([#155](https://github.com/tavianator/bfs/issues/155))
+
+
+4.0.7
+-----
+
+**June 15, 2025**
+
+### Changes
+
+- `bfs` now takes CPU affinity into account when picking how many threads to use
+ ([`a36774b`](https://github.com/tavianator/bfs/commit/a36774be636c3429c6e73de33bf65a1bdbdcfb4b))
+
+- `-execdir /bin/...` is now allowed even with a relative path in `$PATH`
+ ([`cb40f51`](https://github.com/tavianator/bfs/commit/cb40f51e4e6375a10265484b6959c6b1b0591378))
+
+- *Expect* is no longer a test suite dependency
+ ([`7102fec`](https://github.com/tavianator/bfs/commit/7102fec257835302cb4978160bba4cbebd0b63e1))
+
+### Bug fixes
+
+- Only the last `-files0-from` argument now has any effect, to match GNU find
+ ([`a662fda`](https://github.com/tavianator/bfs/commit/a662fda2642e17478bc8e78adb4c6642a8505cdb))
+
+- Fixed `-execdir {}`, which was inadvertently broken in bfs 4.0
+ ([`def4a83`](https://github.com/tavianator/bfs/commit/def4a832425bfe94b96b8cb1146a83552b754fb4))
+
+
+4.0.6
+-----
+
+**February 26, 2025**
+
+### Bug fixes
+
+- Fixed `-fstype` with btrfs subvolumes (requires Linux 5.8+)
+ ([`0dccdae`](https://github.com/tavianator/bfs/commit/0dccdae4510ff5603247be871e64a6119647ea2a))
+
+- Fixed `-ls` with timestamps very far in the future
+ ([`dd5df1f`](https://github.com/tavianator/bfs/commit/dd5df1f8997550c5bf49205578027715b957bd01))
+
+- Fixed the `posix/exec_sigmask` test on mips64el Linux
+ ([`532dec0`](https://github.com/tavianator/bfs/commit/532dec0849dcdc3e15e530ac40a8168f146a41cd))
+
+- Fixed time-related tests with `mawk 1.3.4 20250131`
+ ([#152](https://github.com/tavianator/bfs/issues/152))
+
+
+4.0.5
+-----
+
+**January 18, 2025**
+
+### Bug fixes
+
+- Fixed a bug that could cause child processes (e.g. from `-exec`) to run with all signals blocked.
+ The bug was introduced in version 3.3.
+ ([`af207e7`](https://github.com/tavianator/bfs/commit/af207e702148e5c9ae08047d7a2dce6394653b62))
+
+### Changes
+
+- Fixed the build against old liburing versions
+ ([#147](https://github.com/tavianator/bfs/issues/147))
+
+- Async I/O performance optimizations
+
+
+4.0.4
+-----
+
+**October 31, 2024**
+
+## Bug fixes
+
+- Fixed a man page typo
+ ([#144](https://github.com/tavianator/bfs/pull/144))
+
+- Fixed the build on PowerPC macOS
+ ([#145](https://github.com/tavianator/bfs/issues/145))
+
+- Fixed a bug introduced in bfs 4.0.3 that colorized every file as if it had capabilities on non-Linux systems
+ ([#146](https://github.com/tavianator/bfs/pull/146))
+
+
+4.0.3
+-----
+
+**October 22, 2024**
+
+### Bug fixes
+
+- Fixed an assertion failure when `$LS_COLORS` contained escaped NUL bytes like `*\0.gz=`
+ ([`f5eaadb9`](https://github.com/tavianator/bfs/commit/f5eaadb96fb94b2d3666e53a99495840a3099aec))
+
+- Fixed a use-after-free bug introduced in bfs 4.0 when unregistering and re-registering signal hooks.
+ This could be reproduced with `bfs -nocolor` by repeatedly sending `SIGINFO`/`SIGUSR1` to toggle the status bar.
+ ([`39ff273`](https://github.com/tavianator/bfs/commit/39ff273df97e51b1285358b9e6808b117ea8adb1))
+
+- Fixed a hang present since bfs 3.0 colorizing paths like `notdir/file`, where `notdir` is a symlink pointing to a non-directory file.
+ ([`b89f22cb`](https://github.com/tavianator/bfs/commit/b89f22cbf250958a802915eb7b6bf0e5f38376ca))
+
+
+4.0.2
+-----
+
+**September 17, 2024**
+
+### New features
+
+- Implemented `./configure --version=X.Y.Z`, mainly for packagers to override the version number
+ ([`4a278d3`](https://github.com/tavianator/bfs/commit/4a278d3e39a685379711727eac7bfaa83679e0e4))
+
+### Changes
+
+- Minor refactoring of the build system
+
+### Bug fixes
+
+- Fixed `./configure --help`, which was broken since `bfs` 4.0
+ ([`07ae989`](https://github.com/tavianator/bfs/commit/07ae98906dbb0caaac2f758d72e88dd0975b2a81))
+
+- Fixed compiler flag auto-detection on systems with non-GNU `sed`.
+ This fixes a potential race condition on FreeBSD since `bfs` 4.0 due to the [switch to `_Fork()`](https://github.com/tavianator/bfs/commit/085bb402c7b2c2f96624fb0523ff3f9686fe26d9) without passing `-z now` to the linker.
+ ([`34e6081`](https://github.com/tavianator/bfs/commit/34e60816adb0ea8ddb155a454676a99ab225dc8a))
+
+- Fixed `$MAKE distcheck` when `$MAKE` is not `make`, e.g. `gmake distcheck` on BSD
+ ([`2135b00`](https://github.com/tavianator/bfs/commit/2135b00d215efc5c2c38e1abd3254baf31229ad4))
+
+- Fixed some roff syntax issues in the `bfs` manpage
+ ([`812ecd1`](https://github.com/tavianator/bfs/commit/812ecd1feeb002252dd4d732b395d31c4179afaf))
+
+- Fixed an assertion failure optimizing expressions like `bfs -not \( -prune , -type f \)` since `bfs` 3.1.
+ Release builds were not affected, since their assertions are disabled and the behaviour was otherwise correct.
+ ([`b1a9998`](https://github.com/tavianator/bfs/commit/b1a999892b9e13181ddd9a7d895f3d1c65fbb449))
+
+
+4.0.1
+-----
+
+**August 19, 2024**
+
+### Bug fixes
+
+- `bfs` no longer prints a "suppressed errors" warning unless `-noerror` is actually suppressing errors
+ ([`5d03c9d`](https://github.com/tavianator/bfs/commit/5d03c9d460d1c1afcdf062d494537986ce96a690))
+
+
+4.0
+---
+
+**August 16, 2024**
+
+### New features
+
+- To match BSD `find` (and the POSIX Utility Syntax Guidelines), multiple flags can now be given in a single argument like `-LEXO2`.
+ Previously, you would have had to write `-L -E -X -O2`.
+ ([`c0fd33a`](https://github.com/tavianator/bfs/commit/c0fd33aaef5f345566a41c7c2558f27adf05558b))
+
+- Explicit timestamps can now be written as `@SECONDS_SINCE_EPOCH`.
+ For example, `bfs -newermt @946684800` will print files modified since January 1, 2000 (UTC).
+ ([`c6bb003`](https://github.com/tavianator/bfs/commit/c6bb003b8882e9a16941f5803d072ec1cb728318))
+
+- The new `-noerror` option suppresses all error messages during traversal.
+ ([#142](https://github.com/tavianator/bfs/issues/142))
+
+### Changes
+
+- `-mount` now excludes mount points entirely, to comply with the recently published POSIX 2024 standard.
+ Use `-xdev` to include the mount point itself, but not its contents.
+ `bfs` has been warning about this change since version 1.5.1 (September 2019).
+ ([`33b85e1`](https://github.com/tavianator/bfs/commit/33b85e1f8769e7f75721887638ae454d109a034f))
+
+- `-perm` now takes the current file creation mask into account when parsing a symbolic mode like `+rw`, as clarified by [POSIX defect 1392](https://www.austingroupbugs.net/view.php?id=1392).
+ This matches the behaviour of BSD `find`, contrary to the behaviour of GNU `find`.
+ ([`6290ce4`](https://github.com/tavianator/bfs/commit/6290ce41f3ec1f889abb881cf90ca91da869b5b2))
+
+### Bug fixes
+
+- Fixed commands like `./configure CC=clang --enable-release` that set variables before other options
+ ([`49a5d48`](https://github.com/tavianator/bfs/commit/49a5d48d0a43bac313c8b8d1b167e60da9eaadf6))
+
+- Fixed the build on RISC-V with GCC versions older than 14
+ ([`e93a1dc`](https://github.com/tavianator/bfs/commit/e93a1dccd82f831a2f0d2cc382d8af5e1fda55ed))
+
+- Fixed running `bfs` under Valgrind
+ ([`a01cfac`](https://github.com/tavianator/bfs/commit/a01cfacd423af28af6b7c13ba51e2395f3a52ee7))
+
+- Fixed the exit code when failing to execute a non-existent command with `-exec`/`-ok` on some platforms including OpenBSD and HPPA
+ ([`8c130ca`](https://github.com/tavianator/bfs/commit/8c130ca0117fd225c24569be2ec16c7dc2150a13))
+
+- Fixed `$LS_COLORS` case-sensitivity to match GNU ls more closely when the same extension is specified multiple times
+ ([`08030ae`](https://github.com/tavianator/bfs/commit/08030aea919039165c02805e8c637a9ec1ad0d70))
+
+- Fixed the `-status` bar on Solaris/Illumos
+
+
+3.*
+===
+
+3.3.1
+-----
+
+**June 3, 2024**
+
+### Bug fixes
+
+- Reduced the scope of the symbolic link loop change in version 3.3.
+ `-xtype l` remains true for symbolic link loops, matching a change in GNU findutils 4.10.0.
+ However, `-L` will report an error, just like `bfs` prior to 3.3 and other `find` implementations, as required by POSIX.
+
+
+3.3
+---
+
+**May 28, 2024**
+
+### New features
+
+- The `-status` bar can now be toggled by `SIGINFO` (<kbd>Ctrl</kbd>+<kbd>T</kbd>) on systems that support it, and `SIGUSR1` on other systems
+
+- `-regextype` now supports all regex types from GNU find ([#21](https://github.com/tavianator/bfs/issues/21))
+
+- File birth times are now supported on OpenBSD
+
+### Changes
+
+- Symbolic link loops are now treated like other broken links, rather than an error
+
+- `./configure` now expects `--with-libacl`, `--without-libcap`, etc. rather than `--enable-`/`--disable-`
+
+- The ` ` (space) flag is now restricted to numeric `-printf` specifiers
+
+### Bug fixes
+
+- `-regextype emacs` now supports [shy](https://www.gnu.org/software/emacs/manual/html_node/elisp/Regexp-Backslash.html#index-shy-groups) (non-capturing) groups
+
+- Fixed `-status` bar visual corruption when the terminal is resized
+
+- `bfs` now prints a reset escape sequence when terminated by a signal in the middle of colored output ([#138](https://github.com/tavianator/bfs/issues/138))
+
+- `./configure CFLAGS=...` no longer overrides flags from `pkg-config` during configuration
+
+
+3.2
+---
+
+**May 2, 2024**
+
+### New features
+
+- New `-limit N` action that quits immediately after `N` results
+
+- Implemented `-context` (from GNU find) for matching SELinux contexts ([#27](https://github.com/tavianator/bfs/issues/27))
+
+- Implemented `-printf %Z` for printing SELinux contexts
+
+### Changes
+
+- The build system has been rewritten, and there is now a configure step:
+
+ $ ./configure
+ $ make
+
+ See `./configure --help` or [docs/BUILDING.md](/docs/BUILDING.md) for more details.
+
+- Improved platform support
+ - Implemented `-acl` on Solaris/Illumos
+ - Implemented `-xattr` on DragonFly BSD
+
+### Bug fixes
+
+- Fixed some rarely-used code paths that clean up after allocation failures
+
+3.1.3
+-----
+
+**March 6, 2024**
+
+### Bug fixes
+
+- On Linux, the `io_uring` feature probing introduced in `bfs` 3.1.2 only applied to one thread, causing all other threads to avoid using io_uring entirely.
+ The probe results are now copied to all threads correctly.
+ ([`f64f76b`](https://github.com/tavianator/bfs/commit/f64f76b55400b71e8576ed7e4a377eb5ef9576aa))
+
+
+3.1.2
+-----
+
+**February 29, 2024**
+
+### Bug fixes
+
+- On Linux, we now check for supported `io_uring` operations before using them, which should fix `bfs` on 5.X series kernels that support `io_uring` but not all of `openat()`/`close()`/`statx()` ([`8bc72d6`](https://github.com/tavianator/bfs/commit/8bc72d6c20c5e38783c4956c4d9fde9b3ee9140c))
+
+- Fixed a test failure triggered by certain filesystem types for `/tmp` ([#131](https://github.com/tavianator/bfs/issues/131))
+
+- Fixed parsing and interpretation of timezone offsets for explicit reference times used in `-*since` and `-newerXt` ([`a9f3cde`](https://github.com/tavianator/bfs/commit/a9f3cde30426b546ba6e3172e1a7951213a72049))
+
+- Fixed the build on m68k ([`c749c11`](https://github.com/tavianator/bfs/commit/c749c11b04444ca40941dd2ddc5802faed148f6a))
+
+
+3.1.1
+-----
+
+**February 16, 2024**
+
+### Changes
+
+- Performance and scalability improvements
+
+- The file count in `bfs -status` now has a thousands separator
+
+
+3.1
+---
+
+**February 6, 2024**
+
+### New features
+
+- On Linux, `bfs` now uses [io_uring](https://en.wikipedia.org/wiki/Io_uring) for async I/O
+
+- On all platforms, `bfs` can now perform `stat()` calls in parallel, accelerating queries like `-links`, `-newer`, and `-size`, as well as colorized output
+
+- On FreeBSD, `-type w` now works to find whiteouts like the system `find`
+
+### Changes
+
+- Improved `bfs -j2` performance ([`b2ab7a1`](https://github.com/tavianator/bfs/commit/b2ab7a151fca517f4879e76e626ec85ad3de97c7))
+
+- Optimized `-exec` by using `posix_spawn()` when possible, which can avoid the overhead of `fork()` ([`95fbde1`](https://github.com/tavianator/bfs/commit/95fbde17a66377b6fbe7ff1f014301dbbf09270d))
+
+- `-execdir` and `-okdir` are now rejected if `$PATH` contains a relative path, matching the behaviour of GNU find ([`163baf1`](https://github.com/tavianator/bfs/commit/163baf1c9af13be0ce705b133e41e0c3d6427398))
+
+- Leading whitespace is no longer accepted in integer command line arguments like `-links ' 1'` ([`e0d7dc5`](https://github.com/tavianator/bfs/commit/e0d7dc5dfd7bdaa62b6bc18e9c1cce00bbe08577))
+
+### Bug fixes
+
+- `-quit` and `-exit` could be ignored in the iterative deepening modes (`-S {ids,eds}`).
+ This is now fixed ([`670ebd9`](https://github.com/tavianator/bfs/commit/670ebd97fb431e830b1500b2e7e8013b121fb2c5)).
+ The bug was introduced in version 3.0.3 (commit [`5f16169`]).
+
+- Fixed two possible errors in sort mode (`-s`):
+ - Too many open files ([`710c083`](https://github.com/tavianator/bfs/commit/710c083ff02eb1cc5b8daa6778784f3d1cd3c08d))
+ - Out of memory ([`76ffc8d`](https://github.com/tavianator/bfs/commit/76ffc8d30cb1160d55d855d8ac630a2b9075fbcf))
+
+- Fixed handling of FreeBSD union mounts ([`3ac3bee`](https://github.com/tavianator/bfs/commit/3ac3bee7b0d9c9be693415206efa664bf4a7d4a7))
+
+- Fixed `NO_COLOR` handling when it's set to the empty string ([`79aee58`](https://github.com/tavianator/bfs/commit/79aee58a4621d01c4b1e98c332775f3b87213ddb))
+
+- Fixed some portability issues:
+ - [OpenBSD](https://github.com/tavianator/bfs/compare/ee200c07643801c8b53e5b80df704ecbf77a884e...79f1521b0e628be72bed3a648f0ae90b62fc69b8)
+ - [NetBSD](https://github.com/tavianator/bfs/compare/683f2c41c72efcb82ce866e3dcc311ac9bd8b66d...6435684a7d515e18247ae1b3dd9ec8681fee22d0)
+ - [DragonFly BSD](https://github.com/tavianator/bfs/compare/08867473e75e8e20ca76c7fb181204839e28b271...45fb1d952c3b262278a3b22e9c7d60cca19a5407)
+ - [Illumos](https://github.com/tavianator/bfs/compare/4010140cb748cc4f7f57b0a3d514485796c665ce...ae94cdc00136685abe61d55e1e357caaa636d785)
+
+
+3.0.4
+-----
+
+**October 12, 2023**
+
+### Bug fixes
+
+- Fixed a segfault when reporting errors under musl ([`d40eb87`])
+
+[`d40eb87`]: https://github.com/tavianator/bfs/commit/d40eb87cc00f50a5debb8899eacb7fcf1065badf
+
+
+3.0.3
+-----
+
+**October 12, 2023**
+
+### Changes
+
+- Iterative deepening modes (`-S {ids,eds}`) were optimized by delaying teardown until the very end ([`5f16169`])
+
+- Parallel depth-first search (`-S dfs`) was optimized to avoid enqueueing every file separately ([`2572273`])
+
+### Bug fixes
+
+- Iterative deepening modes (`-S {ids,eds}`) were performing iterative *breadth*-first searches since `bfs` 3.0, negating any advantages they may have had over normal breadth-first search.
+ They now do iterative *depth*-first searches as expected.
+ ([`a029d95`])
+
+- Fixed a linked-list corruption that could lead to an infinite loop on macOS and other non-Linux, non-FreeBSD platforms ([`773f4a4`])
+
+[`5f16169`]: https://github.com/tavianator/bfs/commit/5f1616912ba3a7a23ce6bce02df3791b73da38ab
+[`2572273`]: https://github.com/tavianator/bfs/commit/257227326fe60fe70e80433fd34d1ebcb2f9f623
+[`a029d95`]: https://github.com/tavianator/bfs/commit/a029d95b5736a74879f32089514a5a6b63d6efbc
+[`773f4a4`]: https://github.com/tavianator/bfs/commit/773f4a446f03da62d88e6d17be49fdc0a3e38465
+
+
+3.0.2
+-----
+
+**September 6, 2023**
+
+### Changes
+
+- `-files0-from` now allows an empty set of paths to be given, matching GNU findutils 4.9.0
+
+- Reduced memory consumption in multi-threaded searches
+
+- Many man page updates
+
+### Bug fixes
+
+- Fixed an out-of-bounds memory read that could occur when escaping a string containing an incomplete multi-byte character
+
+
+3.0.1
+-----
+
+**July 18, 2023**
+
+### Bug fixes
+
+- Traversal fixes that mostly affect large directory trees ([#107])
+
+ - `bfs` could encounter `EMFILE`, close a file, and retry many times, particularly with `-j1`
+
+ - Breadth-first search could become highly unbalanced, negating many of the benefits of `bfs`
+
+ - On non-{Linux,FreeBSD} platforms, directories could stay open longer than necessary, consuming extra memory
+
+[#107]: https://github.com/tavianator/bfs/pull/107
+
+
+3.0
+---
+
+**July 13, 2023**
+
+### New features
+
+- `bfs` now reads directories asynchronously and in parallel ([#101]).
+ Performance is significantly improved as a result.
+ Parallelism is controlled by the new `-j` flag, e.g. `-j1`, `-j2`, etc.
+
+[#101]: https://github.com/tavianator/bfs/issues/101
+
+### Changes
+
+- `bfs` now uses the [C17] standard version, up from C11
+
+- Due to [#101], `bfs` now requires some additional C and POSIX features:
+ - [Standard C atomics] (`<stdatomic.h>`)
+ - [POSIX threads] (`<pthread.h>`)
+
+- `$LS_COLORS` extensions written in different cases (e.g. `*.jpg=35:*.JPG=01;35`) are now matched case-sensitively, to match the new behaviour of GNU ls since coreutils version 9.2
+
+- Added a warning/error if `$LS_COLORS` can't be parsed, depending on whether `-color` is requested explicitly
+
+- Filenames with control characters are now escaped when printing with `-color`
+
+- Build flags like `WITH_ONIGURUMA` have been renamed to `USE_ONIGURUMA`
+
+[C17]: https://en.cppreference.com/w/c/17
+[Standard C atomics]: https://en.cppreference.com/w/c/atomic
+[POSIX threads]: https://pubs.opengroup.org/onlinepubs/9699919799/idx/threads.html
+
+### Bug fixes
+
+- Fixed handling of the "normal text" color (`no` in `$LS_COLORS`) to match GNU ls
+
+
+2.*
+===
+
+2.6.3
+-----
+
+**January 31, 2023**
+
+- Fixed running the tests as root on Linux [`8b24de3`]
+
+- Fixed some tests on Android [`2724dfb`] [`0a5a80c`]
+
+- Stopped relying on non-POSIX touch(1) features in the tests.
+ This should fix the tests on at least OpenBSD.
+ [`2d5edb3`]
+
+- User/group caches are now filled lazily instead of eagerly [`b41dca5`]
+
+- More caches and I/O streams are flushed before -exec/-ok [`f98a1c4`]
+
+- Fixed various memory safety issues found by fuzzing \
+ [`712b137`] [`5ce883d`] [`da02def`] [`c55e855`]
+
+- Fixed a test failure on certain macOS versions [`8b24de3`]
+
+- Mitigated a race condition when determining filesystem types ([#97])
+
+- Lots of refactoring and optimization
+
+[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559
+[`2724dfb`]: https://github.com/tavianator/bfs/commit/2724dfbd17552f892a0d8b39b96cbe9e49d66fdb
+[`0a5a80c`]: https://github.com/tavianator/bfs/commit/0a5a80c98cc7e5d8735b615fa197a6cff2bb08cc
+[`2d5edb3`]: https://github.com/tavianator/bfs/commit/2d5edb37b924715b4fbee4d917ac334c773fca61
+[`b41dca5`]: https://github.com/tavianator/bfs/commit/b41dca52762c5188638236ae81b9f4597bb29ac9
+[`f98a1c4`]: https://github.com/tavianator/bfs/commit/f98a1c4a1cf61ff7d6483388ca1fac365fb0b31b
+[`712b137`]: https://github.com/tavianator/bfs/commit/712b13756a09014ef730c8f9b96da4dc2f09b762
+[`5ce883d`]: https://github.com/tavianator/bfs/commit/5ce883daaafc69f83b01dac5db0647e9662a6e87
+[`da02def`]: https://github.com/tavianator/bfs/commit/da02defb91c3a1bda0ea7e653d81f997f1c8884a
+[`c55e855`]: https://github.com/tavianator/bfs/commit/c55e85580df10c5afdc6fc0710e756a456aa8e93
+[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559
+[#97]: https://github.com/tavianator/bfs/issues/97
+
+
+2.6.2
+-----
+
+**October 21, 2022**
+
+- Fixed use of uninitialized memory on parsing errors involving `-fprintf`
+
+- Fixed Android build issues ([#96])
+
+- Refactored the test suite
+
+[#96]: https://github.com/tavianator/bfs/issues/96
+
+
+2.6.1
+-----
+
+**July 7, 2022**
+
+- Fix `stat()` errors on GNU Hurd systems with glibc older than 2.35
+
+- Added fish shell tab completion ([#94]).
+ Thanks @xfgusta!
+
+[#94]: https://github.com/tavianator/bfs/pull/94
+
+
+2.6
+---
+
+**May 21, 2022**
+
+- Fixed deleting large NFS directories on FreeBSD ([#67]).
+
+- Added support for a `bfs`-specific `BFS_COLORS` environment variable.
+
+- Refactored the build system, directory structure, and documentation ([#88], [#89], [#91]).
+ Thanks @ElectronicsArchiver!
+
+- Added `zsh` completion ([#86]).
+ Thanks @VorpalBlade!
+
+- Updated the default color scheme to match GNU coreutils 9.1.
+ Files with capabilities set are no longer colored differently by default, resulting in a significant performance improvement.
+
+- Became less aggressive at triggering automounts
+
+- Added support for out-of-tree builds with `BUILDDIR`
+
+[#67]: https://github.com/tavianator/bfs/issues/67
+[#86]: https://github.com/tavianator/bfs/issues/86
+[#88]: https://github.com/tavianator/bfs/issues/88
+[#89]: https://github.com/tavianator/bfs/issues/89
+[#91]: https://github.com/tavianator/bfs/issues/91
+
+
+2.5
+---
+
+**March 27, 2022**
+
+- Added compiler-style context for errors and warnings.
+ Errors look like this:
+
+ $ bfs -nam needle
+ bfs: error: bfs -nam needle
+ bfs: error: ~~~~
+ bfs: error: Unknown argument; did you mean -name?
+
+ and warnings look like this:
+
+ $ bfs -print -name 'needle'
+ bfs: warning: bfs -print -name needle
+ bfs: warning: ~~~~~~~~~~~~
+ bfs: warning: The result of this expression is ignored.
+
+- Updated from C99 to C11
+
+- Fixed the tests when built against musl
+
+- Fixed a build error reported on Manjaro
+
+
+2.4.1
+-----
+
+**February 24, 2022**
+
+- Fixed the build when Oniguruma is not installed in the default search paths ([#82])
+
+- Fixed string encoding bugs with Oniguruma enabled
+
+- Fixed regex error reporting bugs
+
+[#82]: https://github.com/tavianator/bfs/issues/82
+
+
+2.4
+---
+
+**February 22, 2022**
+
+- Added the Oniguruma regular expression library as an (optional, but enabled by default) dependency ([#81]).
+ Oniguruma supports more regular expression syntax types than the POSIX regex API, and often performs better.
+ To build `bfs` without this new dependency, do `make WITH_ONIGURUMA=` to disable it.
+ Thanks @data-man!
+
+- Added support for the `ed`, `emacs`, `grep`, and `sed` regular expression types ([#21])
+
+- Before executing a process with `-exec[dir]`/`-ok[dir]`, `bfs` now ensures all output streams are flushed.
+ Previously, I/O from subprocesses could be interleaved unpredictably with buffered I/O from `bfs` itself.
+
+[#81]: https://github.com/tavianator/bfs/pull/81
+[#21]: https://github.com/tavianator/bfs/issues/21
+
+
+2.3.1
+-----
+
+**January 21, 2022**
+
+- Fixed the build on Debian kFreeBSD
+
+- Fixed a crash on GNU Hurd when piping bfs's output
+
+- Fixed a double-`close()` on non-Linux platforms if `fdopendir()` fails
+
+- Reduced memory allocations on startup
+
+
+2.3
+---
+
+**November 25, 2021**
+
+- More tweaks to `PAGER` and `LESS` handling for `bfs -help` ([#76])
+
+- Use 512-byte blocks for `-ls` when `POSIXLY_CORRECT` is set ([#77])
+
+- Implemented `-files0-from FILE` to take a list of `'\0'`-separated starting paths.
+ GNU find will implement the same feature in an upcoming release.
+
+- Added colors to `-printf` output ([#62])
+
+- Faster recovery from `E2BIG` during `-exec`
+
+[#76]: https://github.com/tavianator/bfs/issues/76
+[#77]: https://github.com/tavianator/bfs/issues/77
+[#62]: https://github.com/tavianator/bfs/issues/62
+
+
+2.2.1
+-----
+
+**June 2, 2021**
+
+- Fixed some incorrect coloring of broken links when links are being followed (`-L`)
+
+- Made the tests work when run as root by dropping privileges.
+ This may be helpful for certain packaging or CI environments, but is not recommended.
+
+- Treat empty `PAGER` and `LESS` environment variables like they're unset, for `bfs -help` ([#71]).
+ Thanks @markus-oberhumer!
+
+- The soft `RLIMIT_NOFILE` is now raised automatically to a fairly large value when possible.
+ This provides a minor performance benefit for large directory trees.
+
+- Implemented time units for `-mtime` as found in FreeBSD find ([#75])
+
+[#71]: https://github.com/tavianator/bfs/issues/71
+[#75]: https://github.com/tavianator/bfs/issues/75
+
+
+2.2
+---
+
+**March 6, 2021**
+
+- Fixed `-hidden` on hidden start paths
+
+- Added a Bash completion script.
+ Thanks @bmundt6!
+
+- Fixed rounding in `-used`.
+ Corresponding fixes were made to GNU find in version 4.8.0.
+
+- Optimized the open directory representation.
+ On Linux, much libc overhead is bypassed by issuing syscalls directly.
+ On all platforms, a few fewer syscalls and open file descriptors will be used.
+
+- Implemented `-flags` from BSD find
+
+
+2.1
+---
+
+**November 11, 2020**
+
+- Added a new `-status` option that displays the search progress in a bar at the bottom of the terminal
+
+- Fixed an optimizer bug introduced in version 2.0 that affected some combinations of `-user`/`-group` and `-nouser`/`-nogroup`
+
+
+2.0
+---
+
+**October 14, 2020**
+
+- [#8]: New `-exclude <expression>` syntax to more easily and reliably filter out paths.
+ For example:
+
+ bfs -name config -exclude -name .git
+
+ will find all files named `config`, without searching any directories (or files) named `.git`.
+ In this case, the same effect could have been achieved (more awkwardly) with `-prune`:
+
+ bfs ! \( -name .git -prune \) -name config
+
+ But `-exclude` will work in more cases:
+
+ # -exclude works with -depth, while -prune doesn't:
+ bfs -depth -name config -exclude -name .git
+
+ # -exclude applies even to paths below the minimum depth:
+ bfs -mindepth 3 -name config -exclude -name .git
+
+- [#30]: `-nohidden` is now equivalent to `-exclude -hidden`.
+ This changes the behavior of command lines like
+
+ bfs -type f -nohidden
+
+ to do what was intended.
+
+- Optimized the iterative deepening (`-S ids`) implementation
+
+- Added a new search strategy: exponential deepening search (`-S eds`).
+ This strategy provides many of the benefits of iterative deepening, but much faster due to fewer re-traversals.
+
+- Fixed an optimizer bug that could skip `-empty`/`-xtype` if they didn't always lead to an action
+
+- Implemented `-xattrname` to find files with a particular extended attribute (from macOS find)
+
+- Made `-printf %l` still respect the width specifier (e.g. `%10l`) for non-links, to match GNU find
+
+- Made `bfs` fail if `-color` is given explicitly and `LS_COLORS` can't be parsed, rather than falling back to non-colored output
+
+[#8]: https://github.com/tavianator/bfs/issues/8
+[#30]: https://github.com/tavianator/bfs/issues/30
+
+
+1.*
+===
+
+1.7
+---
+
+**April 22, 2020**
+
+- Fixed `-ls` printing numeric IDs instead of user/group names in large directory trees
+- Cached the user and group tables for a performance boost
+- Fixed interpretation of "default" ACLs
+- Implemented `-s` flag to sort results
+
+
+1.6
+---
+
+**February 25, 2020**
+
+- Implemented `-newerXt` (explicit reference times), `-since`, `-asince`, etc.
+- Fixed `-empty` to skip special files (pipes, devices, sockets, etc.)
+
+
+1.5.2
+-----
+
+**January 9, 2020**
+
+- Fixed the build on NetBSD
+- Added support for NFSv4 ACLs on FreeBSD
+- Added a `+` after the file mode for files with ACLs in `-ls`
+- Supported more file types (whiteouts, doors) in symbolic modes for `-ls`/`-printf %M`
+- Implemented `-xattr` on FreeBSD
+
+
+1.5.1
+-----
+
+**September 14, 2019**
+
+- Added a warning to `-mount`, since it will change behaviour in the next POSIX revision
+- Added a workaround for environments that block `statx()` with `seccomp()`, like older Docker
+- Fixed coloring of nonexistent leading directories
+- Avoided calling `stat()` on all mount points at startup
+
+
+1.5
+---
+
+**June 27, 2019**
+
+- New `-xattr` predicate to find files with extended attributes
+- Fixed the `-acl` implementation on macOS
+- Implemented depth-first (`-S dfs`) and iterative deepening search (`-S ids`)
+- Piped `-help` output into `$PAGER` by default
+- Fixed crashes on some invalid `LS_COLORS` values
+
+
+1.4.1
+-----
+
+**April 5, 2019**
+
+- Added a nicer error message when the tests are run as root
+- Fixed detection of comparison expressions with signs, to match GNU find for things like `-uid ++10`
+- Added support for https://no-color.org/
+- Decreased the number of `stat()` calls necessary in some cases
+
+
+1.4
+---
+
+**April 15, 2019**
+
+- New `-unique` option that filters out duplicate files ([#48])
+- Optimized the file coloring implementation
+- Fixed the coloring implementation to match GNU ls more closely in many corner cases
+ - Implemented escape sequence parsing for `LS_COLORS`
+ - Implemented `ln=target` for coloring links like their targets
+ - Fixed the order of fallbacks used when some color keys are unset
+- Add a workaround for incorrect file types for bind-mounted files on Linux ([#37])
+
+[#48]: https://github.com/tavianator/bfs/issues/48
+[#37]: https://github.com/tavianator/bfs/issues/37
+
+
+1.3.3
+-----
+
+**February 10, 2019**
+
+- Fixed unpredictable behaviour for empty responses to `-ok`/`-okdir` caused by an uninitialized string
+- Writing to standard output now causes `bfs` to fail if the descriptor was closed
+- Fixed incomplete file coloring in error messages
+- Added some data flow optimizations
+- Fixed `-nogroup`/`-nouser` in big directory trees
+- Added `-type w` for whiteouts, as supported by FreeBSD `find`
+- Re-wrote the `-help` message and manual page
+
+
+1.3.2
+-----
+
+**January 11, 2019**
+
+- Fixed an out-of-bounds read if LS_COLORS doesn't end with a `:`
+- Allowed multiple debug flags to be specified like `-D opt,tree`
+
+
+1.3.1
+-----
+
+**January 3, 2019**
+
+- Fixed some portability problems affecting FreeBSD
+
+
+1.3
+---
+
+**January 2, 2019**
+
+New features:
+
+- `-acl` finds files with non-trivial Access Control Lists (from FreeBSD)
+- `-capable` finds files with capabilities set
+- `-D all` turns on all debugging flags at once
+
+Fixes:
+
+- `LS_COLORS` handling has been improved:
+ - Extension colors are now case-insensitive like GNU `ls`
+ - `or` (orphan) and `mi` (missing) files are now treated differently
+ - Default colors can be unset with `di=00` or similar
+ - Specific colors fall back to more general colors when unspecified in more places
+ - `LS_COLORS` no longer needs a trailing colon
+- `-ls`/`-fls` now prints the major/minor numbers for device nodes
+- `-exec ;` is rejected rather than segfaulting
+- `bfs` now builds on old Linux versions that require `-lrt` for POSIX timers
+- For files whose access/change/modification times can't be read, `bfs` no longer fails unless those times are needed for tests
+- The testsuite is now more correct and portable
+
+
+1.2.4
+-----
+
+**September 24, 2018**
+
+- GNU find compatibility fixes for `-printf`:
+ - `%Y` now prints `?` if an error occurs resolving the link
+ - `%B` is now supported for birth/creation time (as well as `%W`/`%w`)
+ - All standard `strftime()` formats are supported, not just the ones from the GNU find manual
+- Optimizations are now re-run if any expressions are reordered
+- `-exec` and friends no longer leave zombie processes around when `exec()` fails
+
+
+1.2.3
+-----
+
+**July 15, 2018**
+
+- Fixed `test_depth_error` on filesystems that don't fill in `d_type`
+- Fixed the build on Linux architectures that don't have the `statx()` syscall (ia64, sh4)
+- Fixed use of AT_EMPTY_PATH for fstatat on systems that don't support it (Hurd)
+- Fixed `ARG_MAX` accounting on architectures with large pages (ppc64le)
+- Fixed the build against the upcoming glibc 2.28 release that includes its own `statx()` wrapper
+
+
+1.2.2
+-----
+
+**June 23, 2018**
+
+- Minor bug fixes:
+ - Fixed `-exec ... '{}' +` argument size tracking after recovering from `E2BIG`
+ - Fixed `-fstype` if `/proc` is available but `/etc/mtab` is not
+ - Fixed an uninitialized variable when given `-perm +rw...`
+ - Fixed some potential "error: 'path': Success" messages
+- Reduced reliance on GNU coreutils in the testsuite
+- Refactored and simplified the internals of `bftw()`
+
+
+1.2.1
+-----
+
+**February 8, 2018**
+
+- Performance optimizations
+
+
+1.2
+---
+
+**January 20, 2018**
+
+- Added support for the `-perm +7777` syntax deprecated by GNU find (equivalent to `-perm /7777`), for compatibility with BSD finds
+- Added support for file birth/creation times on platforms that report it
+ - `-Bmin`/`-Btime`/`-Bnewer`
+ - `B` flag for `-newerXY`
+ - `%w` and `%Wk` directives for `-printf`
+ - Uses the `statx(2)` system call on new enough Linux kernels
+- More robustness to `E2BIG` added to the `-exec` implementation
+
+
+1.1.4
+-----
+
+**October 27, 2017**
+
+- Added a man page
+- Fixed cases where multiple actions write to the same file
+- Report errors that occur when closing files/flushing streams
+- Fixed "argument list too long" errors with `-exec ... '{}' +`
+
+
+1.1.3
+-----
+
+**October 4, 2017**
+
+- Refactored the optimizer
+- Implemented data flow optimizations
+
+
+1.1.2
+-----
+
+**September 10, 2017**
+
+- Fixed `-samefile` and similar predicates when passed broken symbolic links
+- Implemented `-fstype` on Solaris
+- Fixed `-fstype` under musl
+- Implemented `-D search`
+- Implemented a cost-based optimizer
+
+
+1.1.1
+-----
+
+**August 10, 2017**
+
+- Re-licensed under the BSD Zero Clause License
+- Fixed some corner cases with `-exec` and `-ok` parsing
+
+
+1.1
+---
+
+**July 22, 2017**
+
+- Implemented some primaries from NetBSD `find`:
+ - `-exit [STATUS]` (like `-quit`, but with an optional explicit exit status)
+ - `-printx` (escape special characters for `xargs`)
+ - `-rm` (alias for `-delete`)
+- Warn if `-prune` will have no effect due to `-depth`
+- Handle y/n prompts according to the user's locale
+- Prompt the user to correct typos without having to re-run `bfs`
+- Fixed handling of paths longer than `PATH_MAX`
+- Fixed spurious "Inappropriate ioctl for device" errors when redirecting `-exec ... +` output
+- Fixed the handling of paths that treat a file as a directory (e.g. `a/b/c` where `a/b` is a regular file)
+- Fixed an expression optimizer bug that broke command lines like `bfs -name '*' -o -print`
+
+
+1.0.2
+-----
+
+**June 15, 2017**
+
+Bugfix release.
+
+- Fixed handling of \0 inside -printf format strings
+- Fixed `-perm` interpretation of permcopy actions (e.g. `u=rw,g=r`)
+
+
+1.0.1
+-----
+
+**May 17, 2017**
+
+Bugfix release.
+
+- Portability fixes that mostly affect GNU Hurd
+- Implemented `-D exec`
+- Made `-quit` not disable the implicit `-print`
+
+
+1.0
+---
+
+**April 24, 2017**
+
+This is the first release of bfs with support for all of GNU find's primitives.
+
+Changes since 0.96:
+
+- Implemented `-fstype`
+- Implemented `-exec/-execdir ... +`
+- Implemented BSD's `-X`
+- Fixed the tests under Bash 3 (mostly for macOS)
+- Some minor optimizations and fixes
+
+
+0.*
+===
+
+
+0.96
+----
+
+**March 11, 2017**
+
+73/76 GNU find features supported.
+
+- Implemented -nouser and -nogroup
+- Implemented -printf and -fprintf
+- Implemented -ls and -fls
+- Implemented -type with multiple types at once (e.g. -type f,d,l)
+- Fixed 32-bit builds
+- Fixed -lname on "symlinks" in Linux /proc
+- Fixed -quit to take effect as soon as it's reached
+- Stopped redirecting standard input from /dev/null for -ok and -okdir, as that violates POSIX
+- Many test suite improvements
+
+
+0.88
+----
+
+**December 20, 2016**
+
+67/76 GNU find features supported.
+
+- Fixed the build on macOS, and some other UNIXes
+- Implemented `-regex`, `-iregex`, `-regextype`, and BSD's `-E`
+- Implemented `-x` (same as `-mount`/`-xdev`) from BSD
+- Implemented `-mnewer` (same as `-newer`) from BSD
+- Implemented `-depth N` from BSD
+- Implemented `-sparse` from FreeBSD
+- Implemented the `T` and `P` suffices for `-size`, for BSD compatibility
+- Added support for `-gid NAME` and `-uid NAME` as in BSD
+
+
+0.84.1
+------
+
+**November 24, 2016**
+
+Bugfix release.
+
+- Fixed [#7] again
+- Like GNU find, don't print warnings by default if standard input is not a terminal
+- Redirect standard input from /dev/null for -ok and -okdir
+- Skip . when -delete'ing
+- Fixed -execdir when the root path has no slashes
+- Fixed -execdir in /
+- Support -perm +MODE for symbolic modes
+- Fixed the build on FreeBSD
+
+[#7]: https://github.com/tavianator/bfs/issues/7
+
+
+0.84
+----
+
+**October 29, 2016**
+
+64/76 GNU find features supported.
+
+- Spelling suggestion improvements
+- Handle `--`
+- (Untested) support for exotic file types like doors, ports, and whiteouts
+- Improved robustness in the face of closed std{in,out,err}
+- Fixed the build on macOS
+- Implement `-ignore_readdir_race`, `-noignore_readdir_race`
+- Implement `-perm`
+
+
+0.82
+----
+
+**September 4, 2016**
+
+62/76 GNU find features supported.
+
+- Rework optimization levels
+ - `-O1`
+ - Simple boolean simplification
+ - `-O2`
+ - Purity-based optimizations, allowing side-effect-free tests like `-name` or `-type` to be moved or removed
+ - `-O3` (**default**):
+ - Re-order tests to reduce the expected cost (TODO)
+ - `-O4`
+ - Aggressive optimizations that may have surprising effects on warning/error messages and runtime, but should not otherwise affect the results
+ - `-Ofast`:
+ - Always the highest level, currently the same as `-O4`
+- Color files with multiple hard links correctly
+- Treat `-`, `)`, and `,` as paths when required to by POSIX
+ - `)` and `,` are only supported before the expression begins
+- Implement `-D opt`
+- Implement `-D rates`
+- Implement `-fprint`
+- Implement `-fprint0`
+- Implement BSD's `-f`
+- Suggest fixes for typo'd arguments
+
+0.79
+----
+
+**May 27, 2016**
+
+60/76 GNU find features supported.
+
+- Remove an errant debug `printf()` from `-used`
+- Implement the `{} ;` variants of `-exec`, `-execdir`, `-ok`, and `-okdir`
+
+
+0.74
+----
+
+**March 12, 2016**
+
+56/76 GNU find features supported.
+
+- Color broken symlinks correctly
+- Fix [#7]
+- Fix `-daystart`'s rounding of midnight
+- Implement (most of) `-newerXY`
+- Implement `-used`
+- Implement `-size`
+
+[#7]: https://github.com/tavianator/bfs/issues/7
+
+
+0.70
+----
+
+**February 23, 2016**
+
+53/76 GNU find features supported.
+
+- New `make install` and `make uninstall` targets
+- Squelch non-positional warnings for `-follow`
+- Reduce memory footprint by as much as 64% by closing `DIR*`s earlier
+- Speed up `bfs` by ~5% by using a better FD cache eviction policy
+- Fix infinite recursion when evaluating `! expr`
+- Optimize unused pure expressions (e.g. `-empty -a -false`)
+- Optimize double-negation (e.g. `! ! -name foo`)
+- Implement `-D stat` and `-D tree`
+- Implement `-O`
+
+
+0.67
+----
+
+**February 14, 2016**
+
+Initial release.
+
+51/76 GNU find features supported.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..099157d
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+Contributing to `bfs`
+=====================
+
+License
+-------
+
+`bfs` is licensed under the [Zero-Clause BSD License](https://opensource.org/licenses/0BSD), a maximally permissive license.
+Contributions must use the same license.
+
+Individual files contain the following tag instead of the full license text:
+
+ SPDX-License-Identifier: 0BSD
+
+This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/
+
+
+Implementation
+--------------
+
+`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision)).
+You can get a feel for the coding style by skimming the source code.
+[`main.c`](/src/main.c) contains an overview of the rest of source files.
+A quick summary:
+
+- Tabs for indentation, spaces for alignment.
+- Most types and functions should be namespaced with `bfs_`.
+ Exceptions are made for things that could be generally useful outside of `bfs`.
+- Error handling follows the C standard library conventions: return a nonzero `int` or a `NULL` pointer, with the error code in `errno`.
+ All failure cases should be handled, including `malloc()` failures.
+- `goto` is not considered harmful for cleaning up in error paths.
+
+
+Tests
+-----
+
+`bfs` includes an extensive test suite.
+See the [build documentation](BUILDING.md#testing) for details on running the tests.
+
+Test cases are grouped by the standard or `find` implementation that supports the tested feature(s):
+
+| Group | Description |
+|---------------------------------|---------------------------------------|
+| [`tests/posix`](/tests/posix) | POSIX compatibility tests |
+| [`tests/bsd`](/tests/bsd) | BSD `find` features |
+| [`tests/gnu`](/tests/gnu) | GNU `find` features |
+| [`tests/common`](/tests/common) | Features common to BSD and GNU `find` |
+| [`tests/bfs`](/tests/bfs) | `bfs`-specific tests |
+
+Both new features and bug fixes should have associated tests.
+To add a test, create a new `*.sh` file in the appropriate group.
+Snapshot tests use the `bfs_diff` function to automatically compare the generated and expected outputs.
+For example,
+
+```bash
+# posix/something.sh
+bfs_diff basic -name something
+```
+
+`basic` is one of the directory trees generated for test cases; others include `links`, `loops`, `deep`, and `rainbow`.
+
+Run `./tests/tests.sh posix/something --update` to generate the reference snapshot (and don't forget to `git add` it).
diff --git a/docs/RELATED.md b/docs/RELATED.md
new file mode 100644
index 0000000..6e7bd38
--- /dev/null
+++ b/docs/RELATED.md
@@ -0,0 +1,43 @@
+# Related utilities
+
+There are many tools that can be used to find files.
+This is a catalogue of some of the most important/interesting ones.
+
+## `find`-compatible
+
+### System `find` implementations
+
+These `find` implementations are commonly installed as the system `find` utility in UNIX-like operating systems:
+
+- [GNU findutils](https://www.gnu.org/software/findutils/) ([manual](https://www.gnu.org/software/findutils/manual/html_node/find_html/index.html), [source](https://git.savannah.gnu.org/cgit/findutils.git))
+- BSD `find`
+ - FreeBSD `find` ([manual](https://www.freebsd.org/cgi/man.cgi?find(1)), [source](https://cgit.freebsd.org/src/tree/usr.bin/find))
+ - OpenBSD `find` ([manual](https://man.openbsd.org/find.1), [source](https://cvsweb.openbsd.org/src/usr.bin/find/))
+ - NetBSD `find` ([manual](https://man.netbsd.org/find.1), [source](http://cvsweb.netbsd.org/bsdweb.cgi/src/usr.bin/find/))
+- macOS `find` ([manual](https://ss64.com/osx/find.html), [source](https://github.com/apple-oss-distributions/shell_cmds/tree/main/find))
+- Solaris `find`
+ - [Illumos](https://illumos.org/) `find` ([manual](https://illumos.org/man/1/find), [source](https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/find/find.c))
+
+### Alternative `find` implementations
+
+These are not usually installed as the system `find`, but are designed to be `find`-compatible
+
+- [`bfs`](https://tavianator.com/projects/bfs.html) ([manual](https://man.archlinux.org/man/bfs.1), [source](https://github.com/tavianator/bfs))
+- [schilytools](https://codeberg.org/schilytools/schilytools) `sfind` ([source](https://codeberg.org/schilytools/schilytools/src/branch/master/sfind))
+- [BusyBox](https://busybox.net/) `find` ([manual](https://busybox.net/downloads/BusyBox.html#find), [source](https://git.busybox.net/busybox/tree/findutils/find.c))
+- [ToyBox](https://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c))
+- [Heirloom Project](https://heirloom.sourceforge.net/) `find` ([manual](https://heirloom.sourceforge.net/man/find.1.html), [source](https://github.com/eunuchs/heirloom-project/blob/master/heirloom/heirloom/find/find.c))
+- [uutils](https://uutils.github.io/) `find` ([source](https://github.com/uutils/findutils))
+
+## `find` alternatives
+
+These utilities are not `find`-compatible, but serve a similar purpose:
+
+- [`fd`](https://github.com/sharkdp/fd): A simple, fast and user-friendly alternative to 'find'
+- `locate`
+ - [GNU `locate`](https://www.gnu.org/software/findutils/locate)
+ - [`mlocate`](https://pagure.io/mlocate) ([manual](), [source](https://pagure.io/mlocate/tree/master))
+ - [`plocate`](https://plocate.sesse.net/) ([manual](https://plocate.sesse.net/plocate.1.html), [source](https://git.sesse.net/?p=plocate))
+- [`walk`](https://github.com/google/walk): Plan 9 style utilities to replace find(1)
+- [fselect](https://github.com/jhspetersson/fselect): Find files with SQL-like queries
+- [rawhide](https://github.com/raforg/rawhide): find files using pretty C expressions
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..dd3277a
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,126 @@
+Security
+========
+
+Threat model
+------------
+
+`bfs` is a command line program running on multi-user operating systems.
+Those other users may be malicious, but `bfs` should not allow them to do anything they couldn't already do.
+That includes situations where one user (especially `root`) is running `bfs` on files owned or controlled by another user.
+
+On the other hand, `bfs` implicitly trusts the user running it.
+Anyone with enough control over the command line of `bfs` or any `find`-compatible tool can wreak havoc with dangerous actions like `-exec`, `-delete`, etc.
+
+> [!CAUTION]
+> The only untrusted input that should *ever* be passed on the `bfs` command line are **file paths**.
+> It is *always* unsafe to allow *any* other part of the command line to be affected by untrusted input.
+> Use the `-f` flag, or `-files0-from`, to ensure that the input is interpreted as a path.
+
+This still has security implications, including:
+
+- **Information disclosure:** an attacker may learn whether particular files exist by observing `bfs`'s output, exit status, or even side channels like execution time.
+- **Denial of service:** large directory trees or slow/network storage may cause `bfs` to consume excessive system resources.
+
+> [!TIP]
+> When in doubt, do not pass any untrusted input to `bfs`.
+
+
+Executing commands
+------------------
+
+The `-exec` family of actions execute commands, passing the matched paths as arguments.
+File names that begin with a dash may be misinterpreted as options, so `bfs` adds a leading `./` in some instances:
+
+```console
+user@host$ bfs -execdir echo {} \;
+./-rf
+```
+
+This might save you from accidentally running `rm -rf` (for example) when you didn't mean to.
+This mitigation applies to `-execdir`, but not `-exec`, because the full path typically does not begin with a dash.
+But it is possible, so be careful:
+
+```console
+user@host$ bfs -f -rf -exec echo {} \;
+-rf
+```
+
+
+Race conditions
+---------------
+
+Like many programs that interface with the file system, `bfs` can be affected by race conditions&mdash;in particular, "[time-of-check to time-of-use](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use)" (TOCTTOU) issues.
+For example,
+
+```console
+user@host$ bfs / -user user -exec dangerous_command {} \;
+```
+
+is not guaranteed to only run `dangerous_command` on files you own, because another user may run
+
+```console
+evil@host$ mv /path/to/file /path/to/exile
+evil@host$ mv ~/malicious /path/to/file
+```
+
+in between checking `-user user` and executing the command.
+
+> [!WARNING]
+> Be careful when running `bfs` on directories that other users have write access to, because they can modify the directory tree while `bfs` is running, leading to unpredictable results and possible TOCTTOU issues.
+
+
+Output sanitization
+-------------------
+
+In general, printing arbitrary data to a terminal may have [security](https://hdm.io/writing/termulation.txt) [implications](https://dgl.cx/2023/09/ansi-terminal-security#vulnerabilities-using-known-replies).
+On many platforms, file paths may be completely arbitrary data (except for NUL (`\0`) bytes).
+Therefore, when `bfs` is writing output to a terminal, it will escape non-printable characters:
+
+<pre>
+user@host$ touch $'\e[1mBOLD\e[0m'
+user@host$ bfs
+.
+./$'\e[1mBOLD\e[0m'
+</pre>
+
+However, this is fragile as it only applies when outputting directly to a terminal:
+
+<pre>
+user@host$ bfs | grep BOLD
+<strong>BOLD</strong>
+</pre>
+
+
+Code quality
+------------
+
+Every correctness issue in `bfs` is a potential security issue, because acting on the wrong path may do arbitrarily bad things.
+For example:
+
+```console
+root@host# bfs /etc -name passwd -exec cat {} \;
+```
+
+should print `/etc/passwd` but not `/etc/shadow`.
+`bfs` tries to ensure correct behavior through careful programming practice, an extensive testsuite, and static analysis.
+
+`bfs` is written in C, which is a memory unsafe language.
+Bugs that lead to memory corruption are likely to be exploitable due to the nature of C.
+We use [sanitizers](https://github.com/google/sanitizers) to try to detect these bugs.
+Fuzzing has also been applied in the past, and deploying continuous fuzzing is a work in progress.
+
+
+Supported versions
+------------------
+
+`bfs` comes with [no warranty](/LICENSE), and is maintained by [me](https://tavianator.com/) and [other volunteers](https://github.com/tavianator/bfs/graphs/contributors) in our spare time.
+In that sense, there are no *supported* versions.
+However, as long as I maintain `bfs` I will attempt to address any security issues swiftly.
+In general, security fixes will be part of the latest release, though for significant issues I may backport fixes to older release series.
+
+
+Reporting a vulnerability
+-------------------------
+
+If you think you have found a sensitive security issue in `bfs`, you can [report it privately](https://github.com/tavianator/bfs/security/advisories/new).
+Or you can [report it publicly](https://github.com/tavianator/bfs/issues/new); I won't judge you.
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..16aeaf6
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,192 @@
+Using `bfs`
+===========
+
+`bfs` has the same command line syntax as `find`, and almost any `find` command that works with a major `find` implementation will also work with `bfs`.
+When invoked with no arguments, `bfs` will list everything under the current directory recursively, breadth-first:
+
+```console
+$ bfs
+.
+./LICENSE
+./Makefile
+./README.md
+./completions
+./docs
+./src
+./tests
+./completions/bfs.bash
+./completions/bfs.zsh
+./docs/BUILDING.md
+./docs/CHANGELOG.md
+./docs/CONTRIBUTING.md
+./docs/USAGE.md
+./docs/bfs.1
+...
+```
+
+
+Paths
+-----
+
+Arguments that don't begin with `-` are treated as paths to search.
+If one or more paths are specified, they are used instead of the current directory:
+
+```console
+$ bfs /usr/bin /usr/lib
+/usr/bin
+/usr/lib
+/usr/bin/bfs
+...
+/usr/lib/libc.so
+...
+```
+
+
+Expressions
+-----------
+
+Arguments that start with `-` form an *expression* which `bfs` evaluates to filter the matched files, and to do things with the files that match.
+The most common expression is probably `-name`, which matches filenames against a glob pattern:
+
+```console
+$ bfs -name '*.md'
+./README.md
+./docs/BUILDING.md
+./docs/CHANGELOG.md
+./docs/CONTRIBUTING.md
+./docs/USAGE.md
+```
+
+### Operators
+
+When you put multiple expressions next to each other, both of them must match:
+
+```console
+$ bfs -name '*.md' -name '*ING*'
+./docs/BUILDING.md
+./docs/CONTRIBUTING.md
+```
+
+This works because the expressions are implicitly combined with *logical and*.
+You could be explicit by writing
+
+```console
+$ bfs -name '*.md' -and -name '*ING'`
+```
+
+There are other operators like `-or`:
+
+```console
+$ bfs -name '*.md' -or -name 'bfs.*'
+./README.md
+./completions/bfs.bash
+./completions/bfs.fish
+./completions/bfs.zsh
+./docs/BUILDING.md
+./docs/CHANGELOG.md
+./docs/CONTRIBUTING.md
+./docs/USAGE.md
+./docs/bfs.1
+```
+
+and `-not`:
+
+```console
+$ bfs -name '*.md' -and -not -name '*ING*'
+./README.md
+./docs/CHANGELOG.md
+./docs/USAGE.md
+```
+
+### Actions
+
+Every `bfs` expression returns either `true` or `false`.
+For expressions like `-name`, that's all they do.
+But some expressions, called *actions*, have other side effects.
+
+If no actions are included in the expression, `bfs` adds the `-print` action automatically, which is why the above examples actually print any output.
+The default `-print` is suppressed if any actions are given explicitly.
+Available actions include printing with alternate formats (`-ls`, `-printf`, etc.), executing commands (`-exec`, `-execdir`, etc.), deleting files (`-delete`), and stopping the search (`-quit`, `-exit`).
+
+
+Extensions
+----------
+
+`bfs` implements a few extensions not found in other `find` implementations.
+
+### `-exclude`
+
+The `-exclude` operator skips an entire subtree whenever an expression matches.
+For example, `-exclude -name .git` will exclude any files or directories named `.git` from the search results.
+`-exclude` is easier to use than the standard `-prune` action; compare
+
+ bfs -name config -exclude -name .git
+
+to the equivalent
+
+ find ! \( -name .git -prune \) -name config
+
+Unlike `-prune`, `-exclude` even works in combination with `-depth`/`-delete`.
+
+---
+
+### `-limit`
+
+The `-limit N` action makes `bfs` quit once it gets evaluated `N` times.
+Placing it after an action like `-print` limits the number of results that get printed, for example:
+
+```console
+$ bfs -s -type f -name '*.txt'
+./1.txt
+./2.txt
+./3.txt
+./4.txt
+$ bfs -s -type f -name '*.txt' -print -limit 2
+./1.txt
+./2.txt
+```
+
+This is similar to
+
+```console
+$ bfs -s -type f -name '*.txt' | head -n2
+```
+
+but more powerful because you can apply separate limits to different expressions:
+
+```console
+$ bfs \( -name '*.txt' -print -limit 3 -o -name '*.log' -print -limit 4 \) -limit 5
+[At most 3 .txt files, at most 4 .log files, and at most 5 in total]
+```
+
+and more efficient because it will quit immediately.
+When piping to `head`, `bfs` will only quit *after* it tries to output too many results.
+
+---
+
+### `-hidden`/`-nohidden`
+
+`-hidden` matches "hidden" files (dotfiles).
+`bfs -hidden` is effectively shorthand for
+
+ find \( -name '.*' -not -name . -not -name .. \)
+
+`-nohidden` is equivalent to `-exclude -hidden`.
+
+---
+
+### `-unique`
+
+This option ensures that `bfs` only visits each file once, even if it's reachable through multiple hard or symbolic links.
+It's particularly useful when following symbolic links (`-L`).
+
+---
+
+### `-color`/`-nocolor`
+
+When printing to a terminal, `bfs` automatically colors paths like GNU `ls`, according to the `LS_COLORS` environment variable.
+The `-color` and `-nocolor` options override the automatic behavior, which may be handy when you want to preserve colors through a pipe:
+
+ bfs -color | less -R
+
+If the [`NO_COLOR`](https://no-color.org/) environment variable is set, colors will be disabled by default.
diff --git a/bfs.1 b/docs/bfs.1
index a132214..c6141a6 100644
--- a/bfs.1
+++ b/docs/bfs.1
@@ -1,4 +1,6 @@
-.TH BFS 1
+.\" Copyright © Tavian Barnes <tavianator@tavianator.com>
+.\" SPDX-License-Identifier: 0BSD
+.TH BFS 1 2025-06-15 "bfs 4.0.8"
.SH NAME
bfs \- breadth-first search for your files
.SH SYNOPSIS
@@ -41,17 +43,17 @@ For example,
.PP
.nf
.RS
-.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-and \-print
+.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-and \-print
.RE
.fi
.PP
-will print the all the paths that are either .txt files or symbolic links to .txt files.
+will print all the paths that are either .txt files or symbolic links to .txt files.
.B \-and
is implied between two consecutive expressions, so this is equivalent:
.PP
.nf
.RS
-.B bfs \\\( \-name '*.txt' \-or \-lname '*.txt' \\\\) \-print
+.B bfs \e( \-name '*.txt' \-or \-lname '*.txt' \e) \-print
.RE
.fi
.PP
@@ -71,7 +73,7 @@ will also accept
.I \-N
or
.IR +N .
-.IR \-N
+.I \-N
means "less than
.IR N ,"
and
@@ -90,7 +92,9 @@ Follow all symbolic links.
Never follow symbolic links (the default).
.TP
.B \-E
-Use extended regular expressions (same as \fB\-regextype posix-extended\fR).
+Use extended regular expressions (same as
+.B \-regextype
+.IR posix-extended ).
.TP
.B \-X
Filter out files with
@@ -109,20 +113,20 @@ The sorting takes place within each directory separately, which makes it differe
but still provides a deterministic ordering.
.TP
.B \-x
-Don't descend into other mount points (same as \fB\-xdev\fR).
+Don't descend into other mount points (same as
+.BR \-xdev ).
.TP
-\fB\-f \fIPATH\fR
+.BI "\-f " PATH
Treat
.I PATH
as a path to search (useful if it begins with a dash).
-.PP
.TP
-\fB\-D \fIFLAG\fR
+.BI "\-D " FLAG
Turn on a debugging flag (see
.B \-D
.IR help ).
.PP
-\fB\-O\fIN\fR
+.BI \-O N
.RS
Enable optimization level
.I N
@@ -171,36 +175,49 @@ consumes too much memory.
.TP
.I eds
Exponential deepening search.
-A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g 0-1, 1-2, 2-4, 4-8, etc.).
+A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g. 0-1, 1-2, 2-4, 4-8, etc.).
Provides many of the benefits of breadth-first search with depth-first's reduced memory consumption.
Typically far faster than
.B \-S
.IR ids .
.RE
+.TP
+.BI \-j N
+Search with
+.I N
+threads in parallel (default: number of CPUs, up to
+.IR 8 ).
.SH OPERATORS
.TP
-\fB( \fIexpression \fB)\fR
+.BI "( " expression " )"
Parentheses are used for grouping expressions together.
You'll probably have to write
-.B \\\\(
+.B \e(
.I expression
-.B \\\\)
+.B \e)
to avoid the parentheses being interpreted by the shell.
.PP
\fB! \fIexpression\fR
.br
-\fB\-not \fIexpression\fR
+.B \-not
+.I expression
.RS
The "not" operator: returns the negation of the truth value of the
.IR expression .
-You may have to write \fB\\! \fIexpression\fR to avoid \fB!\fR being interpreted by the shell.
+You may have to write \fB\e! \fIexpression\fR to avoid
+.B !
+being interpreted by the shell.
.RE
.PP
-\fIexpression\fR \fIexpression\fR
+.I expression expression
.br
-\fIexpression \fB\-a \fIexpression\fR
+.I expression
+.B \-a
+.I expression
.br
-\fIexpression \fB\-and \fIexpression\fR
+.I expression
+.B \-and
+.I expression
.RS
Short-circuiting "and" operator: if the left-hand
.I expression
@@ -212,9 +229,13 @@ otherwise, returns
.BR false .
.RE
.PP
-\fIexpression \fB\-o \fIexpression\fR
+.I expression
+.B \-o
+.I expression
.br
-\fIexpression \fB\-or \fIexpression\fR
+.I expression
+.B \-or
+.I expression
.RS
Short-circuiting "or" operator: if the left-hand
.I expression
@@ -226,14 +247,14 @@ otherwise, returns
.BR true .
.RE
.TP
-\fIexpression \fB, \fIexpression\fR
+.IB "expression " , " expression"
The "comma" operator: evaluates the left-hand
.I expression
but discards the result, returning the right-hand
.IR expression .
.SH SPECIAL FORMS
.TP
-\fB\-exclude \fIexpression\fR
+.BI "\-exclude " expression
Exclude all paths matching the
.I expression
from the search.
@@ -245,8 +266,21 @@ or
.B \-mindepth
for example.
Exclusions are always applied before other expressions, so it may be least confusing to put them first on the command line.
-.SH OPTIONS
.PP
+.B \-help
+.br
+.B \-\-help
+.RS
+Print usage information, and exit immediately (without parsing the rest of the command line or processing any files).
+.RE
+.PP
+.B \-version
+.br
+.B \-\-version
+.RS
+Print version information, and exit immediately.
+.RE
+.SH OPTIONS
.B \-color
.br
.B \-nocolor
@@ -268,8 +302,8 @@ Search in post-order (descendents first).
Follow all symbolic links (same as
.BR \-L ).
.TP
-\fB\-files0\-from \fIFILE\fR
-Treat the NUL ('\\0')-separated paths in
+.BI "\-files0\-from " FILE
+Treat the NUL ('\e0')-separated paths in
.I FILE
as starting points for the search.
Pass
@@ -277,9 +311,9 @@ Pass
.I \-
to read the paths from standard input.
.PP
-\fB\-ignore_readdir_race\fR
+.B \-ignore_readdir_race
.br
-\fB\-noignore_readdir_race\fR
+.B \-noignore_readdir_race
.RS
Whether to report an error if
.B bfs
@@ -287,18 +321,21 @@ detects that the file tree is modified during the search (default:
.BR \-noignore_readdir_race ).
.RE
.PP
-\fB\-maxdepth \fIN\fR
+.B \-maxdepth
+.I N
.br
-\fB\-mindepth \fIN\fR
+.B \-mindepth
+.I N
.RS
Ignore files deeper/shallower than
.IR N .
.RE
.TP
.B \-mount
-Don't descend into other mount points (same as
-.B \-xdev
-for now, but will skip mount points entirely in the future).
+Exclude mount points entirely from the results.
+.TP
+.B \-noerror
+Ignore any errors that occur during traversal.
.TP
.B \-nohidden
Exclude hidden files and directories.
@@ -306,14 +343,43 @@ Exclude hidden files and directories.
.B \-noleaf
Ignored; for compatibility with GNU find.
.TP
-\fB\-regextype \fITYPE\fR
+.BI "\-regextype " TYPE
Use
.IR TYPE -flavored
-regexes (default:
-.IR posix-basic ;
-see
-.B \-regextype
-.IR help ).
+regular expressions.
+The possible types are
+.RS
+.TP
+.I posix-basic
+POSIX basic regular expressions (the default).
+.TP
+.I posix-extended
+POSIX extended regular expressions.
+.TP
+.I ed
+Like
+.BR ed (1)
+(same as
+.IR posix-basic ).
+.TP
+.I emacs
+Like
+.BR emacs (1).
+.TP
+.I grep
+Like
+.BR grep (1).
+.TP
+.I sed
+Like
+.BR sed (1)
+(same as
+.IR posix-basic ).
+.PP
+See
+.BR regex (7)
+for a description of regular expression syntax.
+.RE
.TP
.B \-status
Display a status bar while searching.
@@ -332,6 +398,9 @@ Turn on or off warnings about the command line.
.TP
.B \-xdev
Don't descend into other mount points.
+Unlike
+.BR \-mount ,
+the mount point itself is still included.
.SH TESTS
.TP
.B \-acl
@@ -352,13 +421,17 @@ Find files
minutes ago.
.RE
.PP
-\fB\-anewer \fIFILE\fR
+.B \-anewer
+.I FILE
.br
-\fB\-Bnewer \fIFILE\fR
+.B \-Bnewer
+.I FILE
.br
-\fB\-cnewer \fIFILE\fR
+.B \-cnewer
+.I FILE
.br
-\fB\-mnewer \fIFILE\fR
+.B \-mnewer
+.I FILE
.RS
Find files
.BR a ccessed/ B irthed/ c hanged/ m odified
@@ -367,13 +440,17 @@ more recently than
was modified.
.RE
.PP
-\fB\-asince \fITIME\fR
+.B \-asince
+.I TIME
.br
-\fB\-Bsince \fITIME\fR
+.B \-Bsince
+.I TIME
.br
-\fB\-csince \fITIME\fR
+.B \-csince
+.I TIME
.br
-\fB\-msince \fITIME\fR
+.B \-msince
+.I TIME
.RS
Find files
.BR a ccessed/ B irthed/ c hanged/ m odified
@@ -403,6 +480,10 @@ Find files with POSIX.1e
.BR capabilities (7)
set.
.TP
+.BI "\-context " GLOB
+Find files whose SELinux context matches the
+.IR GLOB .
+.TP
\fB\-depth\fR [\fI\-+\fR]\fIN\fR
Find files with depth
.IR N .
@@ -426,9 +507,13 @@ Find files the current user can execute/read/write.
Always false/true.
.RE
.TP
-.B \-fstype TYPE
+\fB\-flags\fR [\fI\-+\fR]\fIFLAGS\fR
+Find files with matching inode
+.BR FLAGS .
+.TP
+.BI "\-fstype " TYPE
Find files on file systems with the given
-.BR TYPE .
+.IR TYPE .
.PP
\fB\-gid\fR [\fI\-+\fR]\fIN\fR
.br
@@ -438,9 +523,11 @@ Find files owned by group/user ID
.IR N .
.RE
.PP
-\fB\-group \fINAME\fR
+.B \-group
+.I NAME
.br
-\fB\-user \fINAME\fR
+.B \-user
+.I NAME
.RS
Find files owned by the group/user
.IR NAME .
@@ -450,15 +537,20 @@ Find files owned by the group/user
Find hidden files (those beginning with
.IR . ).
.PP
-\fB\-ilname \fIGLOB\fR
+.B \-ilname
+.I GLOB
.br
-\fB\-iname \fIGLOB\fR
+.B \-iname
+.I GLOB
.br
-\fB\-ipath \fIGLOB\fR
+.B \-ipath
+.I GLOB
.br
-\fB\-iregex \fIREGEX\fR
+.B \-iregex
+.I REGEX
.br
-\fB\-iwholename \fIGLOB\fR
+.B \-iwholename
+.I GLOB
.RS
Case-insensitive versions of
.BR \-lname / \-name / \-path / \-regex / \-wholename .
@@ -473,19 +565,19 @@ Find files with
.I N
hard links.
.TP
-\fB\-lname \fIGLOB\fR
+.BI "\-lname " GLOB
Find symbolic links whose target matches the
.IR GLOB .
.TP
-\fB\-name \fIGLOB\fR
+.BI "\-name " GLOB
Find files whose name matches the
.IR GLOB .
.TP
-\fB\-newer \fIFILE\fR
+.BI "\-newer " FILE
Find files newer than
.IR FILE .
.TP
-\fB\-newer\fIXY \fIREFERENCE\fR
+.BI \-newer "XY REFERENCE"
Find files whose
.I X
time is newer than the
@@ -506,13 +598,12 @@ to parse
as an ISO 8601-style timestamp. For example:
.PP
.RS
-1991-12-14
-.br
-1991-12-14T03:00
-.br
-1991-12-14T03:00-07:00
-.br
-1991-12-14T10:00Z
+.nf
+\(bu \fI1991-12-14\fR
+\(bu \fI1991-12-14T03:00\fR
+\(bu \fI1991-12-14T03:00-07:00\fR
+\(bu '\fI1991-12-14 10:00Z\fR'
+.fi
.RE
.PP
.B \-nogroup
@@ -522,26 +613,28 @@ as an ISO 8601-style timestamp. For example:
Find files owned by nonexistent groups/users.
.RE
.PP
-\fB\-path \fIGLOB\fR
+.B \-path
+.I GLOB
.br
-\fB\-wholename \fIGLOB\fR
+.B \-wholename
+.I GLOB
.RS
Find files whose entire path matches the
.IR GLOB .
.RE
.TP
-\fB\-perm\fR [\fI\-\fR]\fIMODE\fR
+\fB\-perm\fR [\fI\-+/\fR]\fIMODE\fR
Find files with a matching mode.
.TP
-\fB\-regex \fIREGEX\fR
+.BI "\-regex " REGEX
Find files whose entire path matches the regular expression
.IR REGEX .
.TP
-\fB\-samefile \fIFILE\fR
+.BI "\-samefile " FILE
Find hard links to
.IR FILE .
.TP
-\fB\-since \fITIME\fR
+.BI "\-since " TIME
Find files modified since the ISO 8601-style timestamp
.IR TIME .
See
@@ -549,35 +642,67 @@ See
for examples of the timestamp format.
.TP
\fB\-size\fR [\fI\-+\fR]\fIN\fR[\fIcwbkMGTP\fR]
-Find files with the given size, in 1-byte
-.IR c haracters,
-2-byte
-.IR w ords,
-512-byte
-.IR b locks,
-(default) or
-.IR k iB/ M iB/ G iB/ T iB/ P iB.
+Find files with the given size.
+The unit can be one of
+.PP
+.RS
+.nf
+\(bu \fIc\fRhars (1 byte)
+\(bu \fIw\fRords (2 bytes)
+\(bu \fIb\fRlocks (512 bytes, the default)
+\(bu \fIk\fRiB (1024 bytes)
+\(bu \fIM\fRiB (1024 kiB)
+\(bu \fIG\fRiB (1024 MiB)
+\(bu \fIT\fRiB (1024 GiB)
+\(bu \fIP\fRiB (1024 TiB)
+.fi
+.RE
.TP
.B \-sparse
Find files that occupy fewer disk blocks than expected.
.TP
\fB\-type\fR [\fIbcdlpfswD\fR]
Find files of the given type.
-Possible types are
+The possible types are
+.PP
+.RS
+\(bu
.IR b lock
-device,
+device
+.br
+\(bu
.IR c haracter
-device,
-.IR d irectory,
-symbolic
-.IR l ink,
-.IR p ipe,
-regular
-.IR f ile,
-.IR s ocket,
-.IR w hiteout,
-and
-.IR D oor.
+device
+.br
+\(bu
+.IR d irectory
+.br
+\(bu
+.IR l ink
+(symbolic)
+.br
+\(bu
+.IR p ipe
+.br
+\(bu
+.IR f ile
+(regular)
+.br
+\(bu
+.IR s ocket
+.br
+\(bu
+.IR w hiteout
+.br
+\(bu
+.IR D oor
+.PP
+Multiple types can be given at once, separated by commas.
+For example,
+.B \-type
+.I d,f
+matches both directories and regular files.
+.RE
.TP
\fB\-used\fR [\fI\-+\fR]\fIN\fR
Find files last accessed
@@ -588,7 +713,7 @@ days after they were changed.
Find files with extended attributes
.RB ( xattr (7)).
.TP
-\fB\-xattrname\fR \fINAME\fR
+.BI "\-xattrname " NAME
Find files with the extended attribute
.IR NAME .
.TP
@@ -597,28 +722,31 @@ Find files of the given type, following links when
.B \-type
would not, and vice versa.
.SH ACTIONS
-.PP
.B \-delete
.br
.B \-rm
.RS
-Delete any found files (implies \fB-depth\fR).
+Delete any found files (implies
+.BR \-depth ).
.RE
.TP
-\fB\-exec \fIcommand ... {} ;\fR
+.BI "\-exec " "command ... {} ;"
Execute a command.
.TP
-\fB\-exec \fIcommand ... {} +\fR
+.BI "\-exec " "command ... {} +"
Execute a command with multiple files at once.
.TP
-\fB\-ok \fIcommand ... {} ;\fR
+.BI "\-ok " "command ... {} ;"
Prompt the user whether to execute a command.
.PP
-\fB\-execdir \fIcommand ... {} ;\fR
+.B \-execdir
+.I command ... {} ;
.br
-\fB\-execdir \fIcommand ... {} +\fR
+.B \-execdir
+.I command ... {} +
.br
-\fB\-okdir \fIcommand ... {} ;\fR
+.B \-okdir
+.I command ... {} ;
.RS
Like
.BR \-exec / \-ok ,
@@ -626,15 +754,21 @@ but run the command in the same directory as the found file(s).
.RE
.TP
\fB\-exit\fR [\fISTATUS\fR]
-Exit immediately with the given status (0 if unspecified).
+Exit immediately with the given status
+.RI ( 0
+if unspecified).
.PP
-\fB\-fls \fIFILE\fR
+.B \-fls
+.I FILE
.br
-\fB\-fprint \fIFILE\fR
+.B \-fprint
+.I FILE
.br
-\fB\-fprint0 \fIFILE\fR
+.B \-fprint0
+.I FILE
.br
-\fB\-fprintf \fIFILE FORMAT\fR
+.B \-fprintf
+.I FILE FORMAT
.RS
Like
.BR \-ls / \-print / \-print0 / \-printf ,
@@ -643,6 +777,11 @@ but write to
instead of standard output.
.RE
.TP
+.BI "\-limit " N
+Quit once this action is evaluated
+.I N
+times.
+.TP
.B \-ls
List files like
.B ls
@@ -654,12 +793,12 @@ Print the path to the found file.
.B \-print0
Like
.BR \-print ,
-but use the null character ('\\0') as a separator rather than newlines.
+but use the null character ('\e0') as a separator rather than newlines.
Useful in conjunction with
.B xargs
.IR \-0 .
.TP
-\fB\-printf \fIFORMAT\fR
+.BI "\-printf " FORMAT
Print according to a format string (see
.BR find (1)).
These additional format directives are supported:
@@ -689,15 +828,16 @@ instead.
.TP
.B \-prune
Don't descend into this directory.
+This has no effect if
+.B \-depth
+is enabled (either explicitly, or implicitly by
+.BR \-delete ).
+Use
+.B \-exclude
+instead in that case.
.TP
.B \-quit
Quit immediately.
-.TP
-.B \-version
-Print version information.
-.TP
-.B \-help
-Print usage information.
.SH ENVIRONMENT
Certain environment variables affect the behavior of
.BR bfs .
@@ -715,16 +855,26 @@ Yes/no prompts (e.g. from
.BR \-ok )
will also be interpreted according to the current locale.
.RE
-.TP
+.PP
.B LS_COLORS
+.br
+.B BFS_COLORS
+.RS
Controls the colors used when displaying file paths if
.B \-color
is enabled.
.B bfs
-interprets this environment variable is interpreted the same way GNU
+interprets
+.B LS_COLORS
+the same way GNU
.BR ls (1)
does (see
.BR dir_colors (5)).
+.B BFS_COLORS
+can be used to customize
+.B bfs
+without affecting other commands.
+.RE
.TP
.B NO_COLOR
Causes
@@ -738,17 +888,48 @@ Specifies the pager used for
.B \-help
output.
Defaults to
+.BR less (1),
+if found on the current
+.BR PATH ,
+otherwise
.BR more (1).
.TP
+.B PATH
+Used to resolve executables for
+.BR \-exec [ dir ]
+and
+.BR \-ok [ dir ].
+.TP
.B POSIXLY_CORRECT
Makes
.B bfs
conform more strictly to the POSIX.1-2017 specification for
.BR find (1).
-Currently this just disables warnings by default.
+Currently this has two effects:
+.RS
+.IP \(bu
+Disables warnings by default, because POSIX prohibits writing to standard error (except for the
+.B \-ok
+prompt), unless the command also fails with a non-zero exit status.
+.IP \(bu
+Makes
+.B \-ls
+and
+.B \-fls
+use 512-byte blocks instead of 1024-byte blocks.
+(POSIX does not specify these actions, but BSD
+.BR find (1)
+implementations use 512-byte blocks, while GNU
+.BR find (1)
+uses 1024-byte blocks by default.)
+.PP
It does not disable
.BR bfs 's
various extensions to the base POSIX functionality.
+.B POSIXLY_CORRECT
+has the same effects on GNU
+.BR find (1).
+.RE
.SH EXAMPLES
.TP
.B bfs
@@ -763,7 +944,7 @@ is quoted to ensure the glob is processed by
.B bfs
rather than the shell.
.TP
-\fBbfs \-name access_log \-L \fI/var\fR
+.BI "bfs \-name access_log \-L " /var
Finds all files named
.B access_log
under
@@ -772,7 +953,7 @@ following symbolic links.
.B bfs
allows flags and paths to appear anywhere on the command line.
.TP
-\fBbfs \fI~ \fB\-not \-user $USER\fR
+.BI "bfs " ~ " \-not \-user $USER"
Prints all files in your home directory not owned by you.
.TP
.B bfs \-xtype l
@@ -780,12 +961,12 @@ Finds broken symbolic links.
.TP
.B bfs \-name config \-exclude \-name .git
Finds all files named
-.BR config,
+.BR config ,
skipping every
.B .git
directory.
.TP
-.B bfs \-type f \-executable \-exec strip '{}' +
+.B bfs \-type f \-executable \-exec strip {} +
Runs
.BR strip (1)
on all executable files it finds, passing it multiple files at a time.
diff --git a/dstring.c b/dstring.c
deleted file mode 100644
index 58e74fe..0000000
--- a/dstring.c
+++ /dev/null
@@ -1,215 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "dstring.h"
-#include <assert.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-/**
- * The memory representation of a dynamic string. Users get a pointer to data.
- */
-struct dstring {
- size_t capacity;
- size_t length;
- char data[];
-};
-
-/** Get the string header from the string data pointer. */
-static struct dstring *dstrheader(const char *dstr) {
- return (struct dstring *)(dstr - offsetof(struct dstring, data));
-}
-
-/** Get the correct size for a dstring with the given capacity. */
-static size_t dstrsize(size_t capacity) {
- return BFS_FLEX_SIZEOF(struct dstring, data, capacity + 1);
-}
-
-/** Allocate a dstring with the given contents. */
-static char *dstralloc_impl(size_t capacity, size_t length, const char *data) {
- struct dstring *header = malloc(dstrsize(capacity));
- if (!header) {
- return NULL;
- }
-
- header->capacity = capacity;
- header->length = length;
-
- memcpy(header->data, data, length);
- header->data[length] = '\0';
- return header->data;
-}
-
-char *dstralloc(size_t capacity) {
- return dstralloc_impl(capacity, 0, "");
-}
-
-char *dstrdup(const char *str) {
- size_t len = strlen(str);
- return dstralloc_impl(len, len, str);
-}
-
-char *dstrndup(const char *str, size_t n) {
- size_t len = strnlen(str, n);
- return dstralloc_impl(len, len, str);
-}
-
-size_t dstrlen(const char *dstr) {
- return dstrheader(dstr)->length;
-}
-
-int dstreserve(char **dstr, size_t capacity) {
- struct dstring *header = dstrheader(*dstr);
-
- if (capacity > header->capacity) {
- capacity *= 2;
-
- header = realloc(header, dstrsize(capacity));
- if (!header) {
- return -1;
- }
- header->capacity = capacity;
-
- *dstr = header->data;
- }
-
- return 0;
-}
-
-int dstresize(char **dstr, size_t length) {
- if (dstreserve(dstr, length) != 0) {
- return -1;
- }
-
- struct dstring *header = dstrheader(*dstr);
- header->length = length;
- header->data[length] = '\0';
-
- return 0;
-}
-
-/** Common implementation of dstr{cat,ncat,app}. */
-static int dstrcat_impl(char **dest, const char *src, size_t srclen) {
- size_t oldlen = dstrlen(*dest);
- size_t newlen = oldlen + srclen;
-
- if (dstresize(dest, newlen) != 0) {
- return -1;
- }
-
- memcpy(*dest + oldlen, src, srclen);
- return 0;
-}
-
-int dstrcat(char **dest, const char *src) {
- return dstrcat_impl(dest, src, strlen(src));
-}
-
-int dstrncat(char **dest, const char *src, size_t n) {
- return dstrcat_impl(dest, src, strnlen(src, n));
-}
-
-int dstrdcat(char **dest, const char *src) {
- return dstrcat_impl(dest, src, dstrlen(src));
-}
-
-int dstrapp(char **str, char c) {
- return dstrcat_impl(str, &c, 1);
-}
-
-char *dstrprintf(const char *format, ...) {
- va_list args;
-
- va_start(args, format);
- char *str = dstrvprintf(format, args);
- va_end(args);
-
- return str;
-}
-
-char *dstrvprintf(const char *format, va_list args) {
- // Guess a capacity to try to avoid reallocating
- char *str = dstralloc(2*strlen(format));
- if (!str) {
- return NULL;
- }
-
- if (dstrvcatf(&str, format, args) != 0) {
- dstrfree(str);
- return NULL;
- }
-
- return str;
-}
-
-int dstrcatf(char **str, const char *format, ...) {
- va_list args;
-
- va_start(args, format);
- int ret = dstrvcatf(str, format, args);
- va_end(args);
-
- return ret;
-}
-
-int dstrvcatf(char **str, const char *format, va_list args) {
- // Guess a capacity to try to avoid calling vsnprintf() twice
- size_t len = dstrlen(*str);
- dstreserve(str, len + 2*strlen(format));
- size_t cap = dstrheader(*str)->capacity;
-
- va_list copy;
- va_copy(copy, args);
-
- char *tail = *str + len;
- int ret = vsnprintf(tail, cap - len + 1, format, args);
- if (ret < 0) {
- goto fail;
- }
-
- size_t tail_len = ret;
- if (tail_len > cap - len) {
- cap = len + tail_len;
- if (dstreserve(str, cap) != 0) {
- goto fail;
- }
-
- tail = *str + len;
- ret = vsnprintf(tail, tail_len + 1, format, copy);
- if (ret < 0 || (size_t)ret != tail_len) {
- assert(!"Length of formatted string changed");
- goto fail;
- }
- }
-
- va_end(copy);
-
- struct dstring *header = dstrheader(*str);
- header->length += tail_len;
- return 0;
-
-fail:
- *tail = '\0';
- return -1;
-}
-
-void dstrfree(char *dstr) {
- if (dstr) {
- free(dstrheader(dstr));
- }
-}
diff --git a/dstring.h b/dstring.h
deleted file mode 100644
index 54106f3..0000000
--- a/dstring.h
+++ /dev/null
@@ -1,194 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A dynamic string library.
- */
-
-#ifndef BFS_DSTRING_H
-#define BFS_DSTRING_H
-
-#include "util.h"
-#include <stdarg.h>
-#include <stddef.h>
-
-/**
- * Allocate a dynamic string.
- *
- * @param capacity
- * The initial capacity of the string.
- */
-char *dstralloc(size_t capacity);
-
-/**
- * Create a dynamic copy of a string.
- *
- * @param str
- * The NUL-terminated string to copy.
- */
-char *dstrdup(const char *str);
-
-/**
- * Create a length-limited dynamic copy of a string.
- *
- * @param str
- * The string to copy.
- * @param n
- * The maximum number of characters to copy from str.
- */
-char *dstrndup(const char *str, size_t n);
-
-/**
- * Get a dynamic string's length.
- *
- * @param dstr
- * The string to measure.
- * @return The length of dstr.
- */
-size_t dstrlen(const char *dstr);
-
-/**
- * Reserve some capacity in a dynamic string.
- *
- * @param dstr
- * The dynamic string to preallocate.
- * @param capacity
- * The new capacity for the string.
- * @return 0 on success, -1 on failure.
- */
-int dstreserve(char **dstr, size_t capacity);
-
-/**
- * Resize a dynamic string.
- *
- * @param dstr
- * The dynamic string to resize.
- * @param length
- * The new length for the dynamic string.
- * @return 0 on success, -1 on failure.
- */
-int dstresize(char **dstr, size_t length);
-
-/**
- * Append to a dynamic string.
- *
- * @param dest
- * The destination dynamic string.
- * @param src
- * The string to append.
- * @return 0 on success, -1 on failure.
- */
-int dstrcat(char **dest, const char *src);
-
-/**
- * Append to a dynamic string.
- *
- * @param dest
- * The destination dynamic string.
- * @param src
- * The string to append.
- * @param n
- * The maximum number of characters to take from src.
- * @return 0 on success, -1 on failure.
- */
-int dstrncat(char **dest, const char *src, size_t n);
-
-/**
- * Append a dynamic string to another dynamic string.
- *
- * @param dest
- * The destination dynamic string.
- * @param src
- * The dynamic string to append.
- * @return
- * 0 on success, -1 on failure.
- */
-int dstrdcat(char **dest, const char *src);
-
-/**
- * Append a single character to a dynamic string.
- *
- * @param str
- * The string to append to.
- * @param c
- * The character to append.
- * @return 0 on success, -1 on failure.
- */
-int dstrapp(char **str, char c);
-
-/**
- * Create a dynamic string from a format string.
- *
- * @param format
- * The format string to fill in.
- * @param ...
- * Any arguments for the format string.
- * @return
- * The created string, or NULL on failure.
- */
-BFS_FORMATTER(1, 2)
-char *dstrprintf(const char *format, ...);
-
-/**
- * Create a dynamic string from a format string and a va_list.
- *
- * @param format
- * The format string to fill in.
- * @param args
- * The arguments for the format string.
- * @return
- * The created string, or NULL on failure.
- */
-char *dstrvprintf(const char *format, va_list args);
-
-/**
- * Format some text onto the end of a dynamic string.
- *
- * @param str
- * The destination dynamic string.
- * @param format
- * The format string to fill in.
- * @param ...
- * Any arguments for the format string.
- * @return
- * 0 on success, -1 on failure.
- */
-BFS_FORMATTER(2, 3)
-int dstrcatf(char **str, const char *format, ...);
-
-/**
- * Format some text from a va_list onto the end of a dynamic string.
- *
- * @param str
- * The destination dynamic string.
- * @param format
- * The format string to fill in.
- * @param args
- * The arguments for the format string.
- * @return
- * 0 on success, -1 on failure.
- */
-int dstrvcatf(char **str, const char *format, va_list args);
-
-/**
- * Free a dynamic string.
- *
- * @param dstr
- * The string to free.
- */
-void dstrfree(char *dstr);
-
-#endif // BFS_DSTRING_H
diff --git a/eval.h b/eval.h
deleted file mode 100644
index 533857c..0000000
--- a/eval.h
+++ /dev/null
@@ -1,113 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * The evaluation functions that implement literal expressions like -name,
- * -print, etc.
- */
-
-#ifndef BFS_EVAL_H
-#define BFS_EVAL_H
-
-#include <stdbool.h>
-
-struct bfs_ctx;
-struct expr;
-
-/**
- * Ephemeral state for evaluating an expression.
- */
-struct eval_state;
-
-/**
- * Expression evaluation function.
- *
- * @param expr
- * The current expression.
- * @param state
- * The current evaluation state.
- * @return
- * The result of the test.
- */
-typedef bool eval_fn(const struct expr *expr, struct eval_state *state);
-
-/**
- * Evaluate the command line.
- *
- * @param ctx
- * The bfs context to evaluate.
- * @return
- * EXIT_SUCCESS on success, otherwise on failure.
- */
-int bfs_eval(const struct bfs_ctx *ctx);
-
-// Predicate evaluation functions
-
-bool eval_true(const struct expr *expr, struct eval_state *state);
-bool eval_false(const struct expr *expr, struct eval_state *state);
-
-bool eval_access(const struct expr *expr, struct eval_state *state);
-bool eval_acl(const struct expr *expr, struct eval_state *state);
-bool eval_capable(const struct expr *expr, struct eval_state *state);
-bool eval_perm(const struct expr *expr, struct eval_state *state);
-bool eval_xattr(const struct expr *expr, struct eval_state *state);
-bool eval_xattrname(const struct expr *expr, struct eval_state *state);
-
-bool eval_newer(const struct expr *expr, struct eval_state *state);
-bool eval_time(const struct expr *expr, struct eval_state *state);
-bool eval_used(const struct expr *expr, struct eval_state *state);
-
-bool eval_gid(const struct expr *expr, struct eval_state *state);
-bool eval_uid(const struct expr *expr, struct eval_state *state);
-bool eval_nogroup(const struct expr *expr, struct eval_state *state);
-bool eval_nouser(const struct expr *expr, struct eval_state *state);
-
-bool eval_depth(const struct expr *expr, struct eval_state *state);
-bool eval_empty(const struct expr *expr, struct eval_state *state);
-bool eval_flags(const struct expr *expr, struct eval_state *state);
-bool eval_fstype(const struct expr *expr, struct eval_state *state);
-bool eval_hidden(const struct expr *expr, struct eval_state *state);
-bool eval_inum(const struct expr *expr, struct eval_state *state);
-bool eval_links(const struct expr *expr, struct eval_state *state);
-bool eval_samefile(const struct expr *expr, struct eval_state *state);
-bool eval_size(const struct expr *expr, struct eval_state *state);
-bool eval_sparse(const struct expr *expr, struct eval_state *state);
-bool eval_type(const struct expr *expr, struct eval_state *state);
-bool eval_xtype(const struct expr *expr, struct eval_state *state);
-
-bool eval_lname(const struct expr *expr, struct eval_state *state);
-bool eval_name(const struct expr *expr, struct eval_state *state);
-bool eval_path(const struct expr *expr, struct eval_state *state);
-bool eval_regex(const struct expr *expr, struct eval_state *state);
-
-bool eval_delete(const struct expr *expr, struct eval_state *state);
-bool eval_exec(const struct expr *expr, struct eval_state *state);
-bool eval_exit(const struct expr *expr, struct eval_state *state);
-bool eval_fls(const struct expr *expr, struct eval_state *state);
-bool eval_fprint(const struct expr *expr, struct eval_state *state);
-bool eval_fprint0(const struct expr *expr, struct eval_state *state);
-bool eval_fprintf(const struct expr *expr, struct eval_state *state);
-bool eval_fprintx(const struct expr *expr, struct eval_state *state);
-bool eval_prune(const struct expr *expr, struct eval_state *state);
-bool eval_quit(const struct expr *expr, struct eval_state *state);
-
-// Operator evaluation functions
-bool eval_not(const struct expr *expr, struct eval_state *state);
-bool eval_and(const struct expr *expr, struct eval_state *state);
-bool eval_or(const struct expr *expr, struct eval_state *state);
-bool eval_comma(const struct expr *expr, struct eval_state *state);
-
-#endif // BFS_EVAL_H
diff --git a/expr.h b/expr.h
deleted file mode 100644
index c25d1ca..0000000
--- a/expr.h
+++ /dev/null
@@ -1,207 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2018 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * The expression tree representation.
- */
-
-#ifndef BFS_EXPR_H
-#define BFS_EXPR_H
-
-#include "color.h"
-#include "eval.h"
-#include "exec.h"
-#include "printf.h"
-#include "stat.h"
-#include <regex.h>
-#include <stdbool.h>
-#include <stddef.h>
-#include <sys/types.h>
-#include <time.h>
-
-/**
- * Possible types of numeric comparison.
- */
-enum cmp_flag {
- /** Exactly n. */
- CMP_EXACT,
- /** Less than n. */
- CMP_LESS,
- /** Greater than n. */
- CMP_GREATER,
-};
-
-/**
- * Possible types of mode comparison.
- */
-enum mode_cmp {
- /** Mode is an exact match (MODE). */
- MODE_EXACT,
- /** Mode has all these bits (-MODE). */
- MODE_ALL,
- /** Mode has any of these bits (/MODE). */
- MODE_ANY,
-};
-
-/**
- * Possible time units.
- */
-enum time_unit {
- /** Seconds. */
- SECONDS,
- /** Minutes. */
- MINUTES,
- /** Days. */
- DAYS,
-};
-
-/**
- * Possible file size units.
- */
-enum size_unit {
- /** 512-byte blocks. */
- SIZE_BLOCKS,
- /** Single bytes. */
- SIZE_BYTES,
- /** Two-byte words. */
- SIZE_WORDS,
- /** Kibibytes. */
- SIZE_KB,
- /** Mebibytes. */
- SIZE_MB,
- /** Gibibytes. */
- SIZE_GB,
- /** Tebibytes. */
- SIZE_TB,
- /** Pebibytes. */
- SIZE_PB,
-};
-
-/**
- * A command line expression.
- */
-struct expr {
- /** The function that evaluates this expression. */
- eval_fn *eval;
-
- /** The left hand side of the expression. */
- struct expr *lhs;
- /** The right hand side of the expression. */
- struct expr *rhs;
-
- /** Whether this expression has no side effects. */
- bool pure;
- /** Whether this expression always evaluates to true. */
- bool always_true;
- /** Whether this expression always evaluates to false. */
- bool always_false;
-
- /** Estimated cost. */
- double cost;
- /** Estimated probability of success. */
- double probability;
- /** Number of times this predicate was executed. */
- size_t evaluations;
- /** Number of times this predicate succeeded. */
- size_t successes;
- /** Total time spent running this predicate. */
- struct timespec elapsed;
-
- /** The number of command line arguments for this expression. */
- size_t argc;
- /** The command line arguments comprising this expression. */
- char **argv;
-
- /** The optional comparison flag. */
- enum cmp_flag cmp_flag;
-
- /** The mode comparison flag. */
- enum mode_cmp mode_cmp;
- /** Mode to use for files. */
- mode_t file_mode;
- /** Mode to use for directories (different due to X). */
- mode_t dir_mode;
-
- /** Flags that should be set. */
- unsigned long long set_flags;
- /** Flags that should be cleared. */
- unsigned long long clear_flags;
-
- /** The optional stat field to look at. */
- enum bfs_stat_field stat_field;
- /** The optional reference time. */
- struct timespec reftime;
- /** The optional time unit. */
- enum time_unit time_unit;
-
- /** The optional size unit. */
- enum size_unit size_unit;
-
- /** Optional device number for a target file. */
- dev_t dev;
- /** Optional inode number for a target file. */
- ino_t ino;
-
- /** File to output to. */
- CFILE *cfile;
-
- /** Optional compiled regex. */
- regex_t *regex;
-
- /** Optional exec command. */
- struct bfs_exec *execbuf;
-
- /** Optional printf command. */
- struct bfs_printf *printf;
-
- /** Optional integer data for this expression. */
- long long idata;
-
- /** Optional string data for this expression. */
- const char *sdata;
-
- /** The number of files this expression keeps open between evaluations. */
- int persistent_fds;
- /** The number of files this expression opens during evaluation. */
- int ephemeral_fds;
-};
-
-/** Singleton true expression instance. */
-extern struct expr expr_true;
-/** Singleton false expression instance. */
-extern struct expr expr_false;
-
-/**
- * Create a new expression.
- */
-struct expr *new_expr(eval_fn *eval, size_t argc, char **argv);
-
-/**
- * @return Whether expr is known to always quit.
- */
-bool expr_never_returns(const struct expr *expr);
-
-/**
- * @return The result of the comparison for this expression.
- */
-bool expr_cmp(const struct expr *expr, long long n);
-
-/**
- * Free an expression tree.
- */
-void free_expr(struct expr *expr);
-
-#endif // BFS_EXPR_H
diff --git a/flags.sh b/flags.sh
deleted file mode 100755
index 15a3a77..0000000
--- a/flags.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-echo "$@" >.newflags
-
-if [ -e .flags ] && cmp -s .flags .newflags; then
- rm .newflags
-else
- mv .newflags .flags
-fi
diff --git a/fsade.h b/fsade.h
deleted file mode 100644
index e964112..0000000
--- a/fsade.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A facade over (file)system features that are (un)implemented differently
- * between platforms.
- */
-
-#ifndef BFS_FSADE_H
-#define BFS_FSADE_H
-
-#include "util.h"
-#include <stdbool.h>
-
-#define BFS_CAN_CHECK_ACL BFS_HAS_SYS_ACL
-
-#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__
-# include <sys/capability.h>
-# ifdef CAP_CHOWN
-# define BFS_CAN_CHECK_CAPABILITIES true
-# endif
-#endif
-
-#define BFS_CAN_CHECK_XATTRS (BFS_HAS_SYS_EXTATTR || BFS_HAS_SYS_XATTR)
-
-struct BFTW;
-
-/**
- * Check if a file has a non-trivial Access Control List.
- *
- * @param ftwbuf
- * The file to check.
- * @return
- * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
- */
-int bfs_check_acl(const struct BFTW *ftwbuf);
-
-/**
- * Check if a file has a non-trivial capability set.
- *
- * @param ftwbuf
- * The file to check.
- * @return
- * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
- */
-int bfs_check_capabilities(const struct BFTW *ftwbuf);
-
-/**
- * Check if a file has any extended attributes set.
- *
- * @param ftwbuf
- * The file to check.
- * @return
- * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
- */
-int bfs_check_xattrs(const struct BFTW *ftwbuf);
-
-/**
- * Check if a file has an extended attribute with the given name.
- *
- * @param ftwbuf
- * The file to check.
- * @param name
- * The name of the xattr to check.
- * @return
- * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
- */
-int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name);
-
-#endif // BFS_FSADE_H
diff --git a/mtab.c b/mtab.c
deleted file mode 100644
index 91a40aa..0000000
--- a/mtab.c
+++ /dev/null
@@ -1,246 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "mtab.h"
-#include "darray.h"
-#include "stat.h"
-#include "trie.h"
-#include "util.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
-#endif
-
-#if BFS_HAS_MNTENT
-# define BFS_MNTENT 1
-#elif BSD
-# define BFS_MNTINFO 1
-#elif __SVR4
-# define BFS_MNTTAB 1
-#endif
-
-#if BFS_MNTENT
-# include <mntent.h>
-# include <paths.h>
-# include <stdio.h>
-#elif BFS_MNTINFO
-# include <sys/mount.h>
-# include <sys/ucred.h>
-#elif BFS_MNTTAB
-# include <stdio.h>
-# include <sys/mnttab.h>
-#endif
-
-/**
- * A mount point in the table.
- */
-struct bfs_mtab_entry {
- /** The path to the mount point. */
- char *path;
- /** The filesystem type. */
- char *type;
-};
-
-struct bfs_mtab {
- /** The list of mount points. */
- struct bfs_mtab_entry *entries;
- /** The basenames of every mount point. */
- struct trie names;
-
- /** A map from device ID to fstype (populated lazily). */
- struct trie types;
- /** Whether the types map has been populated. */
- bool types_filled;
-};
-
-/**
- * Add an entry to the mount table.
- */
-static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) {
- struct bfs_mtab_entry entry = {
- .path = strdup(path),
- .type = strdup(type),
- };
-
- if (!entry.path || !entry.type) {
- goto fail_entry;
- }
-
- if (DARRAY_PUSH(&mtab->entries, &entry) != 0) {
- goto fail_entry;
- }
-
- if (!trie_insert_str(&mtab->names, xbasename(path))) {
- goto fail;
- }
-
- return 0;
-
-fail_entry:
- free(entry.type);
- free(entry.path);
-fail:
- return -1;
-}
-
-struct bfs_mtab *bfs_mtab_parse() {
- struct bfs_mtab *mtab = malloc(sizeof(*mtab));
- if (!mtab) {
- return NULL;
- }
-
- mtab->entries = NULL;
- trie_init(&mtab->names);
- trie_init(&mtab->types);
- mtab->types_filled = false;
-
- int error = 0;
-
-#if BFS_MNTENT
-
- FILE *file = setmntent(_PATH_MOUNTED, "r");
- if (!file) {
- // In case we're in a chroot or something with /proc but no /etc/mtab
- error = errno;
- file = setmntent("/proc/mounts", "r");
- }
- if (!file) {
- goto fail;
- }
-
- struct mntent *mnt;
- while ((mnt = getmntent(file))) {
- if (bfs_mtab_add(mtab, mnt->mnt_dir, mnt->mnt_type) != 0) {
- error = errno;
- endmntent(file);
- goto fail;
- }
- }
-
- endmntent(file);
-
-#elif BFS_MNTINFO
-
-#if __NetBSD__
- typedef struct statvfs bfs_statfs;
-#else
- typedef struct statfs bfs_statfs;
-#endif
-
- bfs_statfs *mntbuf;
- int size = getmntinfo(&mntbuf, MNT_WAIT);
- if (size < 0) {
- error = errno;
- goto fail;
- }
-
- for (bfs_statfs *mnt = mntbuf; mnt < mntbuf + size; ++mnt) {
- if (bfs_mtab_add(mtab, mnt->f_mntonname, mnt->f_fstypename) != 0) {
- error = errno;
- goto fail;
- }
- }
-
-#elif BFS_MNTTAB
-
- FILE *file = fopen(MNTTAB, "r");
- if (!file) {
- error = errno;
- goto fail;
- }
-
- struct mnttab mnt;
- while (getmntent(file, &mnt) == 0) {
- if (bfs_mtab_add(mtab, mnt.mnt_mountp, mnt.mnt_fstype) != 0) {
- error = errno;
- fclose(file);
- goto fail;
- }
- }
-
- fclose(file);
-
-#else
-
- error = ENOTSUP;
- goto fail;
-
-#endif
-
- return mtab;
-
-fail:
- bfs_mtab_free(mtab);
- errno = error;
- return NULL;
-}
-
-static void bfs_mtab_fill_types(struct bfs_mtab *mtab) {
- for (size_t i = 0; i < darray_length(mtab->entries); ++i) {
- struct bfs_mtab_entry *entry = mtab->entries + i;
-
- struct bfs_stat sb;
- if (bfs_stat(AT_FDCWD, entry->path, BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC, &sb) != 0) {
- continue;
- }
-
- struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev));
- if (leaf) {
- leaf->value = entry->type;
- }
- }
-
- mtab->types_filled = true;
-}
-
-const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) {
- if (!mtab->types_filled) {
- bfs_mtab_fill_types((struct bfs_mtab *)mtab);
- }
-
- const struct trie_leaf *leaf = trie_find_mem(&mtab->types, &statbuf->dev, sizeof(statbuf->dev));
- if (leaf) {
- return leaf->value;
- } else {
- return "unknown";
- }
-}
-
-bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path) {
- const char *name = xbasename(path);
- return trie_find_str(&mtab->names, name);
-}
-
-void bfs_mtab_free(struct bfs_mtab *mtab) {
- if (mtab) {
- trie_destroy(&mtab->types);
- trie_destroy(&mtab->names);
-
- for (size_t i = 0; i < darray_length(mtab->entries); ++i) {
- free(mtab->entries[i].type);
- free(mtab->entries[i].path);
- }
- darray_free(mtab->entries);
-
- free(mtab);
- }
-}
diff --git a/mtab.h b/mtab.h
deleted file mode 100644
index 807539d..0000000
--- a/mtab.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A facade over platform-specific APIs for enumerating mounted filesystems.
- */
-
-#ifndef BFS_MTAB_H
-#define BFS_MTAB_H
-
-#include <stdbool.h>
-
-struct bfs_stat;
-
-/**
- * A file system mount table.
- */
-struct bfs_mtab;
-
-/**
- * Parse the mount table.
- *
- * @return
- * The parsed mount table, or NULL on error.
- */
-struct bfs_mtab *bfs_mtab_parse(void);
-
-/**
- * Determine the file system type that a file is on.
- *
- * @param mtab
- * The current mount table.
- * @param statbuf
- * The bfs_stat() buffer for the file in question.
- * @return
- * The type of file system containing this file, "unknown" if not known,
- * or NULL on error.
- */
-const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf);
-
-/**
- * Check if a file could be a mount point.
- *
- * @param mtab
- * The current mount table.
- * @param path
- * The path to check.
- * @return
- * Whether the named file could be a mount point.
- */
-bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path);
-
-/**
- * Free a mount table.
- */
-void bfs_mtab_free(struct bfs_mtab *mtab);
-
-#endif // BFS_MTAB_H
diff --git a/opt.c b/opt.c
deleted file mode 100644
index 96b99da..0000000
--- a/opt.c
+++ /dev/null
@@ -1,1051 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * The expression optimizer. Different optimization levels are supported:
- *
- * -O1: basic logical simplifications, like folding (-true -and -foo) to -foo.
- *
- * -O2: dead code elimination and data flow analysis. struct opt_facts is used
- * to record data flow facts that are true at various points of evaluation.
- * Specifically, struct opt_facts records the facts that must be true before an
- * expression is evaluated (state->facts), and those that must be true after the
- * expression is evaluated, given that it returns true (state->facts_when_true)
- * or false (state->facts_when_true). Additionally, state->facts_when_impure
- * records the possible data flow facts before any expressions with side effects
- * are evaluated.
- *
- * -O3: expression re-ordering to reduce expected cost. In an expression like
- * (-foo -and -bar), if both -foo and -bar are pure (no side effects), they can
- * be re-ordered to (-bar -and -foo). This is profitable if the expected cost
- * is lower for the re-ordered expression, for example if -foo is very slow or
- * -bar is likely to return false.
- *
- * -O4/-Ofast: aggressive optimizations that may affect correctness in corner
- * cases. The main effect is to use facts_when_impure to determine if any side-
- * effects are reachable at all, and skipping the traversal if not.
- */
-
-#include "opt.h"
-#include "color.h"
-#include "ctx.h"
-#include "diag.h"
-#include "eval.h"
-#include "expr.h"
-#include "pwcache.h"
-#include "util.h"
-#include <assert.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <unistd.h>
-
-static char *fake_and_arg = "-a";
-static char *fake_or_arg = "-o";
-static char *fake_not_arg = "!";
-
-/**
- * A contrained integer range.
- */
-struct range {
- /** The (inclusive) minimum value. */
- long long min;
- /** The (inclusive) maximum value. */
- long long max;
-};
-
-/** Compute the minimum of two values. */
-static long long min_value(long long a, long long b) {
- if (a < b) {
- return a;
- } else {
- return b;
- }
-}
-
-/** Compute the maximum of two values. */
-static long long max_value(long long a, long long b) {
- if (a > b) {
- return a;
- } else {
- return b;
- }
-}
-
-/** Constrain the minimum of a range. */
-static void constrain_min(struct range *range, long long value) {
- range->min = max_value(range->min, value);
-}
-
-/** Contrain the maximum of a range. */
-static void constrain_max(struct range *range, long long value) {
- range->max = min_value(range->max, value);
-}
-
-/** Remove a single value from a range. */
-static void range_remove(struct range *range, long long value) {
- if (range->min == value) {
- if (range->min == LLONG_MAX) {
- range->max = LLONG_MIN;
- } else {
- ++range->min;
- }
- }
-
- if (range->max == value) {
- if (range->max == LLONG_MIN) {
- range->min = LLONG_MAX;
- } else {
- --range->max;
- }
- }
-}
-
-/** Compute the union of two ranges. */
-static void range_union(struct range *result, const struct range *lhs, const struct range *rhs) {
- result->min = min_value(lhs->min, rhs->min);
- result->max = max_value(lhs->max, rhs->max);
-}
-
-/** Check if a range contains no values. */
-static bool range_is_impossible(const struct range *range) {
- return range->min > range->max;
-}
-
-/** Set a range to contain no values. */
-static void set_range_impossible(struct range *range) {
- range->min = LLONG_MAX;
- range->max = LLONG_MIN;
-}
-
-/**
- * Types of ranges we track.
- */
-enum range_type {
- /** Search tree depth. */
- DEPTH_RANGE,
- /** Group ID. */
- GID_RANGE,
- /** Inode number. */
- INUM_RANGE,
- /** Hard link count. */
- LINKS_RANGE,
- /** File size. */
- SIZE_RANGE,
- /** User ID. */
- UID_RANGE,
- /** The number of range_types. */
- RANGE_TYPES,
-};
-
-/**
- * A possibly-known value of a predicate.
- */
-enum known_pred {
- /** The state is impossible to reach. */
- PRED_IMPOSSIBLE = -2,
- /** The value of the predicate is not known. */
- PRED_UNKNOWN = -1,
- /** The predicate is known to be false. */
- PRED_FALSE = false,
- /** The predicate is known to be true. */
- PRED_TRUE = true,
-};
-
-/** Make a predicate known. */
-static void constrain_pred(enum known_pred *pred, bool value) {
- if (*pred == PRED_UNKNOWN) {
- *pred = value;
- } else if (*pred == !value) {
- *pred = PRED_IMPOSSIBLE;
- }
-}
-
-/** Compute the union of two known predicates. */
-static enum known_pred pred_union(enum known_pred lhs, enum known_pred rhs) {
- if (lhs == PRED_IMPOSSIBLE) {
- return rhs;
- } else if (rhs == PRED_IMPOSSIBLE) {
- return lhs;
- } else if (lhs == rhs) {
- return lhs;
- } else {
- return PRED_UNKNOWN;
- }
-}
-
-/**
- * Types of predicates we track.
- */
-enum pred_type {
- /** -readable */
- READABLE_PRED,
- /** -writable */
- WRITABLE_PRED,
- /** -executable */
- EXECUTABLE_PRED,
- /** -acl */
- ACL_PRED,
- /** -capable */
- CAPABLE_PRED,
- /** -empty */
- EMPTY_PRED,
- /** -hidden */
- HIDDEN_PRED,
- /** -nogroup */
- NOGROUP_PRED,
- /** -nouser */
- NOUSER_PRED,
- /** -sparse */
- SPARSE_PRED,
- /** -xattr */
- XATTR_PRED,
- /** The number of pred_types. */
- PRED_TYPES,
-};
-
-/**
- * Data flow facts about an evaluation point.
- */
-struct opt_facts {
- /** The value ranges we track. */
- struct range ranges[RANGE_TYPES];
-
- /** The predicates we track. */
- enum known_pred preds[PRED_TYPES];
-
- /** Bitmask of possible file types. */
- unsigned int types;
- /** Bitmask of possible link target types. */
- unsigned int xtypes;
-};
-
-/** Initialize some data flow facts. */
-static void facts_init(struct opt_facts *facts) {
- for (int i = 0; i < RANGE_TYPES; ++i) {
- struct range *range = &facts->ranges[i];
- range->min = 0; // All ranges we currently track are non-negative
- range->max = LLONG_MAX;
- }
-
- for (int i = 0; i < PRED_TYPES; ++i) {
- facts->preds[i] = PRED_UNKNOWN;
- }
-
- facts->types = ~0;
- facts->xtypes = ~0;
-}
-
-/** Compute the union of two fact sets. */
-static void facts_union(struct opt_facts *result, const struct opt_facts *lhs, const struct opt_facts *rhs) {
- for (int i = 0; i < RANGE_TYPES; ++i) {
- range_union(&result->ranges[i], &lhs->ranges[i], &rhs->ranges[i]);
- }
-
- for (int i = 0; i < PRED_TYPES; ++i) {
- result->preds[i] = pred_union(lhs->preds[i], rhs->preds[i]);
- }
-
- result->types = lhs->types | rhs->types;
- result->xtypes = lhs->xtypes | rhs->xtypes;
-}
-
-/** Determine whether a fact set is impossible. */
-static bool facts_are_impossible(const struct opt_facts *facts) {
- for (int i = 0; i < RANGE_TYPES; ++i) {
- if (range_is_impossible(&facts->ranges[i])) {
- return true;
- }
- }
-
- for (int i = 0; i < PRED_TYPES; ++i) {
- if (facts->preds[i] == PRED_IMPOSSIBLE) {
- return true;
- }
- }
-
- if (!facts->types || !facts->xtypes) {
- return true;
- }
-
- return false;
-}
-
-/** Set some facts to be impossible. */
-static void set_facts_impossible(struct opt_facts *facts) {
- for (int i = 0; i < RANGE_TYPES; ++i) {
- set_range_impossible(&facts->ranges[i]);
- }
-
- for (int i = 0; i < PRED_TYPES; ++i) {
- facts->preds[i] = PRED_IMPOSSIBLE;
- }
-
- facts->types = 0;
- facts->xtypes = 0;
-}
-
-/**
- * Optimizer state.
- */
-struct opt_state {
- /** The context we're optimizing. */
- const struct bfs_ctx *ctx;
-
- /** Data flow facts before this expression is evaluated. */
- struct opt_facts facts;
- /** Data flow facts after this expression returns true. */
- struct opt_facts facts_when_true;
- /** Data flow facts after this expression returns false. */
- struct opt_facts facts_when_false;
- /** Data flow facts before any side-effecting expressions are evaluated. */
- struct opt_facts *facts_when_impure;
-};
-
-/** Log an optimization. */
-BFS_FORMATTER(3, 4)
-static bool debug_opt(const struct opt_state *state, int level, const char *format, ...) {
- assert(state->ctx->optlevel >= level);
-
- if (bfs_debug(state->ctx, DEBUG_OPT, "${cyn}-O%d${rs}: ", level)) {
- va_list args;
- va_start(args, format);
- cvfprintf(state->ctx->cerr, format, args);
- va_end(args);
- return true;
- } else {
- return false;
- }
-}
-
-/** Extract a child expression, freeing the outer expression. */
-static struct expr *extract_child_expr(struct expr *expr, struct expr **child) {
- struct expr *ret = *child;
- *child = NULL;
- free_expr(expr);
- return ret;
-}
-
-/**
- * Negate an expression.
- */
-static struct expr *negate_expr(struct expr *rhs, char **argv) {
- if (rhs->eval == eval_not) {
- return extract_child_expr(rhs, &rhs->rhs);
- }
-
- struct expr *expr = new_expr(eval_not, 1, argv);
- if (!expr) {
- free_expr(rhs);
- return NULL;
- }
-
- expr->rhs = rhs;
- return expr;
-}
-
-static struct expr *optimize_not_expr(const struct opt_state *state, struct expr *expr);
-static struct expr *optimize_and_expr(const struct opt_state *state, struct expr *expr);
-static struct expr *optimize_or_expr(const struct opt_state *state, struct expr *expr);
-
-/**
- * Apply De Morgan's laws.
- */
-static struct expr *de_morgan(const struct opt_state *state, struct expr *expr, char **argv) {
- bool debug = debug_opt(state, 1, "De Morgan's laws: %pe ", expr);
-
- struct expr *parent = negate_expr(expr, argv);
- if (!parent) {
- return NULL;
- }
-
- bool has_parent = true;
- if (parent->eval != eval_not) {
- expr = parent;
- has_parent = false;
- }
-
- assert(expr->eval == eval_and || expr->eval == eval_or);
- if (expr->eval == eval_and) {
- expr->eval = eval_or;
- expr->argv = &fake_or_arg;
- } else {
- expr->eval = eval_and;
- expr->argv = &fake_and_arg;
- }
-
- expr->lhs = negate_expr(expr->lhs, argv);
- expr->rhs = negate_expr(expr->rhs, argv);
- if (!expr->lhs || !expr->rhs) {
- free_expr(parent);
- return NULL;
- }
-
- if (debug) {
- cfprintf(state->ctx->cerr, "<==> %pe\n", parent);
- }
-
- if (expr->lhs->eval == eval_not) {
- expr->lhs = optimize_not_expr(state, expr->lhs);
- }
- if (expr->rhs->eval == eval_not) {
- expr->rhs = optimize_not_expr(state, expr->rhs);
- }
- if (!expr->lhs || !expr->rhs) {
- free_expr(parent);
- return NULL;
- }
-
- if (expr->eval == eval_and) {
- expr = optimize_and_expr(state, expr);
- } else {
- expr = optimize_or_expr(state, expr);
- }
- if (has_parent) {
- parent->rhs = expr;
- } else {
- parent = expr;
- }
- if (!expr) {
- free_expr(parent);
- return NULL;
- }
-
- if (has_parent) {
- parent = optimize_not_expr(state, parent);
- }
- return parent;
-}
-
-/** Optimize an expression recursively. */
-static struct expr *optimize_expr_recursive(struct opt_state *state, struct expr *expr);
-
-/**
- * Optimize a negation.
- */
-static struct expr *optimize_not_expr(const struct opt_state *state, struct expr *expr) {
- assert(expr->eval == eval_not);
-
- struct expr *rhs = expr->rhs;
-
- int optlevel = state->ctx->optlevel;
- if (optlevel >= 1) {
- if (rhs == &expr_true) {
- debug_opt(state, 1, "constant propagation: %pe <==> %pe\n", expr, &expr_false);
- free_expr(expr);
- return &expr_false;
- } else if (rhs == &expr_false) {
- debug_opt(state, 1, "constant propagation: %pe <==> %pe\n", expr, &expr_true);
- free_expr(expr);
- return &expr_true;
- } else if (rhs->eval == eval_not) {
- debug_opt(state, 1, "double negation: %pe <==> %pe\n", expr, rhs->rhs);
- return extract_child_expr(expr, &rhs->rhs);
- } else if (expr_never_returns(rhs)) {
- debug_opt(state, 1, "reachability: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if ((rhs->eval == eval_and || rhs->eval == eval_or)
- && (rhs->lhs->eval == eval_not || rhs->rhs->eval == eval_not)) {
- return de_morgan(state, expr, expr->argv);
- }
- }
-
- expr->pure = rhs->pure;
- expr->always_true = rhs->always_false;
- expr->always_false = rhs->always_true;
- expr->cost = rhs->cost;
- expr->probability = 1.0 - rhs->probability;
-
- return expr;
-}
-
-/** Optimize a negation recursively. */
-static struct expr *optimize_not_expr_recursive(struct opt_state *state, struct expr *expr) {
- struct opt_state rhs_state = *state;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
- }
-
- state->facts_when_true = rhs_state.facts_when_false;
- state->facts_when_false = rhs_state.facts_when_true;
-
- return optimize_not_expr(state, expr);
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/** Optimize a conjunction. */
-static struct expr *optimize_and_expr(const struct opt_state *state, struct expr *expr) {
- assert(expr->eval == eval_and);
-
- struct expr *lhs = expr->lhs;
- struct expr *rhs = expr->rhs;
-
- const struct bfs_ctx *ctx = state->ctx;
- int optlevel = ctx->optlevel;
- if (optlevel >= 1) {
- if (lhs == &expr_true) {
- debug_opt(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (rhs == &expr_true) {
- debug_opt(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_false) {
- debug_opt(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_true && rhs == &expr_false) {
- bool debug = debug_opt(state, 1, "strength reduction: %pe <==> ", expr);
- struct expr *ret = extract_child_expr(expr, &expr->lhs);
- ret = negate_expr(ret, &fake_not_arg);
- if (debug && ret) {
- cfprintf(ctx->cerr, "%pe\n", ret);
- }
- return ret;
- } else if (optlevel >= 2 && lhs->pure && rhs == &expr_false) {
- debug_opt(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (lhs->eval == eval_not && rhs->eval == eval_not) {
- return de_morgan(state, expr, expr->lhs->argv);
- }
- }
-
- expr->pure = lhs->pure && rhs->pure;
- expr->always_true = lhs->always_true && rhs->always_true;
- expr->always_false = lhs->always_false || rhs->always_false;
- expr->cost = lhs->cost + lhs->probability*rhs->cost;
- expr->probability = lhs->probability*rhs->probability;
-
- return expr;
-}
-
-/** Optimize a conjunction recursively. */
-static struct expr *optimize_and_expr_recursive(struct opt_state *state, struct expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
- }
-
- struct opt_state rhs_state = *state;
- rhs_state.facts = lhs_state.facts_when_true;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
- }
-
- state->facts_when_true = rhs_state.facts_when_true;
- facts_union(&state->facts_when_false, &lhs_state.facts_when_false, &rhs_state.facts_when_false);
-
- return optimize_and_expr(state, expr);
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/** Optimize a disjunction. */
-static struct expr *optimize_or_expr(const struct opt_state *state, struct expr *expr) {
- assert(expr->eval == eval_or);
-
- struct expr *lhs = expr->lhs;
- struct expr *rhs = expr->rhs;
-
- const struct bfs_ctx *ctx = state->ctx;
- int optlevel = ctx->optlevel;
- if (optlevel >= 1) {
- if (lhs->always_true) {
- debug_opt(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs == &expr_false) {
- debug_opt(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (rhs == &expr_false) {
- debug_opt(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_false && rhs == &expr_true) {
- bool debug = debug_opt(state, 1, "strength reduction: %pe <==> ", expr);
- struct expr *ret = extract_child_expr(expr, &expr->lhs);
- ret = negate_expr(ret, &fake_not_arg);
- if (debug && ret) {
- cfprintf(ctx->cerr, "%pe\n", ret);
- }
- return ret;
- } else if (optlevel >= 2 && lhs->pure && rhs == &expr_true) {
- debug_opt(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (lhs->eval == eval_not && rhs->eval == eval_not) {
- return de_morgan(state, expr, expr->lhs->argv);
- }
- }
-
- expr->pure = lhs->pure && rhs->pure;
- expr->always_true = lhs->always_true || rhs->always_true;
- expr->always_false = lhs->always_false && rhs->always_false;
- expr->cost = lhs->cost + (1 - lhs->probability)*rhs->cost;
- expr->probability = lhs->probability + rhs->probability - lhs->probability*rhs->probability;
-
- return expr;
-}
-
-/** Optimize a disjunction recursively. */
-static struct expr *optimize_or_expr_recursive(struct opt_state *state, struct expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
- }
-
- struct opt_state rhs_state = *state;
- rhs_state.facts = lhs_state.facts_when_false;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
- }
-
- facts_union(&state->facts_when_true, &lhs_state.facts_when_true, &rhs_state.facts_when_true);
- state->facts_when_false = rhs_state.facts_when_false;
-
- return optimize_or_expr(state, expr);
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/** Optimize an expression in an ignored-result context. */
-static struct expr *ignore_result(const struct opt_state *state, struct expr *expr) {
- int optlevel = state->ctx->optlevel;
-
- if (optlevel >= 1) {
- while (true) {
- if (expr->eval == eval_not) {
- debug_opt(state, 1, "ignored result: %pe --> %pe\n", expr, expr->rhs);
- expr = extract_child_expr(expr, &expr->rhs);
- } else if (optlevel >= 2
- && (expr->eval == eval_and || expr->eval == eval_or || expr->eval == eval_comma)
- && expr->rhs->pure) {
- debug_opt(state, 2, "ignored result: %pe --> %pe\n", expr, expr->lhs);
- expr = extract_child_expr(expr, &expr->lhs);
- } else {
- break;
- }
- }
-
- if (optlevel >= 2 && expr->pure && expr != &expr_false) {
- debug_opt(state, 2, "ignored result: %pe --> %pe\n", expr, &expr_false);
- free_expr(expr);
- expr = &expr_false;
- }
- }
-
- return expr;
-}
-
-/** Optimize a comma expression. */
-static struct expr *optimize_comma_expr(const struct opt_state *state, struct expr *expr) {
- assert(expr->eval == eval_comma);
-
- struct expr *lhs = expr->lhs;
- struct expr *rhs = expr->rhs;
-
- int optlevel = state->ctx->optlevel;
- if (optlevel >= 1) {
- lhs = expr->lhs = ignore_result(state, lhs);
-
- if (expr_never_returns(lhs)) {
- debug_opt(state, 1, "reachability: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if ((lhs->always_true && rhs == &expr_true)
- || (lhs->always_false && rhs == &expr_false)) {
- debug_opt(state, 1, "redundancy elimination: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (optlevel >= 2 && lhs->pure) {
- debug_opt(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- }
- }
-
- expr->pure = lhs->pure && rhs->pure;
- expr->always_true = expr_never_returns(lhs) || rhs->always_true;
- expr->always_false = expr_never_returns(lhs) || rhs->always_false;
- expr->cost = lhs->cost + rhs->cost;
- expr->probability = rhs->probability;
-
- return expr;
-}
-
-/** Optimize a comma expression recursively. */
-static struct expr *optimize_comma_expr_recursive(struct opt_state *state, struct expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
- }
-
- struct opt_state rhs_state = *state;
- facts_union(&rhs_state.facts, &lhs_state.facts_when_true, &lhs_state.facts_when_false);
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
- }
-
- return optimize_comma_expr(state, expr);
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/** Infer data flow facts about a predicate. */
-static void infer_pred_facts(struct opt_state *state, enum pred_type pred) {
- constrain_pred(&state->facts_when_true.preds[pred], true);
- constrain_pred(&state->facts_when_false.preds[pred], false);
-}
-
-/** Infer data flow facts about an -{execut,read,writ}able expression. */
-static void infer_access_facts(struct opt_state *state, const struct expr *expr) {
- if (expr->idata & R_OK) {
- infer_pred_facts(state, READABLE_PRED);
- }
- if (expr->idata & W_OK) {
- infer_pred_facts(state, WRITABLE_PRED);
- }
- if (expr->idata & X_OK) {
- infer_pred_facts(state, EXECUTABLE_PRED);
- }
-}
-
-/** Infer data flow facts about an icmp-style ([+-]N) expression. */
-static void infer_icmp_facts(struct opt_state *state, const struct expr *expr, enum range_type type) {
- struct range *range_when_true = &state->facts_when_true.ranges[type];
- struct range *range_when_false =& state->facts_when_false.ranges[type];
- long long value = expr->idata;
-
- switch (expr->cmp_flag) {
- case CMP_EXACT:
- constrain_min(range_when_true, value);
- constrain_max(range_when_true, value);
- range_remove(range_when_false, value);
- break;
-
- case CMP_LESS:
- constrain_min(range_when_false, value);
- constrain_max(range_when_true, value);
- range_remove(range_when_true, value);
- break;
-
- case CMP_GREATER:
- constrain_max(range_when_false, value);
- constrain_min(range_when_true, value);
- range_remove(range_when_true, value);
- break;
- }
-}
-
-/** Infer data flow facts about a -gid expression. */
-static void infer_gid_facts(struct opt_state *state, const struct expr *expr) {
- infer_icmp_facts(state, expr, GID_RANGE);
-
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
- struct range *range = &state->facts_when_true.ranges[GID_RANGE];
- if (groups && range->min == range->max) {
- gid_t gid = range->min;
- bool nogroup = !bfs_getgrgid(groups, gid);
- constrain_pred(&state->facts_when_true.preds[NOGROUP_PRED], nogroup);
- }
-}
-
-/** Infer data flow facts about a -uid expression. */
-static void infer_uid_facts(struct opt_state *state, const struct expr *expr) {
- infer_icmp_facts(state, expr, UID_RANGE);
-
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- struct range *range = &state->facts_when_true.ranges[UID_RANGE];
- if (users && range->min == range->max) {
- uid_t uid = range->min;
- bool nouser = !bfs_getpwuid(users, uid);
- constrain_pred(&state->facts_when_true.preds[NOUSER_PRED], nouser);
- }
-}
-
-/** Infer data flow facts about a -samefile expression. */
-static void infer_samefile_facts(struct opt_state *state, const struct expr *expr) {
- struct range *range_when_true = &state->facts_when_true.ranges[INUM_RANGE];
- constrain_min(range_when_true, expr->ino);
- constrain_max(range_when_true, expr->ino);
-}
-
-/** Infer data flow facts about a -type expression. */
-static void infer_type_facts(struct opt_state *state, const struct expr *expr) {
- state->facts_when_true.types &= expr->idata;
- state->facts_when_false.types &= ~expr->idata;
-}
-
-/** Infer data flow facts about an -xtype expression. */
-static void infer_xtype_facts(struct opt_state *state, const struct expr *expr) {
- state->facts_when_true.xtypes &= expr->idata;
- state->facts_when_false.xtypes &= ~expr->idata;
-}
-
-static struct expr *optimize_expr_recursive(struct opt_state *state, struct expr *expr) {
- int optlevel = state->ctx->optlevel;
-
- state->facts_when_true = state->facts;
- state->facts_when_false = state->facts;
-
- if (optlevel >= 2 && facts_are_impossible(&state->facts)) {
- debug_opt(state, 2, "reachability: %pe --> %pe\n", expr, &expr_false);
- free_expr(expr);
- expr = &expr_false;
- goto done;
- }
-
- if (!expr->rhs && !expr->pure) {
- facts_union(state->facts_when_impure, state->facts_when_impure, &state->facts);
- }
-
- if (expr->eval == eval_access) {
- infer_access_facts(state, expr);
- } else if (expr->eval == eval_acl) {
- infer_pred_facts(state, ACL_PRED);
- } else if (expr->eval == eval_capable) {
- infer_pred_facts(state, CAPABLE_PRED);
- } else if (expr->eval == eval_depth) {
- infer_icmp_facts(state, expr, DEPTH_RANGE);
- } else if (expr->eval == eval_empty) {
- infer_pred_facts(state, EMPTY_PRED);
- } else if (expr->eval == eval_gid) {
- infer_gid_facts(state, expr);
- } else if (expr->eval == eval_hidden) {
- infer_pred_facts(state, HIDDEN_PRED);
- } else if (expr->eval == eval_inum) {
- infer_icmp_facts(state, expr, INUM_RANGE);
- } else if (expr->eval == eval_links) {
- infer_icmp_facts(state, expr, LINKS_RANGE);
- } else if (expr->eval == eval_nogroup) {
- infer_pred_facts(state, NOGROUP_PRED);
- } else if (expr->eval == eval_nouser) {
- infer_pred_facts(state, NOUSER_PRED);
- } else if (expr->eval == eval_samefile) {
- infer_samefile_facts(state, expr);
- } else if (expr->eval == eval_size) {
- infer_icmp_facts(state, expr, SIZE_RANGE);
- } else if (expr->eval == eval_sparse) {
- infer_pred_facts(state, SPARSE_PRED);
- } else if (expr->eval == eval_type) {
- infer_type_facts(state, expr);
- } else if (expr->eval == eval_uid) {
- infer_uid_facts(state, expr);
- } else if (expr->eval == eval_xattr) {
- infer_pred_facts(state, XATTR_PRED);
- } else if (expr->eval == eval_xtype) {
- infer_xtype_facts(state, expr);
- } else if (expr->eval == eval_not) {
- expr = optimize_not_expr_recursive(state, expr);
- } else if (expr->eval == eval_and) {
- expr = optimize_and_expr_recursive(state, expr);
- } else if (expr->eval == eval_or) {
- expr = optimize_or_expr_recursive(state, expr);
- } else if (expr->eval == eval_comma) {
- expr = optimize_comma_expr_recursive(state, expr);
- }
-
- if (!expr) {
- goto done;
- }
-
- struct expr *lhs = expr->lhs;
- struct expr *rhs = expr->rhs;
- if (rhs) {
- expr->persistent_fds = rhs->persistent_fds;
- expr->ephemeral_fds = rhs->ephemeral_fds;
- }
- if (lhs) {
- expr->persistent_fds += lhs->persistent_fds;
- if (lhs->ephemeral_fds > expr->ephemeral_fds) {
- expr->ephemeral_fds = lhs->ephemeral_fds;
- }
- }
-
- if (expr->always_true) {
- set_facts_impossible(&state->facts_when_false);
- }
- if (expr->always_false) {
- set_facts_impossible(&state->facts_when_true);
- }
-
- if (optlevel < 2 || expr == &expr_true || expr == &expr_false) {
- goto done;
- }
-
- if (facts_are_impossible(&state->facts_when_true)) {
- if (expr->pure) {
- debug_opt(state, 2, "data flow: %pe --> %pe\n", expr, &expr_false);
- free_expr(expr);
- expr = &expr_false;
- } else {
- expr->always_false = true;
- expr->probability = 0.0;
- }
- } else if (facts_are_impossible(&state->facts_when_false)) {
- if (expr->pure) {
- debug_opt(state, 2, "data flow: %pe --> %pe\n", expr, &expr_true);
- free_expr(expr);
- expr = &expr_true;
- } else {
- expr->always_true = true;
- expr->probability = 1.0;
- }
- }
-
-done:
- return expr;
-}
-
-/** Swap the children of a binary expression if it would reduce the cost. */
-static bool reorder_expr(const struct opt_state *state, struct expr *expr, double swapped_cost) {
- if (swapped_cost < expr->cost) {
- bool debug = debug_opt(state, 3, "cost: %pe <==> ", expr);
- struct expr *lhs = expr->lhs;
- expr->lhs = expr->rhs;
- expr->rhs = lhs;
- if (debug) {
- cfprintf(state->ctx->cerr, "%pe (~${ylw}%g${rs} --> ~${ylw}%g${rs})\n", expr, expr->cost, swapped_cost);
- }
- expr->cost = swapped_cost;
- return true;
- } else {
- return false;
- }
-}
-
-/**
- * Recursively reorder sub-expressions to reduce the overall cost.
- *
- * @param expr
- * The expression to optimize.
- * @return
- * Whether any subexpression was reordered.
- */
-static bool reorder_expr_recursive(const struct opt_state *state, struct expr *expr) {
- bool ret = false;
- struct expr *lhs = expr->lhs;
- struct expr *rhs = expr->rhs;
-
- if (lhs) {
- ret |= reorder_expr_recursive(state, lhs);
- }
- if (rhs) {
- ret |= reorder_expr_recursive(state, rhs);
- }
-
- if (expr->eval == eval_and || expr->eval == eval_or) {
- if (lhs->pure && rhs->pure) {
- double rhs_prob = expr->eval == eval_and ? rhs->probability : 1.0 - rhs->probability;
- double swapped_cost = rhs->cost + rhs_prob*lhs->cost;
- ret |= reorder_expr(state, expr, swapped_cost);
- }
- }
-
- return ret;
-}
-
-/**
- * Optimize a top-level expression.
- */
-static struct expr *optimize_expr(struct opt_state *state, struct expr *expr) {
- struct opt_facts saved_impure = *state->facts_when_impure;
-
- expr = optimize_expr_recursive(state, expr);
- if (!expr) {
- return NULL;
- }
-
- if (state->ctx->optlevel >= 3 && reorder_expr_recursive(state, expr)) {
- // Re-do optimizations to account for the new ordering
- *state->facts_when_impure = saved_impure;
- expr = optimize_expr_recursive(state, expr);
- if (!expr) {
- return NULL;
- }
- }
-
- return expr;
-}
-
-int bfs_optimize(struct bfs_ctx *ctx) {
- bfs_ctx_dump(ctx, DEBUG_OPT);
-
- struct opt_facts facts_when_impure;
- set_facts_impossible(&facts_when_impure);
-
- struct opt_state state = {
- .ctx = ctx,
- .facts_when_impure = &facts_when_impure,
- };
- facts_init(&state.facts);
-
- ctx->exclude = optimize_expr(&state, ctx->exclude);
- if (!ctx->exclude) {
- return -1;
- }
-
- // Only non-excluded files are evaluated
- state.facts = state.facts_when_false;
-
- struct range *depth = &state.facts.ranges[DEPTH_RANGE];
- constrain_min(depth, ctx->mindepth);
- constrain_max(depth, ctx->maxdepth);
-
- ctx->expr = optimize_expr(&state, ctx->expr);
- if (!ctx->expr) {
- return -1;
- }
-
- ctx->expr = ignore_result(&state, ctx->expr);
-
- if (facts_are_impossible(&facts_when_impure)) {
- bfs_warning(ctx, "This command won't do anything.\n\n");
- }
-
- const struct range *depth_when_impure = &facts_when_impure.ranges[DEPTH_RANGE];
- long long mindepth = depth_when_impure->min;
- long long maxdepth = depth_when_impure->max;
-
- int optlevel = ctx->optlevel;
-
- if (optlevel >= 2 && mindepth > ctx->mindepth) {
- if (mindepth > INT_MAX) {
- mindepth = INT_MAX;
- }
- ctx->mindepth = mindepth;
- debug_opt(&state, 2, "data flow: mindepth --> %d\n", ctx->mindepth);
- }
-
- if (optlevel >= 4 && maxdepth < ctx->maxdepth) {
- if (maxdepth < INT_MIN) {
- maxdepth = INT_MIN;
- }
- ctx->maxdepth = maxdepth;
- debug_opt(&state, 4, "data flow: maxdepth --> %d\n", ctx->maxdepth);
- }
-
- return 0;
-}
diff --git a/opt.h b/opt.h
deleted file mode 100644
index 5f8180d..0000000
--- a/opt.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Optimization.
- */
-
-#ifndef BFS_OPT_H
-#define BFS_OPT_H
-
-struct bfs_ctx;
-
-/**
- * Apply optimizations to the command line.
- *
- * @param ctx
- * The bfs context to optimize.
- * @return
- * 0 if successful, -1 on error.
- */
-int bfs_optimize(struct bfs_ctx *ctx);
-
-#endif // BFS_OPT_H
-
diff --git a/parse.c b/parse.c
deleted file mode 100644
index ad8e89c..0000000
--- a/parse.c
+++ /dev/null
@@ -1,3787 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * The command line parser. Expressions are parsed by recursive descent, with a
- * grammar described in the comments of the parse_*() functions. The parser
- * also accepts flags and paths at any point in the expression, by treating
- * flags like always-true options, and skipping over paths wherever they appear.
- */
-
-#include "parse.h"
-#include "bfs.h"
-#include "bftw.h"
-#include "color.h"
-#include "ctx.h"
-#include "darray.h"
-#include "diag.h"
-#include "dir.h"
-#include "eval.h"
-#include "exec.h"
-#include "expr.h"
-#include "fsade.h"
-#include "opt.h"
-#include "printf.h"
-#include "pwcache.h"
-#include "spawn.h"
-#include "stat.h"
-#include "time.h"
-#include "typo.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <fnmatch.h>
-#include <grp.h>
-#include <limits.h>
-#include <pwd.h>
-#include <regex.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/time.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <time.h>
-#include <unistd.h>
-
-// Strings printed by -D tree for "fake" expressions
-static char *fake_and_arg = "-a";
-static char *fake_false_arg = "-false";
-static char *fake_hidden_arg = "-hidden";
-static char *fake_or_arg = "-o";
-static char *fake_print_arg = "-print";
-static char *fake_true_arg = "-true";
-
-// Cost estimation constants
-#define FAST_COST 40.0
-#define STAT_COST 1000.0
-#define PRINT_COST 20000.0
-
-struct expr expr_true = {
- .eval = eval_true,
- .lhs = NULL,
- .rhs = NULL,
- .pure = true,
- .always_true = true,
- .cost = FAST_COST,
- .probability = 1.0,
- .argc = 1,
- .argv = &fake_true_arg,
-};
-
-struct expr expr_false = {
- .eval = eval_false,
- .lhs = NULL,
- .rhs = NULL,
- .pure = true,
- .always_false = true,
- .cost = FAST_COST,
- .probability = 0.0,
- .argc = 1,
- .argv = &fake_false_arg,
-};
-
-/**
- * Free an expression.
- */
-void free_expr(struct expr *expr) {
- if (!expr || expr == &expr_true || expr == &expr_false) {
- return;
- }
-
- if (expr->regex) {
- regfree(expr->regex);
- free(expr->regex);
- }
-
- bfs_printf_free(expr->printf);
- bfs_exec_free(expr->execbuf);
-
- free_expr(expr->lhs);
- free_expr(expr->rhs);
-
- free(expr);
-}
-
-struct expr *new_expr(eval_fn *eval, size_t argc, char **argv) {
- struct expr *expr = malloc(sizeof(*expr));
- if (!expr) {
- perror("malloc()");
- return NULL;
- }
-
- expr->eval = eval;
- expr->lhs = NULL;
- expr->rhs = NULL;
- expr->pure = false;
- expr->always_true = false;
- expr->always_false = false;
- expr->cost = FAST_COST;
- expr->probability = 0.5;
- expr->evaluations = 0;
- expr->successes = 0;
- expr->elapsed.tv_sec = 0;
- expr->elapsed.tv_nsec = 0;
- expr->argc = argc;
- expr->argv = argv;
- expr->cfile = NULL;
- expr->regex = NULL;
- expr->execbuf = NULL;
- expr->printf = NULL;
- expr->persistent_fds = 0;
- expr->ephemeral_fds = 0;
- return expr;
-}
-
-/**
- * Create a new unary expression.
- */
-static struct expr *new_unary_expr(eval_fn *eval, struct expr *rhs, char **argv) {
- struct expr *expr = new_expr(eval, 1, argv);
- if (!expr) {
- free_expr(rhs);
- return NULL;
- }
-
- expr->rhs = rhs;
- expr->persistent_fds = rhs->persistent_fds;
- expr->ephemeral_fds = rhs->ephemeral_fds;
- return expr;
-}
-
-/**
- * Create a new binary expression.
- */
-static struct expr *new_binary_expr(eval_fn *eval, struct expr *lhs, struct expr *rhs, char **argv) {
- struct expr *expr = new_expr(eval, 1, argv);
- if (!expr) {
- free_expr(rhs);
- free_expr(lhs);
- return NULL;
- }
-
- expr->lhs = lhs;
- expr->rhs = rhs;
- expr->persistent_fds = lhs->persistent_fds + rhs->persistent_fds;
- if (lhs->ephemeral_fds > rhs->ephemeral_fds) {
- expr->ephemeral_fds = lhs->ephemeral_fds;
- } else {
- expr->ephemeral_fds = rhs->ephemeral_fds;
- }
- return expr;
-}
-
-/**
- * Check if an expression never returns.
- */
-bool expr_never_returns(const struct expr *expr) {
- // Expressions that never return are vacuously both always true and always false
- return expr->always_true && expr->always_false;
-}
-
-/**
- * Set an expression to always return true.
- */
-static void expr_set_always_true(struct expr *expr) {
- expr->always_true = true;
- expr->probability = 1.0;
-}
-
-/**
- * Set an expression to never return.
- */
-static void expr_set_never_returns(struct expr *expr) {
- expr->always_true = expr->always_false = true;
-}
-
-/**
- * Color use flags.
- */
-enum use_color {
- COLOR_NEVER,
- COLOR_AUTO,
- COLOR_ALWAYS,
-};
-
-/**
- * Ephemeral state for parsing the command line.
- */
-struct parser_state {
- /** The command line being constructed. */
- struct bfs_ctx *ctx;
- /** The command line arguments being parsed. */
- char **argv;
- /** The name of this program. */
- const char *command;
-
- /** The current regex flags to use. */
- int regex_flags;
-
- /** Whether stdout is a terminal. */
- bool stdout_tty;
- /** Whether this session is interactive (stdin and stderr are each a terminal). */
- bool interactive;
- /** Whether stdin has been consumed by -files0-from -. */
- bool stdin_consumed;
- /** Whether -color or -nocolor has been passed. */
- enum use_color use_color;
- /** Whether a -print action is implied. */
- bool implicit_print;
- /** Whether the default root "." should be used. */
- bool implicit_root;
- /** Whether the expression has started. */
- bool expr_started;
- /** Whether any non-option arguments have been encountered. */
- bool non_option_seen;
- /** Whether an information option like -help or -version was passed. */
- bool just_info;
- /** Whether we are currently parsing an -exclude expression. */
- bool excluding;
-
- /** The last non-path argument. */
- const char *last_arg;
- /** A "-depth"-type argument if any. */
- const char *depth_arg;
- /** A "-prune"-type argument if any. */
- const char *prune_arg;
- /** A "-mount"-type argument if any. */
- const char *mount_arg;
- /** An "-xdev"-type argument if any. */
- const char *xdev_arg;
- /** An "-ok"-type argument if any. */
- const char *ok_arg;
-
- /** The current time. */
- struct timespec now;
-};
-
-/**
- * Possible token types.
- */
-enum token_type {
- /** A flag. */
- T_FLAG,
- /** A root path. */
- T_PATH,
- /** An option. */
- T_OPTION,
- /** A test. */
- T_TEST,
- /** An action. */
- T_ACTION,
- /** An operator. */
- T_OPERATOR,
-};
-
-/**
- * Print an error message during parsing.
- */
-BFS_FORMATTER(2, 3)
-static void parse_error(const struct parser_state *state, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bfs_verror(state->ctx, format, args);
- va_end(args);
-}
-
-/**
- * Print a low-level error message during parsing.
- */
-static void parse_perror(const struct parser_state *state, const char *str) {
- bfs_perror(state->ctx, str);
-}
-
-/**
- * Print a warning message during parsing.
- */
-BFS_FORMATTER(2, 3)
-static bool parse_warning(const struct parser_state *state, const char *format, ...) {
- va_list args;
- va_start(args, format);
- bool ret = bfs_vwarning(state->ctx, format, args);
- va_end(args);
- return ret;
-}
-
-/**
- * Fill in a "-print"-type expression.
- */
-static void init_print_expr(struct parser_state *state, struct expr *expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- expr->cfile = state->ctx->cout;
-}
-
-/**
- * Open a file for an expression.
- */
-static int expr_open(struct parser_state *state, struct expr *expr, const char *path) {
- struct bfs_ctx *ctx = state->ctx;
-
- CFILE *cfile = cfopen(path, state->use_color ? ctx->colors : NULL);
- if (!cfile) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", expr->argv[0], path);
- return -1;
- }
-
- CFILE *dedup = bfs_ctx_dedup(ctx, cfile, path);
- if (!dedup) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", expr->argv[0], path);
- cfclose(cfile);
- return -1;
- }
-
- expr->cfile = dedup;
-
- if (dedup != cfile) {
- cfclose(cfile);
- }
- return 0;
-}
-
-/**
- * Invoke bfs_stat() on an argument.
- */
-static int stat_arg(const struct parser_state *state, struct expr *expr, struct bfs_stat *sb) {
- const struct bfs_ctx *ctx = state->ctx;
-
- bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
- enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW;
-
- int ret = bfs_stat(AT_FDCWD, expr->sdata, flags, sb);
- if (ret != 0) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", expr->argv[0], expr->sdata);
- }
- return ret;
-}
-
-/**
- * Parse the expression specified on the command line.
- */
-static struct expr *parse_expr(struct parser_state *state);
-
-/**
- * Advance by a single token.
- */
-static char **parser_advance(struct parser_state *state, enum token_type type, size_t argc) {
- if (type != T_FLAG && type != T_PATH) {
- state->expr_started = true;
-
- if (type != T_OPTION) {
- state->non_option_seen = true;
- }
- }
-
- if (type != T_PATH) {
- state->last_arg = *state->argv;
- }
-
- char **argv = state->argv;
- state->argv += argc;
- return argv;
-}
-
-/**
- * Parse a root path.
- */
-static int parse_root(struct parser_state *state, const char *path) {
- char *copy = strdup(path);
- if (!copy) {
- parse_perror(state, "strdup()");
- return -1;
- }
-
- struct bfs_ctx *ctx = state->ctx;
- if (DARRAY_PUSH(&ctx->paths, &copy) != 0) {
- parse_perror(state, "DARRAY_PUSH()");
- free(copy);
- return -1;
- }
-
- state->implicit_root = false;
- return 0;
-}
-
-/**
- * While parsing an expression, skip any paths and add them to ctx->paths.
- */
-static int skip_paths(struct parser_state *state) {
- while (true) {
- const char *arg = state->argv[0];
- if (!arg) {
- return 0;
- }
-
- if (arg[0] == '-') {
- if (strcmp(arg, "--") == 0) {
- // find uses -- to separate flags from the rest
- // of the command line. We allow mixing flags
- // and paths/predicates, so we just ignore --.
- parser_advance(state, T_FLAG, 1);
- continue;
- }
- if (strcmp(arg, "-") != 0) {
- // - by itself is a file name. Anything else
- // starting with - is a flag/predicate.
- return 0;
- }
- }
-
- // By POSIX, these are always options
- if (strcmp(arg, "(") == 0 || strcmp(arg, "!") == 0) {
- return 0;
- }
-
- if (state->expr_started) {
- // By POSIX, these can be paths. We only treat them as
- // such at the beginning of the command line.
- if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) {
- return 0;
- }
- }
-
- if (parse_root(state, arg) != 0) {
- return -1;
- }
-
- parser_advance(state, T_PATH, 1);
- }
-}
-
-/** Integer parsing flags. */
-enum int_flags {
- IF_BASE_MASK = 0x03F,
- IF_INT = 0x040,
- IF_LONG = 0x080,
- IF_LONG_LONG = 0x0C0,
- IF_SIZE_MASK = 0x0C0,
- IF_UNSIGNED = 0x100,
- IF_PARTIAL_OK = 0x200,
- IF_QUIET = 0x400,
-};
-
-/**
- * Parse an integer.
- */
-static const char *parse_int(const struct parser_state *state, const char *str, void *result, enum int_flags flags) {
- char *endptr;
-
- int base = flags & IF_BASE_MASK;
- if (base == 0) {
- base = 10;
- }
-
- errno = 0;
- long long value = strtoll(str, &endptr, base);
- if (errno != 0) {
- goto bad;
- }
-
- if (endptr == str) {
- goto bad;
- }
-
- if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') {
- goto bad;
- }
-
- if ((flags & IF_UNSIGNED) && value < 0) {
- goto bad;
- }
-
- switch (flags & IF_SIZE_MASK) {
- case IF_INT:
- if (value < INT_MIN || value > INT_MAX) {
- goto bad;
- }
- *(int *)result = value;
- break;
-
- case IF_LONG:
- if (value < LONG_MIN || value > LONG_MAX) {
- goto bad;
- }
- *(long *)result = value;
- break;
-
- case IF_LONG_LONG:
- *(long long *)result = value;
- break;
-
- default:
- assert(!"Invalid int size");
- goto bad;
- }
-
- return endptr;
-
-bad:
- if (!(flags & IF_QUIET)) {
- parse_error(state, "${bld}%s${rs} is not a valid integer.\n", str);
- }
- return NULL;
-}
-
-/**
- * Parse an integer and a comparison flag.
- */
-static const char *parse_icmp(const struct parser_state *state, const char *str, struct expr *expr, enum int_flags flags) {
- switch (str[0]) {
- case '-':
- expr->cmp_flag = CMP_LESS;
- ++str;
- break;
- case '+':
- expr->cmp_flag = CMP_GREATER;
- ++str;
- break;
- default:
- expr->cmp_flag = CMP_EXACT;
- break;
- }
-
- return parse_int(state, str, &expr->idata, flags | IF_LONG_LONG | IF_UNSIGNED);
-}
-
-/**
- * Check if a string could be an integer comparison.
- */
-static bool looks_like_icmp(const char *str) {
- int i;
-
- // One +/- for the comparison flag, one for the sign
- for (i = 0; i < 2; ++i) {
- if (str[i] != '-' && str[i] != '+') {
- break;
- }
- }
-
- return str[i] >= '0' && str[i] <= '9';
-}
-
-/**
- * Parse a single flag.
- */
-static struct expr *parse_flag(struct parser_state *state, size_t argc) {
- parser_advance(state, T_FLAG, argc);
- return &expr_true;
-}
-
-/**
- * Parse a flag that doesn't take a value.
- */
-static struct expr *parse_nullary_flag(struct parser_state *state) {
- return parse_flag(state, 1);
-}
-
-/**
- * Parse a flag that takes a single value.
- */
-static struct expr *parse_unary_flag(struct parser_state *state) {
- return parse_flag(state, 2);
-}
-
-/**
- * Parse a single option.
- */
-static struct expr *parse_option(struct parser_state *state, size_t argc) {
- const char *arg = *parser_advance(state, T_OPTION, argc);
-
- if (state->non_option_seen) {
- parse_warning(state,
- "The ${blu}%s${rs} option applies to the entire command line. For clarity, place\n"
- "it before any non-option arguments.\n\n",
- arg);
- }
-
- return &expr_true;
-}
-
-/**
- * Parse an option that doesn't take a value.
- */
-static struct expr *parse_nullary_option(struct parser_state *state) {
- return parse_option(state, 1);
-}
-
-/**
- * Parse an option that takes a value.
- */
-static struct expr *parse_unary_option(struct parser_state *state) {
- return parse_option(state, 2);
-}
-
-/**
- * Parse a single positional option.
- */
-static struct expr *parse_positional_option(struct parser_state *state, size_t argc) {
- parser_advance(state, T_OPTION, argc);
- return &expr_true;
-}
-
-/**
- * Parse a positional option that doesn't take a value.
- */
-static struct expr *parse_nullary_positional_option(struct parser_state *state) {
- return parse_positional_option(state, 1);
-}
-
-/**
- * Parse a positional option that takes a single value.
- */
-static struct expr *parse_unary_positional_option(struct parser_state *state) {
- return parse_positional_option(state, 2);
-}
-
-/**
- * Parse a single test.
- */
-static struct expr *parse_test(struct parser_state *state, eval_fn *eval, size_t argc) {
- char **argv = parser_advance(state, T_TEST, argc);
- struct expr *expr = new_expr(eval, argc, argv);
- if (expr) {
- expr->pure = true;
- }
- return expr;
-}
-
-/**
- * Parse a test that doesn't take a value.
- */
-static struct expr *parse_nullary_test(struct parser_state *state, eval_fn *eval) {
- return parse_test(state, eval, 1);
-}
-
-/**
- * Parse a test that takes a value.
- */
-static struct expr *parse_unary_test(struct parser_state *state, eval_fn *eval) {
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
- if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
- return NULL;
- }
-
- struct expr *expr = parse_test(state, eval, 2);
- if (expr) {
- expr->sdata = value;
- }
- return expr;
-}
-
-/**
- * Parse a single action.
- */
-static struct expr *parse_action(struct parser_state *state, eval_fn *eval, size_t argc) {
- char **argv = state->argv;
-
- if (state->excluding) {
- parse_error(state, "The ${blu}%s${rs} action is not supported within ${red}-exclude${rs}.\n", argv[0]);
- return NULL;
- }
-
- if (eval != eval_prune && eval != eval_quit) {
- state->implicit_print = false;
- }
-
- parser_advance(state, T_ACTION, argc);
- return new_expr(eval, argc, argv);
-}
-
-/**
- * Parse an action that takes no arguments.
- */
-static struct expr *parse_nullary_action(struct parser_state *state, eval_fn *eval) {
- return parse_action(state, eval, 1);
-}
-
-/**
- * Parse an action that takes one argument.
- */
-static struct expr *parse_unary_action(struct parser_state *state, eval_fn *eval) {
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
- if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
- return NULL;
- }
-
- struct expr *expr = parse_action(state, eval, 2);
- if (expr) {
- expr->sdata = value;
- }
- return expr;
-}
-
-/**
- * Parse a test expression with integer data and a comparison flag.
- */
-static struct expr *parse_test_icmp(struct parser_state *state, eval_fn *eval) {
- struct expr *expr = parse_unary_test(state, eval);
- if (!expr) {
- return NULL;
- }
-
- if (!parse_icmp(state, expr->sdata, expr, 0)) {
- free_expr(expr);
- return NULL;
- }
-
- return expr;
-}
-
-/**
- * Print usage information for -D.
- */
-static void debug_help(CFILE *cfile) {
- cfprintf(cfile, "Supported debug flags:\n\n");
-
- cfprintf(cfile, " ${bld}help${rs}: This message.\n");
- cfprintf(cfile, " ${bld}cost${rs}: Show cost estimates.\n");
- cfprintf(cfile, " ${bld}exec${rs}: Print executed command details.\n");
- cfprintf(cfile, " ${bld}opt${rs}: Print optimization details.\n");
- cfprintf(cfile, " ${bld}rates${rs}: Print predicate success rates.\n");
- cfprintf(cfile, " ${bld}search${rs}: Trace the filesystem traversal.\n");
- cfprintf(cfile, " ${bld}stat${rs}: Trace all stat() calls.\n");
- cfprintf(cfile, " ${bld}tree${rs}: Print the parse tree.\n");
- cfprintf(cfile, " ${bld}all${rs}: All debug flags at once.\n");
-}
-
-/** Check if a substring matches a debug flag. */
-static bool parse_debug_flag(const char *flag, size_t len, const char *expected) {
- if (len == strlen(expected)) {
- return strncmp(flag, expected, len) == 0;
- } else {
- return false;
- }
-}
-
-/**
- * Parse -D FLAG.
- */
-static struct expr *parse_debug(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
-
- const char *arg = state->argv[0];
- const char *flags = state->argv[1];
- if (!flags) {
- parse_error(state, "${cyn}%s${rs} needs a flag.\n\n", arg);
- debug_help(ctx->cerr);
- return NULL;
- }
-
- bool unrecognized = false;
-
- for (const char *flag = flags, *next; flag; flag = next) {
- size_t len = strcspn(flag, ",");
- if (flag[len]) {
- next = flag + len + 1;
- } else {
- next = NULL;
- }
-
- if (parse_debug_flag(flag, len, "help")) {
- debug_help(ctx->cout);
- state->just_info = true;
- return NULL;
- } else if (parse_debug_flag(flag, len, "all")) {
- ctx->debug = DEBUG_ALL;
- continue;
- }
-
- enum debug_flags i;
- for (i = 1; DEBUG_ALL & i; i <<= 1) {
- const char *name = debug_flag_name(i);
- if (parse_debug_flag(flag, len, name)) {
- break;
- }
- }
-
- if (DEBUG_ALL & i) {
- ctx->debug |= i;
- } else {
- if (parse_warning(state, "Unrecognized debug flag ${bld}")) {
- fwrite(flag, 1, len, stderr);
- cfprintf(ctx->cerr, "${rs}.\n\n");
- unrecognized = true;
- }
- }
- }
-
- if (unrecognized) {
- debug_help(ctx->cerr);
- cfprintf(ctx->cerr, "\n");
- }
-
- return parse_unary_flag(state);
-}
-
-/**
- * Parse -On.
- */
-static struct expr *parse_optlevel(struct parser_state *state, int arg1, int arg2) {
- int *optlevel = &state->ctx->optlevel;
-
- if (strcmp(state->argv[0], "-Ofast") == 0) {
- *optlevel = 4;
- } else if (!parse_int(state, state->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) {
- return NULL;
- }
-
- if (*optlevel > 4) {
- parse_warning(state, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", state->argv[0] + 2);
- }
-
- return parse_nullary_flag(state);
-}
-
-/**
- * Parse -[PHL], -(no)?follow.
- */
-static struct expr *parse_follow(struct parser_state *state, int flags, int option) {
- struct bfs_ctx *ctx = state->ctx;
- ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
- ctx->flags |= flags;
- if (option) {
- return parse_nullary_positional_option(state);
- } else {
- return parse_nullary_flag(state);
- }
-}
-
-/**
- * Parse -X.
- */
-static struct expr *parse_xargs_safe(struct parser_state *state, int arg1, int arg2) {
- state->ctx->xargs_safe = true;
- return parse_nullary_flag(state);
-}
-
-/**
- * Parse -executable, -readable, -writable
- */
-static struct expr *parse_access(struct parser_state *state, int flag, int arg2) {
- struct expr *expr = parse_nullary_test(state, eval_access);
- if (!expr) {
- return NULL;
- }
-
- expr->idata = flag;
- expr->cost = STAT_COST;
-
- switch (flag) {
- case R_OK:
- expr->probability = 0.99;
- break;
- case W_OK:
- expr->probability = 0.8;
- break;
- case X_OK:
- expr->probability = 0.2;
- break;
- }
-
- return expr;
-}
-
-/**
- * Parse -acl.
- */
-static struct expr *parse_acl(struct parser_state *state, int flag, int arg2) {
-#if BFS_CAN_CHECK_ACL
- struct expr *expr = parse_nullary_test(state, eval_acl);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.00002;
- }
- return expr;
-#else
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", state->argv[0]);
- return NULL;
-#endif
-}
-
-/**
- * Parse -[aBcm]?newer.
- */
-static struct expr *parse_newer(struct parser_state *state, int field, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_newer);
- if (!expr) {
- return NULL;
- }
-
- struct bfs_stat sb;
- if (stat_arg(state, expr, &sb) != 0) {
- goto fail;
- }
-
- expr->cost = STAT_COST;
- expr->reftime = sb.mtime;
- expr->stat_field = field;
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -[aBcm]min.
- */
-static struct expr *parse_min(struct parser_state *state, int field, int arg2) {
- struct expr *expr = parse_test_icmp(state, eval_time);
- if (!expr) {
- return NULL;
- }
-
- expr->cost = STAT_COST;
- expr->reftime = state->now;
- expr->stat_field = field;
- expr->time_unit = MINUTES;
- return expr;
-}
-
-/**
- * Parse -[aBcm]time.
- */
-static struct expr *parse_time(struct parser_state *state, int field, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_time);
- if (!expr) {
- return NULL;
- }
-
- expr->cost = STAT_COST;
- expr->reftime = state->now;
- expr->stat_field = field;
-
- const char *tail = parse_icmp(state, expr->sdata, expr, IF_PARTIAL_OK);
- if (!tail) {
- goto fail;
- }
-
- if (!*tail) {
- expr->time_unit = DAYS;
- return expr;
- }
-
- unsigned long long time = expr->idata;
- expr->idata = 0;
-
- while (true) {
- switch (*tail) {
- case 'w':
- time *= 7;
- BFS_FALLTHROUGH;
- case 'd':
- time *= 24;
- BFS_FALLTHROUGH;
- case 'h':
- time *= 60;
- BFS_FALLTHROUGH;
- case 'm':
- time *= 60;
- BFS_FALLTHROUGH;
- case 's':
- break;
- default:
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Unknown time unit ${bld}%c${rs}.\n",
- expr->argv[0], expr->argv[1], *tail);
- goto fail;
- }
-
- expr->idata += time;
-
- if (!*++tail) {
- break;
- }
-
- tail = parse_int(state, tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED);
- if (!tail) {
- goto fail;
- }
- if (!*tail) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Missing time unit.\n",
- expr->argv[0], expr->argv[1]);
- goto fail;
- }
- }
-
- expr->time_unit = SECONDS;
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -capable.
- */
-static struct expr *parse_capable(struct parser_state *state, int flag, int arg2) {
-#if BFS_CAN_CHECK_CAPABILITIES
- struct expr *expr = parse_nullary_test(state, eval_capable);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.000002;
- }
- return expr;
-#else
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", state->argv[0]);
- return NULL;
-#endif
-}
-
-/**
- * Parse -(no)?color.
- */
-static struct expr *parse_color(struct parser_state *state, int color, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
- struct colors *colors = ctx->colors;
-
- if (color) {
- if (!colors) {
- parse_error(state, "${blu}%s${rs}: %s.\n", state->argv[0], strerror(ctx->colors_error));
- return NULL;
- }
-
- state->use_color = COLOR_ALWAYS;
- ctx->cout->colors = colors;
- ctx->cerr->colors = colors;
- } else {
- state->use_color = COLOR_NEVER;
- ctx->cout->colors = NULL;
- ctx->cerr->colors = NULL;
- }
-
- return parse_nullary_option(state);
-}
-
-/**
- * Parse -{false,true}.
- */
-static struct expr *parse_const(struct parser_state *state, int value, int arg2) {
- parser_advance(state, T_TEST, 1);
- return value ? &expr_true : &expr_false;
-}
-
-/**
- * Parse -daystart.
- */
-static struct expr *parse_daystart(struct parser_state *state, int arg1, int arg2) {
- struct tm tm;
- if (xlocaltime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xlocaltime()");
- return NULL;
- }
-
- if (tm.tm_hour || tm.tm_min || tm.tm_sec || state->now.tv_nsec) {
- ++tm.tm_mday;
- }
- tm.tm_hour = 0;
- tm.tm_min = 0;
- tm.tm_sec = 0;
-
- time_t time;
- if (xmktime(&tm, &time) != 0) {
- parse_perror(state, "xmktime()");
- return NULL;
- }
-
- state->now.tv_sec = time;
- state->now.tv_nsec = 0;
-
- return parse_nullary_positional_option(state);
-}
-
-/**
- * Parse -delete.
- */
-static struct expr *parse_delete(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_POST_ORDER;
- state->depth_arg = state->argv[0];
- return parse_nullary_action(state, eval_delete);
-}
-
-/**
- * Parse -d.
- */
-static struct expr *parse_depth(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_POST_ORDER;
- state->depth_arg = state->argv[0];
- return parse_nullary_flag(state);
-}
-
-/**
- * Parse -depth [N].
- */
-static struct expr *parse_depth_n(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[1];
- if (arg && looks_like_icmp(arg)) {
- return parse_test_icmp(state, eval_depth);
- } else {
- return parse_depth(state, arg1, arg2);
- }
-}
-
-/**
- * Parse -{min,max}depth N.
- */
-static struct expr *parse_depth_limit(struct parser_state *state, int is_min, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
- if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
- return NULL;
- }
-
- int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth;
- if (!parse_int(state, value, depth, IF_INT | IF_UNSIGNED)) {
- return NULL;
- }
-
- return parse_unary_option(state);
-}
-
-/**
- * Parse -empty.
- */
-static struct expr *parse_empty(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_test(state, eval_empty);
- if (!expr) {
- return NULL;
- }
-
- expr->cost = 2000.0;
- expr->probability = 0.01;
-
- if (state->ctx->optlevel < 4) {
- // Since -empty attempts to open and read directories, it may
- // have side effects such as reporting permission errors, and
- // thus shouldn't be re-ordered without aggressive optimizations
- expr->pure = false;
- }
-
- expr->ephemeral_fds = 1;
-
- return expr;
-}
-
-/**
- * Parse -exec(dir)?/-ok(dir)?.
- */
-static struct expr *parse_exec(struct parser_state *state, int flags, int arg2) {
- struct bfs_exec *execbuf = bfs_exec_parse(state->ctx, state->argv, flags);
- if (!execbuf) {
- return NULL;
- }
-
- struct expr *expr = parse_action(state, eval_exec, execbuf->tmpl_argc + 2);
- if (!expr) {
- bfs_exec_free(execbuf);
- return NULL;
- }
-
- expr->execbuf = execbuf;
-
- if (execbuf->flags & BFS_EXEC_MULTI) {
- expr_set_always_true(expr);
- } else {
- expr->cost = 1000000.0;
- }
-
- expr->ephemeral_fds = 2;
- if (execbuf->flags & BFS_EXEC_CHDIR) {
- if (execbuf->flags & BFS_EXEC_MULTI) {
- expr->persistent_fds = 1;
- } else {
- ++expr->ephemeral_fds;
- }
- }
-
- if (execbuf->flags & BFS_EXEC_CONFIRM) {
- state->ok_arg = expr->argv[0];
- }
-
- return expr;
-}
-
-/**
- * Parse -exit [STATUS].
- */
-static struct expr *parse_exit(struct parser_state *state, int arg1, int arg2) {
- size_t argc = 1;
- const char *value = state->argv[1];
-
- int status = EXIT_SUCCESS;
- if (value && parse_int(state, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
- argc = 2;
- }
-
- struct expr *expr = parse_action(state, eval_exit, argc);
- if (expr) {
- expr_set_never_returns(expr);
- expr->idata = status;
- }
- return expr;
-}
-
-/**
- * Parse -f PATH.
- */
-static struct expr *parse_f(struct parser_state *state, int arg1, int arg2) {
- parser_advance(state, T_FLAG, 1);
-
- const char *path = state->argv[0];
- if (!path) {
- parse_error(state, "${cyn}-f${rs} requires a path.\n");
- return NULL;
- }
-
- if (parse_root(state, path) != 0) {
- return NULL;
- }
-
- parser_advance(state, T_PATH, 1);
- return &expr_true;
-}
-
-/**
- * Parse -files0-from PATH.
- */
-static struct expr *parse_files0_from(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
- const char *from = state->argv[1];
- if (!from) {
- parse_error(state, "${blu}%s${rs} requires a path.\n", arg);
- return NULL;
- }
-
- FILE *file;
- if (strcmp(from, "-") == 0) {
- file = stdin;
- } else {
- file = fopen(from, "rb");
- }
- if (!file) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", arg, from);
- return NULL;
- }
-
- struct expr *expr = parse_unary_positional_option(state);
-
- while (true) {
- char *path = xgetdelim(file, '\0');
- if (!path) {
- if (errno) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", arg, from);
- expr = NULL;
- }
- break;
- }
-
- int ret = parse_root(state, path);
- free(path);
- if (ret != 0) {
- expr = NULL;
- break;
- }
- }
-
- if (file == stdin) {
- state->stdin_consumed = true;
- } else {
- fclose(file);
- }
-
- state->implicit_root = false;
- return expr;
-}
-
-/**
- * Parse -flags FLAGS.
- */
-static struct expr *parse_flags(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_flags);
- if (!expr) {
- return NULL;
- }
-
- const char *flags = expr->sdata;
- switch (flags[0]) {
- case '-':
- expr->mode_cmp = MODE_ALL;
- ++flags;
- break;
- case '+':
- expr->mode_cmp = MODE_ANY;
- ++flags;
- break;
- default:
- expr->mode_cmp = MODE_EXACT;
- break;
- }
-
- if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) {
- if (errno == ENOTSUP) {
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", expr->argv[0]);
- } else {
- parse_error(state, "${blu}%s${rs}: Invalid flags ${bld}%s${rs}.\n", expr->argv[0], flags);
- }
- free_expr(expr);
- return NULL;
- }
-
- return expr;
-}
-
-/**
- * Parse -fls FILE.
- */
-static struct expr *parse_fls(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_action(state, eval_fls);
- if (!expr) {
- goto fail;
- }
-
- if (expr_open(state, expr, expr->sdata) != 0) {
- goto fail;
- }
-
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- expr->reftime = state->now;
-
- // We'll need these for user/group names, so initialize them now to
- // avoid EMFILE later
- bfs_ctx_users(state->ctx);
- bfs_ctx_groups(state->ctx);
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -fprint FILE.
- */
-static struct expr *parse_fprint(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_action(state, eval_fprint);
- if (expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- if (expr_open(state, expr, expr->sdata) != 0) {
- goto fail;
- }
- }
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -fprint0 FILE.
- */
-static struct expr *parse_fprint0(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_action(state, eval_fprint0);
- if (expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- if (expr_open(state, expr, expr->sdata) != 0) {
- goto fail;
- }
- }
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -fprintf FILE FORMAT.
- */
-static struct expr *parse_fprintf(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
-
- const char *file = state->argv[1];
- if (!file) {
- parse_error(state, "${blu}%s${rs} needs a file.\n", arg);
- return NULL;
- }
-
- const char *format = state->argv[2];
- if (!format) {
- parse_error(state, "${blu}%s${rs} needs a format string.\n", arg);
- return NULL;
- }
-
- struct expr *expr = parse_action(state, eval_fprintf, 3);
- if (!expr) {
- return NULL;
- }
-
- expr_set_always_true(expr);
-
- expr->cost = PRINT_COST;
-
- if (expr_open(state, expr, file) != 0) {
- goto fail;
- }
-
- expr->printf = bfs_printf_parse(state->ctx, format);
- if (!expr->printf) {
- goto fail;
- }
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -fstype TYPE.
- */
-static struct expr *parse_fstype(struct parser_state *state, int arg1, int arg2) {
- if (!bfs_ctx_mtab(state->ctx)) {
- parse_error(state, "Couldn't parse the mount table: %m.\n");
- return NULL;
- }
-
- struct expr *expr = parse_unary_test(state, eval_fstype);
- if (expr) {
- expr->cost = STAT_COST;
- }
- return expr;
-}
-
-/**
- * Parse -gid/-group.
- */
-static struct expr *parse_group(struct parser_state *state, int arg1, int arg2) {
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
- if (!groups) {
- parse_error(state, "Couldn't parse the group table: %m.\n");
- return NULL;
- }
-
- const char *arg = state->argv[0];
-
- struct expr *expr = parse_unary_test(state, eval_gid);
- if (!expr) {
- return NULL;
- }
-
- const struct group *grp = bfs_getgrnam(groups, expr->sdata);
- if (grp) {
- expr->idata = grp->gr_gid;
- expr->cmp_flag = CMP_EXACT;
- } else if (looks_like_icmp(expr->sdata)) {
- if (!parse_icmp(state, expr->sdata, expr, 0)) {
- goto fail;
- }
- } else {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: No such group.\n", arg, expr->sdata);
- goto fail;
- }
-
- expr->cost = STAT_COST;
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -unique.
- */
-static struct expr *parse_unique(struct parser_state *state, int arg1, int arg2) {
- state->ctx->unique = true;
- return parse_nullary_option(state);
-}
-
-/**
- * Parse -used N.
- */
-static struct expr *parse_used(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_test_icmp(state, eval_used);
- if (expr) {
- expr->cost = STAT_COST;
- }
- return expr;
-}
-
-/**
- * Parse -uid/-user.
- */
-static struct expr *parse_user(struct parser_state *state, int arg1, int arg2) {
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- if (!users) {
- parse_error(state, "Couldn't parse the user table: %m.\n");
- return NULL;
- }
-
- const char *arg = state->argv[0];
-
- struct expr *expr = parse_unary_test(state, eval_uid);
- if (!expr) {
- return NULL;
- }
-
- const struct passwd *pwd = bfs_getpwnam(users, expr->sdata);
- if (pwd) {
- expr->idata = pwd->pw_uid;
- expr->cmp_flag = CMP_EXACT;
- } else if (looks_like_icmp(expr->sdata)) {
- if (!parse_icmp(state, expr->sdata, expr, 0)) {
- goto fail;
- }
- } else {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: No such user.\n", arg, expr->sdata);
- goto fail;
- }
-
- expr->cost = STAT_COST;
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -hidden.
- */
-static struct expr *parse_hidden(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_test(state, eval_hidden);
- if (expr) {
- expr->probability = 0.01;
- }
- return expr;
-}
-
-/**
- * Parse -(no)?ignore_readdir_race.
- */
-static struct expr *parse_ignore_races(struct parser_state *state, int ignore, int arg2) {
- state->ctx->ignore_races = ignore;
- return parse_nullary_option(state);
-}
-
-/**
- * Parse -inum N.
- */
-static struct expr *parse_inum(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_test_icmp(state, eval_inum);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = expr->cmp_flag == CMP_EXACT ? 0.01 : 0.50;
- }
- return expr;
-}
-
-/**
- * Parse -links N.
- */
-static struct expr *parse_links(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_test_icmp(state, eval_links);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = expr_cmp(expr, 1) ? 0.99 : 0.01;
- }
- return expr;
-}
-
-/**
- * Parse -ls.
- */
-static struct expr *parse_ls(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_action(state, eval_fls);
- if (!expr) {
- return NULL;
- }
-
- init_print_expr(state, expr);
- expr->reftime = state->now;
-
- // We'll need these for user/group names, so initialize them now to
- // avoid EMFILE later
- bfs_ctx_users(state->ctx);
- bfs_ctx_groups(state->ctx);
-
- return expr;
-}
-
-/**
- * Parse -mount.
- */
-static struct expr *parse_mount(struct parser_state *state, int arg1, int arg2) {
- parse_warning(state,
- "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n"
- "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n",
- state->argv[0]);
-
- state->ctx->flags |= BFTW_PRUNE_MOUNTS;
- state->mount_arg = state->argv[0];
- return parse_nullary_option(state);
-}
-
-/**
- * Common code for fnmatch() tests.
- */
-static struct expr *parse_fnmatch(const struct parser_state *state, struct expr *expr, bool casefold) {
- if (!expr) {
- return NULL;
- }
-
- if (casefold) {
-#ifdef FNM_CASEFOLD
- expr->idata = FNM_CASEFOLD;
-#else
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", expr->argv[0]);
- free_expr(expr);
- return NULL;
-#endif
- } else {
- expr->idata = 0;
- }
-
- expr->cost = 400.0;
-
- if (strchr(expr->sdata, '*')) {
- expr->probability = 0.5;
- } else {
- expr->probability = 0.1;
- }
-
- return expr;
-}
-
-/**
- * Parse -i?name.
- */
-static struct expr *parse_name(struct parser_state *state, int casefold, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_name);
- return parse_fnmatch(state, expr, casefold);
-}
-
-/**
- * Parse -i?path, -i?wholename.
- */
-static struct expr *parse_path(struct parser_state *state, int casefold, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_path);
- return parse_fnmatch(state, expr, casefold);
-}
-
-/**
- * Parse -i?lname.
- */
-static struct expr *parse_lname(struct parser_state *state, int casefold, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_lname);
- return parse_fnmatch(state, expr, casefold);
-}
-
-/** Get the bfs_stat_field for X/Y in -newerXY. */
-static enum bfs_stat_field parse_newerxy_field(char c) {
- switch (c) {
- case 'a':
- return BFS_STAT_ATIME;
- case 'B':
- return BFS_STAT_BTIME;
- case 'c':
- return BFS_STAT_CTIME;
- case 'm':
- return BFS_STAT_MTIME;
- default:
- return 0;
- }
-}
-
-/** Parse an explicit reference timestamp for -newerXt and -*since. */
-static int parse_reftime(const struct parser_state *state, struct expr *expr) {
- if (parse_timestamp(expr->sdata, &expr->reftime) == 0) {
- return 0;
- } else if (errno != EINVAL) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %m.\n", expr->argv[0], expr->argv[1]);
- return -1;
- }
-
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Invalid timestamp.\n\n", expr->argv[0], expr->argv[1]);
- fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n");
-
- struct tm tm;
- if (xlocaltime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xlocaltime()");
- return -1;
- }
-
- int year = tm.tm_year + 1900;
- int month = tm.tm_mon + 1;
- fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday);
- fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
-
-#if __FreeBSD__
- int gmtoff = tm.tm_gmtoff;
-#else
- int gmtoff = -timezone;
-#endif
- int tz_hour = gmtoff/3600;
- int tz_min = (labs(gmtoff)/60)%60;
- fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n",
- year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min);
-
- if (xgmtime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xgmtime()");
- return -1;
- }
-
- year = tm.tm_year + 1900;
- month = tm.tm_mon + 1;
- fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02dZ\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
-
- return -1;
-}
-
-/**
- * Parse -newerXY.
- */
-static struct expr *parse_newerxy(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
- if (strlen(arg) != 8) {
- parse_error(state, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%s${rs}.\n", arg + 6);
- return NULL;
- }
-
- struct expr *expr = parse_unary_test(state, eval_newer);
- if (!expr) {
- goto fail;
- }
-
- expr->stat_field = parse_newerxy_field(arg[6]);
- if (!expr->stat_field) {
- parse_error(state,
- "${blu}%s${rs}: For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${er}%c${rs}.\n",
- arg, arg[6]);
- goto fail;
- }
-
- if (arg[7] == 't') {
- if (parse_reftime(state, expr) != 0) {
- goto fail;
- }
- } else {
- enum bfs_stat_field field = parse_newerxy_field(arg[7]);
- if (!field) {
- parse_error(state,
- "${blu}%s${rs}: For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${er}%c${rs}.\n",
- arg, arg[7]);
- goto fail;
- }
-
- struct bfs_stat sb;
- if (stat_arg(state, expr, &sb) != 0) {
- goto fail;
- }
-
-
- const struct timespec *reftime = bfs_stat_time(&sb, field);
- if (!reftime) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Couldn't get file %s.\n", arg, expr->sdata, bfs_stat_field_name(field));
- goto fail;
- }
-
- expr->reftime = *reftime;
- }
-
- expr->cost = STAT_COST;
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -nogroup.
- */
-static struct expr *parse_nogroup(struct parser_state *state, int arg1, int arg2) {
- if (!bfs_ctx_groups(state->ctx)) {
- parse_error(state, "Couldn't parse the group table: %m.\n");
- return NULL;
- }
-
- struct expr *expr = parse_nullary_test(state, eval_nogroup);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
-}
-
-/**
- * Parse -nohidden.
- */
-static struct expr *parse_nohidden(struct parser_state *state, int arg1, int arg2) {
- struct expr *hidden = new_expr(eval_hidden, 1, &fake_hidden_arg);
- if (!hidden) {
- return NULL;
- }
-
- hidden->probability = 0.01;
- hidden->pure = true;
-
- struct bfs_ctx *ctx = state->ctx;
- ctx->exclude = new_binary_expr(eval_or, ctx->exclude, hidden, &fake_or_arg);
- if (!ctx->exclude) {
- return NULL;
- }
-
- parser_advance(state, T_OPTION, 1);
- return &expr_true;
-}
-
-/**
- * Parse -noleaf.
- */
-static struct expr *parse_noleaf(struct parser_state *state, int arg1, int arg2) {
- parse_warning(state, "${ex}bfs${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", state->argv[0]);
- return parse_nullary_option(state);
-}
-
-/**
- * Parse -nouser.
- */
-static struct expr *parse_nouser(struct parser_state *state, int arg1, int arg2) {
- if (!bfs_ctx_users(state->ctx)) {
- parse_error(state, "Couldn't parse the user table: %m.\n");
- return NULL;
- }
-
- struct expr *expr = parse_nullary_test(state, eval_nouser);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
-}
-
-/**
- * Parse a permission mode like chmod(1).
- */
-static int parse_mode(const struct parser_state *state, const char *mode, struct expr *expr) {
- if (mode[0] >= '0' && mode[0] <= '9') {
- unsigned int parsed;
- if (!parse_int(state, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
- goto fail;
- }
- if (parsed > 07777) {
- goto fail;
- }
-
- expr->file_mode = parsed;
- expr->dir_mode = parsed;
- return 0;
- }
-
- expr->file_mode = 0;
- expr->dir_mode = 0;
-
- // Parse the same grammar as chmod(1), which looks like this:
- //
- // MODE : CLAUSE ["," CLAUSE]*
- //
- // CLAUSE : WHO* ACTION+
- //
- // WHO : "u" | "g" | "o" | "a"
- //
- // ACTION : OP PERM*
- // | OP PERMCOPY
- //
- // OP : "+" | "-" | "="
- //
- // PERM : "r" | "w" | "x" | "X" | "s" | "t"
- //
- // PERMCOPY : "u" | "g" | "o"
-
- // State machine state
- enum {
- MODE_CLAUSE,
- MODE_WHO,
- MODE_ACTION,
- MODE_ACTION_APPLY,
- MODE_OP,
- MODE_PERM,
- } mstate = MODE_CLAUSE;
-
- enum {
- MODE_PLUS,
- MODE_MINUS,
- MODE_EQUALS,
- } op;
-
- mode_t who;
- mode_t file_change;
- mode_t dir_change;
-
- const char *i = mode;
- while (true) {
- switch (mstate) {
- case MODE_CLAUSE:
- who = 0;
- mstate = MODE_WHO;
- BFS_FALLTHROUGH;
-
- case MODE_WHO:
- switch (*i) {
- case 'u':
- who |= 0700;
- break;
- case 'g':
- who |= 0070;
- break;
- case 'o':
- who |= 0007;
- break;
- case 'a':
- who |= 0777;
- break;
- default:
- mstate = MODE_ACTION;
- continue;
- }
- break;
-
- case MODE_ACTION_APPLY:
- switch (op) {
- case MODE_EQUALS:
- expr->file_mode &= ~who;
- expr->dir_mode &= ~who;
- BFS_FALLTHROUGH;
- case MODE_PLUS:
- expr->file_mode |= file_change;
- expr->dir_mode |= dir_change;
- break;
- case MODE_MINUS:
- expr->file_mode &= ~file_change;
- expr->dir_mode &= ~dir_change;
- break;
- }
- BFS_FALLTHROUGH;
-
- case MODE_ACTION:
- if (who == 0) {
- who = 0777;
- }
-
- switch (*i) {
- case '+':
- op = MODE_PLUS;
- mstate = MODE_OP;
- break;
- case '-':
- op = MODE_MINUS;
- mstate = MODE_OP;
- break;
- case '=':
- op = MODE_EQUALS;
- mstate = MODE_OP;
- break;
-
- case ',':
- if (mstate == MODE_ACTION_APPLY) {
- mstate = MODE_CLAUSE;
- } else {
- goto fail;
- }
- break;
-
- case '\0':
- if (mstate == MODE_ACTION_APPLY) {
- goto done;
- } else {
- goto fail;
- }
-
- default:
- goto fail;
- }
- break;
-
- case MODE_OP:
- switch (*i) {
- case 'u':
- file_change = (expr->file_mode >> 6) & 07;
- dir_change = (expr->dir_mode >> 6) & 07;
- break;
- case 'g':
- file_change = (expr->file_mode >> 3) & 07;
- dir_change = (expr->dir_mode >> 3) & 07;
- break;
- case 'o':
- file_change = expr->file_mode & 07;
- dir_change = expr->dir_mode & 07;
- break;
-
- default:
- file_change = 0;
- dir_change = 0;
- mstate = MODE_PERM;
- continue;
- }
-
- file_change |= (file_change << 6) | (file_change << 3);
- file_change &= who;
- dir_change |= (dir_change << 6) | (dir_change << 3);
- dir_change &= who;
- mstate = MODE_ACTION_APPLY;
- break;
-
- case MODE_PERM:
- switch (*i) {
- case 'r':
- file_change |= who & 0444;
- dir_change |= who & 0444;
- break;
- case 'w':
- file_change |= who & 0222;
- dir_change |= who & 0222;
- break;
- case 'x':
- file_change |= who & 0111;
- BFS_FALLTHROUGH;
- case 'X':
- dir_change |= who & 0111;
- break;
- case 's':
- if (who & 0700) {
- file_change |= S_ISUID;
- dir_change |= S_ISUID;
- }
- if (who & 0070) {
- file_change |= S_ISGID;
- dir_change |= S_ISGID;
- }
- break;
- case 't':
- if (who & 0007) {
- file_change |= S_ISVTX;
- dir_change |= S_ISVTX;
- }
- break;
- default:
- mstate = MODE_ACTION_APPLY;
- continue;
- }
- break;
- }
-
- ++i;
- }
-
-done:
- return 0;
-
-fail:
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Invalid mode.\n", expr->argv[0], mode);
- return -1;
-}
-
-/**
- * Parse -perm MODE.
- */
-static struct expr *parse_perm(struct parser_state *state, int field, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_perm);
- if (!expr) {
- return NULL;
- }
-
- const char *mode = expr->sdata;
- switch (mode[0]) {
- case '-':
- expr->mode_cmp = MODE_ALL;
- ++mode;
- break;
- case '/':
- expr->mode_cmp = MODE_ANY;
- ++mode;
- break;
- case '+':
- if (mode[1] >= '0' && mode[1] <= '9') {
- expr->mode_cmp = MODE_ANY;
- ++mode;
- break;
- }
- BFS_FALLTHROUGH;
- default:
- expr->mode_cmp = MODE_EXACT;
- break;
- }
-
- if (parse_mode(state, mode, expr) != 0) {
- goto fail;
- }
-
- expr->cost = STAT_COST;
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -print.
- */
-static struct expr *parse_print(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_action(state, eval_fprint);
- if (expr) {
- init_print_expr(state, expr);
- }
- return expr;
-}
-
-/**
- * Parse -print0.
- */
-static struct expr *parse_print0(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_action(state, eval_fprint0);
- if (expr) {
- init_print_expr(state, expr);
- }
- return expr;
-}
-
-/**
- * Parse -printf FORMAT.
- */
-static struct expr *parse_printf(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_action(state, eval_fprintf);
- if (!expr) {
- return NULL;
- }
-
- init_print_expr(state, expr);
-
- expr->printf = bfs_printf_parse(state->ctx, expr->sdata);
- if (!expr->printf) {
- free_expr(expr);
- return NULL;
- }
-
- return expr;
-}
-
-/**
- * Parse -printx.
- */
-static struct expr *parse_printx(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_action(state, eval_fprintx);
- if (expr) {
- init_print_expr(state, expr);
- }
- return expr;
-}
-
-/**
- * Parse -prune.
- */
-static struct expr *parse_prune(struct parser_state *state, int arg1, int arg2) {
- state->prune_arg = state->argv[0];
-
- struct expr *expr = parse_nullary_action(state, eval_prune);
- if (expr) {
- expr_set_always_true(expr);
- }
- return expr;
-}
-
-/**
- * Parse -quit.
- */
-static struct expr *parse_quit(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_action(state, eval_quit);
- if (expr) {
- expr_set_never_returns(expr);
- }
- return expr;
-}
-
-/**
- * Parse -i?regex.
- */
-static struct expr *parse_regex(struct parser_state *state, int flags, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_regex);
- if (!expr) {
- goto fail;
- }
-
- expr->regex = malloc(sizeof(regex_t));
- if (!expr->regex) {
- parse_perror(state, "malloc()");
- goto fail;
- }
-
- int err = regcomp(expr->regex, expr->sdata, state->regex_flags | flags);
- if (err != 0) {
- char *str = xregerror(err, NULL);
- if (str) {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: %s.\n", expr->argv[0], expr->argv[1], str);
- free(str);
- } else {
- parse_perror(state, "xregerror()");
- }
- goto fail_regex;
- }
-
- return expr;
-
-fail_regex:
- free(expr->regex);
- expr->regex = NULL;
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -E.
- */
-static struct expr *parse_regex_extended(struct parser_state *state, int arg1, int arg2) {
- state->regex_flags = REG_EXTENDED;
- return parse_nullary_flag(state);
-}
-
-/**
- * Parse -regextype TYPE.
- */
-static struct expr *parse_regextype(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
- CFILE *cfile = ctx->cerr;
-
- const char *arg = state->argv[0];
- const char *type = state->argv[1];
- if (!type) {
- parse_error(state, "${blu}%s${rs} needs a value.\n\n", arg);
- goto list_types;
- }
-
- if (strcmp(type, "posix-basic") == 0) {
- state->regex_flags = 0;
- } else if (strcmp(type, "posix-extended") == 0) {
- state->regex_flags = REG_EXTENDED;
- } else if (strcmp(type, "help") == 0) {
- state->just_info = true;
- cfile = ctx->cout;
- goto list_types;
- } else {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Unsupported regex type.\n\n", arg, type);
- goto list_types;
- }
-
- return parse_unary_positional_option(state);
-
-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");
- return NULL;
-}
-
-/**
- * Parse -s.
- */
-static struct expr *parse_s(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_SORT;
- return parse_nullary_flag(state);
-}
-
-/**
- * Parse -samefile FILE.
- */
-static struct expr *parse_samefile(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_samefile);
- if (!expr) {
- return NULL;
- }
-
- struct bfs_stat sb;
- if (stat_arg(state, expr, &sb) != 0) {
- free_expr(expr);
- return NULL;
- }
-
- expr->dev = sb.dev;
- expr->ino = sb.ino;
-
- expr->cost = STAT_COST;
- expr->probability = 0.01;
-
- return expr;
-}
-
-/**
- * Parse -S STRATEGY.
- */
-static struct expr *parse_search_strategy(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
- CFILE *cfile = ctx->cerr;
-
- const char *flag = state->argv[0];
- const char *arg = state->argv[1];
- if (!arg) {
- parse_error(state, "${cyn}%s${rs} needs an argument.\n\n", flag);
- goto list_strategies;
- }
-
-
- if (strcmp(arg, "bfs") == 0) {
- ctx->strategy = BFTW_BFS;
- } else if (strcmp(arg, "dfs") == 0) {
- ctx->strategy = BFTW_DFS;
- } else if (strcmp(arg, "ids") == 0) {
- ctx->strategy = BFTW_IDS;
- } else if (strcmp(arg, "eds") == 0) {
- ctx->strategy = BFTW_EDS;
- } else if (strcmp(arg, "help") == 0) {
- state->just_info = true;
- cfile = ctx->cout;
- goto list_strategies;
- } else {
- parse_error(state, "${cyn}%s${rs} ${bld}%s${rs}: Unrecognized search strategy.\n\n", flag, arg);
- goto list_strategies;
- }
-
- return parse_unary_flag(state);
-
-list_strategies:
- cfprintf(cfile, "Supported search strategies:\n\n");
- cfprintf(cfile, " ${bld}bfs${rs}: breadth-first search\n");
- cfprintf(cfile, " ${bld}dfs${rs}: depth-first search\n");
- cfprintf(cfile, " ${bld}ids${rs}: iterative deepening search\n");
- cfprintf(cfile, " ${bld}eds${rs}: exponential deepening search\n");
- return NULL;
-}
-
-/**
- * Parse -[aBcm]?since.
- */
-static struct expr *parse_since(struct parser_state *state, int field, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_newer);
- if (!expr) {
- return NULL;
- }
-
- if (parse_reftime(state, expr) != 0) {
- goto fail;
- }
-
- expr->cost = STAT_COST;
- expr->stat_field = field;
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -size N[cwbkMGTP]?.
- */
-static struct expr *parse_size(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_unary_test(state, eval_size);
- if (!expr) {
- return NULL;
- }
-
- const char *unit = parse_icmp(state, expr->sdata, expr, IF_PARTIAL_OK);
- if (!unit) {
- goto fail;
- }
-
- if (strlen(unit) > 1) {
- goto bad_unit;
- }
-
- switch (*unit) {
- case '\0':
- case 'b':
- expr->size_unit = SIZE_BLOCKS;
- break;
- case 'c':
- expr->size_unit = SIZE_BYTES;
- break;
- case 'w':
- expr->size_unit = SIZE_WORDS;
- break;
- case 'k':
- expr->size_unit = SIZE_KB;
- break;
- case 'M':
- expr->size_unit = SIZE_MB;
- break;
- case 'G':
- expr->size_unit = SIZE_GB;
- break;
- case 'T':
- expr->size_unit = SIZE_TB;
- break;
- case 'P':
- expr->size_unit = SIZE_PB;
- break;
-
- default:
- goto bad_unit;
- }
-
- expr->cost = STAT_COST;
- expr->probability = expr->cmp_flag == CMP_EXACT ? 0.01 : 0.50;
-
- return expr;
-
-bad_unit:
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${er}%s${rs}.\n",
- expr->argv[0], expr->argv[1], unit);
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -sparse.
- */
-static struct expr *parse_sparse(struct parser_state *state, int arg1, int arg2) {
- struct expr *expr = parse_nullary_test(state, eval_sparse);
- if (expr) {
- expr->cost = STAT_COST;
- }
- return expr;
-}
-
-/**
- * Parse -status.
- */
-static struct expr *parse_status(struct parser_state *state, int arg1, int arg2) {
- state->ctx->status = true;
- return parse_nullary_option(state);
-}
-
-/**
- * Parse -x?type [bcdpflsD].
- */
-static struct expr *parse_type(struct parser_state *state, int x, int arg2) {
- eval_fn *eval = x ? eval_xtype : eval_type;
- struct expr *expr = parse_unary_test(state, eval);
- if (!expr) {
- return NULL;
- }
-
- unsigned int types = 0;
- double probability = 0.0;
-
- const char *c = expr->sdata;
- while (true) {
- enum bfs_type type;
- double type_prob;
-
- switch (*c) {
- case 'b':
- type = BFS_BLK;
- type_prob = 0.00000721183;
- break;
- case 'c':
- type = BFS_CHR;
- type_prob = 0.0000499855;
- break;
- case 'd':
- type = BFS_DIR;
- type_prob = 0.114475;
- break;
- case 'D':
- type = BFS_DOOR;
- type_prob = 0.000001;
- break;
- case 'p':
- type = BFS_FIFO;
- type_prob = 0.00000248684;
- break;
- case 'f':
- type = BFS_REG;
- type_prob = 0.859772;
- break;
- case 'l':
- type = BFS_LNK;
- type_prob = 0.0256816;
- break;
- case 's':
- type = BFS_SOCK;
- type_prob = 0.0000116881;
- break;
- case 'w':
- type = BFS_WHT;
- type_prob = 0.000001;
- break;
-
- case '\0':
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Expected a type flag.\n", expr->argv[0], expr->argv[1]);
- goto fail;
-
- default:
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Unknown type flag ${er}%c${rs} (expected one of [${bld}bcdpflsD${rs}]).\n",
- expr->argv[0], expr->argv[1], *c);
- goto fail;
- }
-
- unsigned int flag = 1 << type;
- if (!(types & flag)) {
- types |= flag;
- probability += type_prob;
- }
-
- ++c;
- if (*c == '\0') {
- break;
- } else if (*c == ',') {
- ++c;
- continue;
- } else {
- parse_error(state, "${blu}%s${rs} ${bld}%s${rs}: Types must be comma-separated.\n", expr->argv[0], expr->argv[1]);
- goto fail;
- }
- }
-
- expr->idata = types;
- expr->probability = probability;
-
- if (x && state->ctx->optlevel < 4) {
- // Since -xtype dereferences symbolic links, it may have side
- // effects such as reporting permission errors, and thus
- // shouldn't be re-ordered without aggressive optimizations
- expr->pure = false;
- }
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-/**
- * Parse -(no)?warn.
- */
-static struct expr *parse_warn(struct parser_state *state, int warn, int arg2) {
- state->ctx->warn = warn;
- return parse_nullary_positional_option(state);
-}
-
-/**
- * Parse -xattr.
- */
-static struct expr *parse_xattr(struct parser_state *state, int arg1, int arg2) {
-#if BFS_CAN_CHECK_XATTRS
- struct expr *expr = parse_nullary_test(state, eval_xattr);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
-#else
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", state->argv[0]);
- return NULL;
-#endif
-}
-
-/**
- * Parse -xattrname.
- */
-static struct expr *parse_xattrname(struct parser_state *state, int arg1, int arg2) {
-#if BFS_CAN_CHECK_XATTRS
- struct expr *expr = parse_unary_test(state, eval_xattrname);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
-#else
- parse_error(state, "${blu}%s${rs} is missing platform support.\n", state->argv[0]);
- return NULL;
-#endif
-}
-
-/**
- * Parse -xdev.
- */
-static struct expr *parse_xdev(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_PRUNE_MOUNTS;
- state->xdev_arg = state->argv[0];
- return parse_nullary_option(state);
-}
-
-/**
- * Launch a pager for the help output.
- */
-static CFILE *launch_pager(pid_t *pid, CFILE *cout) {
- char *pager = getenv("PAGER");
-
- char *exe;
- if (pager && pager[0]) {
- exe = bfs_spawn_resolve(pager);
- } else {
- exe = bfs_spawn_resolve("less");
- if (!exe) {
- exe = bfs_spawn_resolve("more");
- }
- }
- if (!exe) {
- goto fail;
- }
-
- int pipefd[2];
- if (pipe(pipefd) != 0) {
- goto fail_exe;
- }
-
- FILE *file = fdopen(pipefd[1], "w");
- if (!file) {
- goto fail_pipe;
- }
- pipefd[1] = -1;
-
- CFILE *ret = cfdup(file, NULL);
- if (!ret) {
- goto fail_file;
- }
- file = NULL;
- ret->close = true;
-
- struct bfs_spawn ctx;
- if (bfs_spawn_init(&ctx) != 0) {
- goto fail_ret;
- }
-
- if (bfs_spawn_addclose(&ctx, fileno(ret->file)) != 0) {
- goto fail_ctx;
- }
- if (bfs_spawn_adddup2(&ctx, pipefd[0], STDIN_FILENO) != 0) {
- goto fail_ctx;
- }
- if (bfs_spawn_addclose(&ctx, pipefd[0]) != 0) {
- goto fail_ctx;
- }
-
- char *argv[] = {
- exe,
- NULL,
- NULL,
- };
-
- if (strcmp(xbasename(exe), "less") == 0) {
- // We know less supports colors, other pagers may not
- ret->colors = cout->colors;
- argv[1] = "-FKRX";
- }
-
- *pid = bfs_spawn(exe, &ctx, argv, NULL);
- if (*pid < 0) {
- goto fail_ctx;
- }
-
- close(pipefd[0]);
- bfs_spawn_destroy(&ctx);
- free(exe);
- return ret;
-
-fail_ctx:
- bfs_spawn_destroy(&ctx);
-fail_ret:
- cfclose(ret);
-fail_file:
- if (file) {
- fclose(file);
- }
-fail_pipe:
- if (pipefd[1] >= 0) {
- close(pipefd[1]);
- }
- if (pipefd[0] >= 0) {
- close(pipefd[0]);
- }
-fail_exe:
- free(exe);
-fail:
- return cout;
-}
-
-/**
- * "Parse" -help.
- */
-static struct expr *parse_help(struct parser_state *state, int arg1, int arg2) {
- CFILE *cout = state->ctx->cout;
-
- pid_t pager = -1;
- if (state->stdout_tty) {
- cout = launch_pager(&pager, cout);
- }
-
- cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n",
- state->command);
-
- cfprintf(cout, "${ex}bfs${rs} is compatible with ${ex}find${rs}, with some extensions. "
- "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n"
- "and ${blu}expressions${rs} may be freely mixed in any order.\n\n");
-
- cfprintf(cout, "${bld}Flags:${rs}\n\n");
-
- cfprintf(cout, " ${cyn}-H${rs}\n");
- cfprintf(cout, " Follow symbolic links on the command line, but not while searching\n");
- cfprintf(cout, " ${cyn}-L${rs}\n");
- cfprintf(cout, " Follow all symbolic links\n");
- cfprintf(cout, " ${cyn}-P${rs}\n");
- cfprintf(cout, " Never follow symbolic links (the default)\n");
-
- cfprintf(cout, " ${cyn}-E${rs}\n");
- cfprintf(cout, " Use extended regular expressions (same as ${blu}-regextype${rs} ${bld}posix-extended${rs})\n");
- cfprintf(cout, " ${cyn}-X${rs}\n");
- cfprintf(cout, " Filter out files with non-${ex}xargs${rs}-safe names\n");
- cfprintf(cout, " ${cyn}-d${rs}\n");
- cfprintf(cout, " Search in post-order (same as ${blu}-depth${rs})\n");
- cfprintf(cout, " ${cyn}-s${rs}\n");
- cfprintf(cout, " Visit directory entries in sorted order\n");
- cfprintf(cout, " ${cyn}-x${rs}\n");
- cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs})\n");
-
- cfprintf(cout, " ${cyn}-f${rs} ${mag}PATH${rs}\n");
- cfprintf(cout, " Treat ${mag}PATH${rs} as a path to search (useful if begins with a dash)\n");
- cfprintf(cout, " ${cyn}-D${rs} ${bld}FLAG${rs}\n");
- cfprintf(cout, " Turn on a debugging flag (see ${cyn}-D${rs} ${bld}help${rs})\n");
- cfprintf(cout, " ${cyn}-O${bld}N${rs}\n");
- cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n");
- cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n");
- cfprintf(cout, " Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n");
- cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n\n");
-
- cfprintf(cout, "${bld}Operators:${rs}\n\n");
-
- cfprintf(cout, " ${red}(${rs} ${blu}expression${rs} ${red})${rs}\n\n");
-
- cfprintf(cout, " ${red}!${rs} ${blu}expression${rs}\n");
- cfprintf(cout, " ${red}-not${rs} ${blu}expression${rs}\n\n");
-
- cfprintf(cout, " ${blu}expression${rs} ${blu}expression${rs}\n");
- cfprintf(cout, " ${blu}expression${rs} ${red}-a${rs} ${blu}expression${rs}\n");
- cfprintf(cout, " ${blu}expression${rs} ${red}-and${rs} ${blu}expression${rs}\n\n");
-
- cfprintf(cout, " ${blu}expression${rs} ${red}-o${rs} ${blu}expression${rs}\n");
- cfprintf(cout, " ${blu}expression${rs} ${red}-or${rs} ${blu}expression${rs}\n\n");
-
- cfprintf(cout, " ${blu}expression${rs} ${red},${rs} ${blu}expression${rs}\n\n");
-
- cfprintf(cout, "${bld}Special forms:${rs}\n\n");
-
- cfprintf(cout, " ${red}-exclude${rs} ${blu}expression${rs}\n");
- cfprintf(cout, " Exclude all paths matching the ${blu}expression${rs} from the search.\n\n");
-
- cfprintf(cout, "${bld}Options:${rs}\n\n");
-
- cfprintf(cout, " ${blu}-color${rs}\n");
- cfprintf(cout, " ${blu}-nocolor${rs}\n");
- cfprintf(cout, " Turn colors on or off (default: ${blu}-color${rs} if outputting to a terminal,\n");
- cfprintf(cout, " ${blu}-nocolor${rs} otherwise)\n");
- cfprintf(cout, " ${blu}-daystart${rs}\n");
- cfprintf(cout, " Measure times relative to the start of today\n");
- cfprintf(cout, " ${blu}-depth${rs}\n");
- cfprintf(cout, " Search in post-order (descendents first)\n");
- cfprintf(cout, " ${blu}-files0-from${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " Search the NUL ('\\0')-separated paths from ${bld}FILE${rs} (${bld}-${rs} for standard input).\n");
- cfprintf(cout, " ${blu}-follow${rs}\n");
- cfprintf(cout, " Follow all symbolic links (same as ${cyn}-L${rs})\n");
- cfprintf(cout, " ${blu}-ignore_readdir_race${rs}\n");
- cfprintf(cout, " ${blu}-noignore_readdir_race${rs}\n");
- cfprintf(cout, " Whether to report an error if ${ex}bfs${rs} detects that the file tree is modified\n");
- cfprintf(cout, " during the search (default: ${blu}-noignore_readdir_race${rs})\n");
- cfprintf(cout, " ${blu}-maxdepth${rs} ${bld}N${rs}\n");
- cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n");
- cfprintf(cout, " Ignore files deeper/shallower than ${bld}N${rs}\n");
- cfprintf(cout, " ${blu}-mount${rs}\n");
- cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs} for now, but will\n");
- cfprintf(cout, " skip mount points entirely in the future)\n");
- cfprintf(cout, " ${blu}-nohidden${rs}\n");
- cfprintf(cout, " Exclude hidden files\n");
- cfprintf(cout, " ${blu}-noleaf${rs}\n");
- cfprintf(cout, " Ignored; for compatibility with GNU find\n");
- cfprintf(cout, " ${blu}-regextype${rs} ${bld}TYPE${rs}\n");
- cfprintf(cout, " Use ${bld}TYPE${rs}-flavored regexes (default: ${bld}posix-basic${rs}; see ${blu}-regextype${rs} ${bld}help${rs})\n");
- cfprintf(cout, " ${blu}-status${rs}\n");
- cfprintf(cout, " Display a status bar while searching\n");
- cfprintf(cout, " ${blu}-unique${rs}\n");
- cfprintf(cout, " Skip any files that have already been seen\n");
- cfprintf(cout, " ${blu}-warn${rs}\n");
- cfprintf(cout, " ${blu}-nowarn${rs}\n");
- cfprintf(cout, " Turn on or off warnings about the command line\n");
- cfprintf(cout, " ${blu}-xdev${rs}\n");
- cfprintf(cout, " Don't descend into other mount points\n\n");
-
- cfprintf(cout, "${bld}Tests:${rs}\n\n");
-
-#if BFS_CAN_CHECK_ACL
- cfprintf(cout, " ${blu}-acl${rs}\n");
- cfprintf(cout, " Find files with a non-trivial Access Control List\n");
-#endif
- cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}min${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} minutes ago\n");
- cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}newer${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}FILE${rs} was\n"
- " modified\n");
- cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}since${rs} ${bld}TIME${rs}\n");
- cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}TIME${rs}\n");
- cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}time${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} days ago\n");
-#if BFS_CAN_CHECK_CAPABILITIES
- cfprintf(cout, " ${blu}-capable${rs}\n");
- cfprintf(cout, " Find files with POSIX.1e capabilities set\n");
-#endif
- cfprintf(cout, " ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files with depth ${bld}N${rs}\n");
- cfprintf(cout, " ${blu}-empty${rs}\n");
- cfprintf(cout, " Find empty files/directories\n");
- cfprintf(cout, " ${blu}-executable${rs}\n");
- cfprintf(cout, " ${blu}-readable${rs}\n");
- cfprintf(cout, " ${blu}-writable${rs}\n");
- cfprintf(cout, " Find files the current user can execute/read/write\n");
- cfprintf(cout, " ${blu}-false${rs}\n");
- cfprintf(cout, " ${blu}-true${rs}\n");
- cfprintf(cout, " Always false/true\n");
- cfprintf(cout, " ${blu}-fstype${rs} ${bld}TYPE${rs}\n");
- cfprintf(cout, " Find files on file systems with the given ${bld}TYPE${rs}\n");
- cfprintf(cout, " ${blu}-gid${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " ${blu}-uid${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files owned by group/user ID ${bld}N${rs}\n");
- cfprintf(cout, " ${blu}-group${rs} ${bld}NAME${rs}\n");
- cfprintf(cout, " ${blu}-user${rs} ${bld}NAME${rs}\n");
- cfprintf(cout, " Find files owned by the group/user ${bld}NAME${rs}\n");
- cfprintf(cout, " ${blu}-hidden${rs}\n");
- cfprintf(cout, " Find hidden files\n");
-#ifdef FNM_CASEFOLD
- cfprintf(cout, " ${blu}-ilname${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-iname${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-ipath${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-iregex${rs} ${bld}REGEX${rs}\n");
- cfprintf(cout, " ${blu}-iwholename${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " Case-insensitive versions of ${blu}-lname${rs}/${blu}-name${rs}/${blu}-path${rs}"
- "/${blu}-regex${rs}/${blu}-wholename${rs}\n");
-#endif
- cfprintf(cout, " ${blu}-inum${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files with inode number ${bld}N${rs}\n");
- cfprintf(cout, " ${blu}-links${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files with ${bld}N${rs} hard links\n");
- cfprintf(cout, " ${blu}-lname${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " Find symbolic links whose target matches the ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-name${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " Find files whose name matches the ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-newer${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " Find files newer than ${bld}FILE${rs}\n");
- cfprintf(cout, " ${blu}-newer${bld}XY${rs} ${bld}REFERENCE${rs}\n");
- cfprintf(cout, " Find files whose ${bld}X${rs} time is newer than the ${bld}Y${rs} time of"
- " ${bld}REFERENCE${rs}. ${bld}X${rs} and ${bld}Y${rs}\n");
- cfprintf(cout, " can be any of [${bld}aBcm${rs}]. ${bld}Y${rs} may also be ${bld}t${rs} to parse ${bld}REFERENCE${rs} an explicit\n");
- cfprintf(cout, " timestamp.\n");
- cfprintf(cout, " ${blu}-nogroup${rs}\n");
- cfprintf(cout, " ${blu}-nouser${rs}\n");
- cfprintf(cout, " Find files owned by nonexistent groups/users\n");
- cfprintf(cout, " ${blu}-path${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-wholename${rs} ${bld}GLOB${rs}\n");
- cfprintf(cout, " Find files whose entire path matches the ${bld}GLOB${rs}\n");
- cfprintf(cout, " ${blu}-perm${rs} ${bld}[-]MODE${rs}\n");
- cfprintf(cout, " Find files with a matching mode\n");
- cfprintf(cout, " ${blu}-regex${rs} ${bld}REGEX${rs}\n");
- cfprintf(cout, " Find files whose entire path matches the regular expression ${bld}REGEX${rs}\n");
- cfprintf(cout, " ${blu}-samefile${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " Find hard links to ${bld}FILE${rs}\n");
- cfprintf(cout, " ${blu}-since${rs} ${bld}TIME${rs}\n");
- cfprintf(cout, " Find files modified since ${bld}TIME${rs}\n");
- cfprintf(cout, " ${blu}-size${rs} ${bld}[-+]N[cwbkMGTP]${rs}\n");
- cfprintf(cout, " Find files with the given size, in 1-byte ${bld}c${rs}haracters, 2-byte ${bld}w${rs}ords,\n");
- cfprintf(cout, " 512-byte ${bld}b${rs}locks (default), or ${bld}k${rs}iB/${bld}M${rs}iB/${bld}G${rs}iB/${bld}T${rs}iB/${bld}P${rs}iB\n");
- cfprintf(cout, " ${blu}-sparse${rs}\n");
- cfprintf(cout, " Find files that occupy fewer disk blocks than expected\n");
- cfprintf(cout, " ${blu}-type${rs} ${bld}[bcdlpfswD]${rs}\n");
- cfprintf(cout, " Find files of the given type\n");
- cfprintf(cout, " ${blu}-used${rs} ${bld}[-+]N${rs}\n");
- cfprintf(cout, " Find files last accessed ${bld}N${rs} days after they were changed\n");
-#if BFS_CAN_CHECK_XATTRS
- cfprintf(cout, " ${blu}-xattr${rs}\n");
- cfprintf(cout, " Find files with extended attributes\n");
- cfprintf(cout, " ${blu}-xattrname${rs} ${bld}NAME${rs}\n");
- cfprintf(cout, " Find files with the extended attribute ${bld}NAME${rs}\n");
-#endif
- cfprintf(cout, " ${blu}-xtype${rs} ${bld}[bcdlpfswD]${rs}\n");
- cfprintf(cout, " Find files of the given type, following links when ${blu}-type${rs} would not, and\n");
- cfprintf(cout, " vice versa\n\n");
-
- cfprintf(cout, "${bld}Actions:${rs}\n\n");
-
- cfprintf(cout, " ${blu}-delete${rs}\n");
- cfprintf(cout, " ${blu}-rm${rs}\n");
- cfprintf(cout, " Delete any found files (implies ${blu}-depth${rs})\n");
- cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} ;${rs}\n");
- cfprintf(cout, " Execute a command\n");
- cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} +${rs}\n");
- cfprintf(cout, " Execute a command with multiple files at once\n");
- cfprintf(cout, " ${blu}-ok${rs} ${bld}command ... {} ;${rs}\n");
- cfprintf(cout, " Prompt the user whether to execute a command\n");
- cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} ;${rs}\n");
- cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} +${rs}\n");
- cfprintf(cout, " ${blu}-okdir${rs} ${bld}command ... {} ;${rs}\n");
- cfprintf(cout, " Like ${blu}-exec${rs}/${blu}-ok${rs}, but run the command in the same directory as the found\n");
- cfprintf(cout, " file(s)\n");
- cfprintf(cout, " ${blu}-exit${rs} [${bld}STATUS${rs}]\n");
- cfprintf(cout, " Exit immediately with the given status (%d if unspecified)\n", EXIT_SUCCESS);
- cfprintf(cout, " ${blu}-fls${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " ${blu}-fprint${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " ${blu}-fprint0${rs} ${bld}FILE${rs}\n");
- cfprintf(cout, " ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n");
- cfprintf(cout, " Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n"
- " output\n");
- cfprintf(cout, " ${blu}-ls${rs}\n");
- cfprintf(cout, " List files like ${ex}ls${rs} ${bld}-dils${rs}\n");
- cfprintf(cout, " ${blu}-print${rs}\n");
- cfprintf(cout, " Print the path to the found file\n");
- cfprintf(cout, " ${blu}-print0${rs}\n");
- cfprintf(cout, " Like ${blu}-print${rs}, but use the null character ('\\0') as a separator rather than\n");
- cfprintf(cout, " newlines\n");
- cfprintf(cout, " ${blu}-printf${rs} ${bld}FORMAT${rs}\n");
- cfprintf(cout, " Print according to a format string (see ${ex}man${rs} ${bld}find${rs}). The additional format\n");
- cfprintf(cout, " directives %%w and %%W${bld}k${rs} for printing file birth times are supported.\n");
- cfprintf(cout, " ${blu}-printx${rs}\n");
- cfprintf(cout, " Like ${blu}-print${rs}, but escape whitespace and quotation characters, to make the\n");
- cfprintf(cout, " output safe for ${ex}xargs${rs}. Consider using ${blu}-print0${rs} and ${ex}xargs${rs} ${bld}-0${rs} instead.\n");
- cfprintf(cout, " ${blu}-prune${rs}\n");
- cfprintf(cout, " Don't descend into this directory\n");
- cfprintf(cout, " ${blu}-quit${rs}\n");
- cfprintf(cout, " Quit immediately\n");
- cfprintf(cout, " ${blu}-version${rs}\n");
- cfprintf(cout, " Print version information\n");
- cfprintf(cout, " ${blu}-help${rs}\n");
- cfprintf(cout, " Print this help message\n\n");
-
- cfprintf(cout, "%s\n", BFS_HOMEPAGE);
-
- if (pager > 0) {
- cfclose(cout);
- waitpid(pager, NULL, 0);
- }
-
- state->just_info = true;
- return NULL;
-}
-
-/**
- * "Parse" -version.
- */
-static struct expr *parse_version(struct parser_state *state, int arg1, int arg2) {
- cfprintf(state->ctx->cout, "${ex}bfs${rs} ${bld}%s${rs}\n\n", BFS_VERSION);
-
- printf("%s\n", BFS_HOMEPAGE);
-
- state->just_info = true;
- return NULL;
-}
-
-typedef struct expr *parse_fn(struct parser_state *state, int arg1, int arg2);
-
-/**
- * An entry in the parse table for literals.
- */
-struct table_entry {
- char *arg;
- enum token_type type;
- parse_fn *parse;
- int arg1;
- int arg2;
- bool prefix;
-};
-
-/**
- * The parse table for literals.
- */
-static const struct table_entry parse_table[] = {
- {"--", T_FLAG},
- {"--help", T_ACTION, parse_help},
- {"--version", T_ACTION, parse_version},
- {"-Bmin", T_TEST, parse_min, BFS_STAT_BTIME},
- {"-Bnewer", T_TEST, parse_newer, BFS_STAT_BTIME},
- {"-Bsince", T_TEST, parse_since, BFS_STAT_BTIME},
- {"-Btime", T_TEST, parse_time, BFS_STAT_BTIME},
- {"-D", T_FLAG, parse_debug},
- {"-E", T_FLAG, parse_regex_extended},
- {"-H", T_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false},
- {"-L", T_FLAG, parse_follow, BFTW_FOLLOW_ALL, false},
- {"-O", T_FLAG, parse_optlevel, 0, 0, true},
- {"-P", T_FLAG, parse_follow, 0, false},
- {"-S", T_FLAG, parse_search_strategy},
- {"-X", T_FLAG, parse_xargs_safe},
- {"-a", T_OPERATOR},
- {"-acl", T_TEST, parse_acl},
- {"-amin", T_TEST, parse_min, BFS_STAT_ATIME},
- {"-and", T_OPERATOR},
- {"-anewer", T_TEST, parse_newer, BFS_STAT_ATIME},
- {"-asince", T_TEST, parse_since, BFS_STAT_ATIME},
- {"-atime", T_TEST, parse_time, BFS_STAT_ATIME},
- {"-capable", T_TEST, parse_capable},
- {"-cmin", T_TEST, parse_min, BFS_STAT_CTIME},
- {"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME},
- {"-color", T_OPTION, parse_color, true},
- {"-csince", T_TEST, parse_since, BFS_STAT_CTIME},
- {"-ctime", T_TEST, parse_time, BFS_STAT_CTIME},
- {"-d", T_FLAG, parse_depth},
- {"-daystart", T_OPTION, parse_daystart},
- {"-delete", T_ACTION, parse_delete},
- {"-depth", T_OPTION, parse_depth_n},
- {"-empty", T_TEST, parse_empty},
- {"-exclude", T_OPERATOR},
- {"-exec", T_ACTION, parse_exec, 0},
- {"-execdir", T_ACTION, parse_exec, BFS_EXEC_CHDIR},
- {"-executable", T_TEST, parse_access, X_OK},
- {"-exit", T_ACTION, parse_exit},
- {"-f", T_FLAG, parse_f},
- {"-false", T_TEST, parse_const, false},
- {"-files0-from", T_OPTION, parse_files0_from},
- {"-flags", T_TEST, parse_flags},
- {"-fls", T_ACTION, parse_fls},
- {"-follow", T_OPTION, parse_follow, BFTW_FOLLOW_ALL, true},
- {"-fprint", T_ACTION, parse_fprint},
- {"-fprint0", T_ACTION, parse_fprint0},
- {"-fprintf", T_ACTION, parse_fprintf},
- {"-fstype", T_TEST, parse_fstype},
- {"-gid", T_TEST, parse_group},
- {"-group", T_TEST, parse_group},
- {"-help", T_ACTION, parse_help},
- {"-hidden", T_TEST, parse_hidden},
- {"-ignore_readdir_race", T_OPTION, parse_ignore_races, true},
- {"-ilname", T_TEST, parse_lname, true},
- {"-iname", T_TEST, parse_name, true},
- {"-inum", T_TEST, parse_inum},
- {"-ipath", T_TEST, parse_path, true},
- {"-iregex", T_TEST, parse_regex, REG_ICASE},
- {"-iwholename", T_TEST, parse_path, true},
- {"-links", T_TEST, parse_links},
- {"-lname", T_TEST, parse_lname, false},
- {"-ls", T_ACTION, parse_ls},
- {"-maxdepth", T_OPTION, parse_depth_limit, false},
- {"-mindepth", T_OPTION, parse_depth_limit, true},
- {"-mmin", T_TEST, parse_min, BFS_STAT_MTIME},
- {"-mnewer", T_TEST, parse_newer, BFS_STAT_MTIME},
- {"-mount", T_OPTION, parse_mount},
- {"-msince", T_TEST, parse_since, BFS_STAT_MTIME},
- {"-mtime", T_TEST, parse_time, BFS_STAT_MTIME},
- {"-name", T_TEST, parse_name, false},
- {"-newer", T_TEST, parse_newer, BFS_STAT_MTIME},
- {"-newer", T_TEST, parse_newerxy, 0, 0, true},
- {"-nocolor", T_OPTION, parse_color, false},
- {"-nogroup", T_TEST, parse_nogroup},
- {"-nohidden", T_TEST, parse_nohidden},
- {"-noignore_readdir_race", T_OPTION, parse_ignore_races, false},
- {"-noleaf", T_OPTION, parse_noleaf},
- {"-not", T_OPERATOR},
- {"-nouser", T_TEST, parse_nouser},
- {"-nowarn", T_OPTION, parse_warn, false},
- {"-o", T_OPERATOR},
- {"-ok", T_ACTION, parse_exec, BFS_EXEC_CONFIRM},
- {"-okdir", T_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR},
- {"-or", T_OPERATOR},
- {"-path", T_TEST, parse_path, false},
- {"-perm", T_TEST, parse_perm},
- {"-print", T_ACTION, parse_print},
- {"-print0", T_ACTION, parse_print0},
- {"-printf", T_ACTION, parse_printf},
- {"-printx", T_ACTION, parse_printx},
- {"-prune", T_ACTION, parse_prune},
- {"-quit", T_ACTION, parse_quit},
- {"-readable", T_TEST, parse_access, R_OK},
- {"-regex", T_TEST, parse_regex, 0},
- {"-regextype", T_OPTION, parse_regextype},
- {"-rm", T_ACTION, parse_delete},
- {"-s", T_FLAG, parse_s},
- {"-samefile", T_TEST, parse_samefile},
- {"-since", T_TEST, parse_since, BFS_STAT_MTIME},
- {"-size", T_TEST, parse_size},
- {"-sparse", T_TEST, parse_sparse},
- {"-status", T_OPTION, parse_status},
- {"-true", T_TEST, parse_const, true},
- {"-type", T_TEST, parse_type, false},
- {"-uid", T_TEST, parse_user},
- {"-unique", T_OPTION, parse_unique},
- {"-used", T_TEST, parse_used},
- {"-user", T_TEST, parse_user},
- {"-version", T_ACTION, parse_version},
- {"-warn", T_OPTION, parse_warn, true},
- {"-wholename", T_TEST, parse_path, false},
- {"-writable", T_TEST, parse_access, W_OK},
- {"-x", T_FLAG, parse_xdev},
- {"-xattr", T_TEST, parse_xattr},
- {"-xattrname", T_TEST, parse_xattrname},
- {"-xdev", T_OPTION, parse_xdev},
- {"-xtype", T_TEST, parse_type, true},
- {0},
-};
-
-/** Look up an argument in the 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) {
- match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0;
- } else {
- match = strcmp(arg, entry->arg) == 0;
- }
- if (match) {
- return entry;
- }
- }
-
- return NULL;
-}
-
-/** Search for a fuzzy match in the parse table. */
-static const struct table_entry *table_lookup_fuzzy(const char *arg) {
- const struct table_entry *best = NULL;
- int best_dist;
-
- for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
- int dist = typo_distance(arg, entry->arg);
- if (!best || dist < best_dist) {
- best = entry;
- best_dist = dist;
- }
- }
-
- return best;
-}
-
-/**
- * LITERAL : OPTION
- * | TEST
- * | ACTION
- */
-static struct expr *parse_literal(struct parser_state *state) {
- // Paths are already skipped at this point
- const char *arg = state->argv[0];
-
- if (arg[0] != '-') {
- goto unexpected;
- }
-
- const struct table_entry *match = table_lookup(arg);
- if (match) {
- if (match->parse) {
- goto matched;
- } else {
- goto unexpected;
- }
- }
-
- match = table_lookup_fuzzy(arg);
-
- CFILE *cerr = state->ctx->cerr;
- parse_error(state, "Unknown argument ${er}%s${rs}; did you mean ", arg);
- switch (match->type) {
- case T_FLAG:
- cfprintf(cerr, "${cyn}%s${rs}?", match->arg);
- break;
- case T_OPERATOR:
- cfprintf(cerr, "${red}%s${rs}?", match->arg);
- break;
- default:
- cfprintf(cerr, "${blu}%s${rs}?", match->arg);
- break;
- }
-
- if (!state->interactive || !match->parse) {
- fprintf(stderr, "\n");
- goto unmatched;
- }
-
- fprintf(stderr, " ");
- if (ynprompt() <= 0) {
- goto unmatched;
- }
-
- fprintf(stderr, "\n");
- state->argv[0] = match->arg;
-
-matched:
- return match->parse(state, match->arg1, match->arg2);
-
-unmatched:
- return NULL;
-
-unexpected:
- parse_error(state, "Expected a predicate; found ${er}%s${rs}.\n", arg);
- return NULL;
-}
-
-/**
- * FACTOR : "(" EXPR ")"
- * | "!" FACTOR | "-not" FACTOR
- * | "-exclude" FACTOR
- * | LITERAL
- */
-static struct expr *parse_factor(struct parser_state *state) {
- if (skip_paths(state) != 0) {
- return NULL;
- }
-
- const char *arg = state->argv[0];
- if (!arg) {
- parse_error(state, "Expression terminated prematurely after ${red}%s${rs}.\n", state->last_arg);
- return NULL;
- }
-
- if (strcmp(arg, "(") == 0) {
- parser_advance(state, T_OPERATOR, 1);
-
- struct expr *expr = parse_expr(state);
- if (!expr) {
- return NULL;
- }
-
- if (skip_paths(state) != 0) {
- free_expr(expr);
- return NULL;
- }
-
- arg = state->argv[0];
- if (!arg || strcmp(arg, ")") != 0) {
- parse_error(state, "Expected a ${red})${rs} after ${blu}%s${rs}.\n", state->argv[-1]);
- free_expr(expr);
- return NULL;
- }
- parser_advance(state, T_OPERATOR, 1);
-
- return expr;
- } else if (strcmp(arg, "-exclude") == 0) {
- parser_advance(state, T_OPERATOR, 1);
-
- if (state->excluding) {
- parse_error(state, "${er}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg);
- return NULL;
- }
- state->excluding = true;
-
- struct expr *factor = parse_factor(state);
- if (!factor) {
- return NULL;
- }
-
- state->excluding = false;
-
- struct bfs_ctx *ctx = state->ctx;
- ctx->exclude = new_binary_expr(eval_or, ctx->exclude, factor, &fake_or_arg);
- if (!ctx->exclude) {
- return NULL;
- }
-
- return &expr_true;
- } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) {
- char **argv = parser_advance(state, T_OPERATOR, 1);
-
- struct expr *factor = parse_factor(state);
- if (!factor) {
- return NULL;
- }
-
- return new_unary_expr(eval_not, factor, argv);
- } else {
- return parse_literal(state);
- }
-}
-
-/**
- * TERM : FACTOR
- * | TERM FACTOR
- * | TERM "-a" FACTOR
- * | TERM "-and" FACTOR
- */
-static struct expr *parse_term(struct parser_state *state) {
- struct expr *term = parse_factor(state);
-
- while (term) {
- if (skip_paths(state) != 0) {
- free_expr(term);
- return NULL;
- }
-
- const char *arg = state->argv[0];
- if (!arg) {
- break;
- }
-
- if (strcmp(arg, "-o") == 0 || strcmp(arg, "-or") == 0
- || strcmp(arg, ",") == 0
- || strcmp(arg, ")") == 0) {
- break;
- }
-
- char **argv = &fake_and_arg;
- if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) {
- argv = parser_advance(state, T_OPERATOR, 1);
- }
-
- struct expr *lhs = term;
- struct expr *rhs = parse_factor(state);
- if (!rhs) {
- free_expr(lhs);
- return NULL;
- }
-
- term = new_binary_expr(eval_and, lhs, rhs, argv);
- }
-
- return term;
-}
-
-/**
- * CLAUSE : TERM
- * | CLAUSE "-o" TERM
- * | CLAUSE "-or" TERM
- */
-static struct expr *parse_clause(struct parser_state *state) {
- struct expr *clause = parse_term(state);
-
- while (clause) {
- if (skip_paths(state) != 0) {
- free_expr(clause);
- return NULL;
- }
-
- const char *arg = state->argv[0];
- if (!arg) {
- break;
- }
-
- if (strcmp(arg, "-o") != 0 && strcmp(arg, "-or") != 0) {
- break;
- }
-
- char **argv = parser_advance(state, T_OPERATOR, 1);
-
- struct expr *lhs = clause;
- struct expr *rhs = parse_term(state);
- if (!rhs) {
- free_expr(lhs);
- return NULL;
- }
-
- clause = new_binary_expr(eval_or, lhs, rhs, argv);
- }
-
- return clause;
-}
-
-/**
- * EXPR : CLAUSE
- * | EXPR "," CLAUSE
- */
-static struct expr *parse_expr(struct parser_state *state) {
- struct expr *expr = parse_clause(state);
-
- while (expr) {
- if (skip_paths(state) != 0) {
- free_expr(expr);
- return NULL;
- }
-
- const char *arg = state->argv[0];
- if (!arg) {
- break;
- }
-
- if (strcmp(arg, ",") != 0) {
- break;
- }
-
- char **argv = parser_advance(state, T_OPERATOR, 1);
-
- struct expr *lhs = expr;
- struct expr *rhs = parse_clause(state);
- if (!rhs) {
- free_expr(lhs);
- return NULL;
- }
-
- expr = new_binary_expr(eval_comma, lhs, rhs, argv);
- }
-
- return expr;
-}
-
-/**
- * Parse the top-level expression.
- */
-static struct expr *parse_whole_expr(struct parser_state *state) {
- if (skip_paths(state) != 0) {
- return NULL;
- }
-
- struct expr *expr = &expr_true;
- if (state->argv[0]) {
- expr = parse_expr(state);
- if (!expr) {
- return NULL;
- }
- }
-
- if (state->argv[0]) {
- parse_error(state, "Unexpected argument ${er}%s${rs}.\n", state->argv[0]);
- goto fail;
- }
-
- if (state->implicit_print) {
- struct expr *print = new_expr(eval_fprint, 1, &fake_print_arg);
- if (!print) {
- goto fail;
- }
- init_print_expr(state, print);
-
- expr = new_binary_expr(eval_and, expr, print, &fake_and_arg);
- if (!expr) {
- goto fail;
- }
- }
-
- if (state->mount_arg && state->xdev_arg) {
- parse_warning(state, "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n", state->xdev_arg, state->mount_arg);
- }
-
- if (state->ctx->warn && state->depth_arg && state->prune_arg) {
- parse_warning(state, "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n", state->prune_arg, state->depth_arg);
-
- if (state->interactive) {
- fprintf(stderr, "Do you want to continue? ");
- if (ynprompt() == 0) {
- goto fail;
- }
- }
-
- fprintf(stderr, "\n");
- }
-
- if (state->ok_arg && state->stdin_consumed) {
- parse_error(state, "${blu}%s${rs} conflicts with ${blu}-files0-from${rs} ${bld}-${rs}.\n", state->ok_arg);
- goto fail;
- }
-
- return expr;
-
-fail:
- free_expr(expr);
- return NULL;
-}
-
-void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
- if (!bfs_debug_prefix(ctx, flag)) {
- return;
- }
-
- CFILE *cerr = ctx->cerr;
-
- cfprintf(cerr, "${ex}%s${rs} ", ctx->argv[0]);
-
- if (ctx->flags & BFTW_FOLLOW_ALL) {
- cfprintf(cerr, "${cyn}-L${rs} ");
- } else if (ctx->flags & BFTW_FOLLOW_ROOTS) {
- cfprintf(cerr, "${cyn}-H${rs} ");
- } else {
- cfprintf(cerr, "${cyn}-P${rs} ");
- }
-
- if (ctx->xargs_safe) {
- cfprintf(cerr, "${cyn}-X${rs} ");
- }
-
- if (ctx->flags & BFTW_SORT) {
- cfprintf(cerr, "${cyn}-s${rs} ");
- }
-
- if (ctx->optlevel != 3) {
- cfprintf(cerr, "${cyn}-O${bld}%d${rs} ", ctx->optlevel);
- }
-
- const char *strategy = NULL;
- switch (ctx->strategy) {
- case BFTW_BFS:
- strategy = "bfs";
- break;
- case BFTW_DFS:
- strategy = "dfs";
- break;
- case BFTW_IDS:
- strategy = "ids";
- break;
- case BFTW_EDS:
- strategy = "eds";
- break;
- }
- assert(strategy);
- cfprintf(cerr, "${cyn}-S${rs} ${bld}%s${rs} ", strategy);
-
- enum debug_flags debug = ctx->debug;
- if (debug == DEBUG_ALL) {
- cfprintf(cerr, "${cyn}-D${rs} ${bld}all${rs} ");
- } else if (debug) {
- cfprintf(cerr, "${cyn}-D${rs} ");
- for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) {
- if (debug & i) {
- cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i));
- debug ^= i;
- if (debug) {
- cfprintf(cerr, ",");
- }
- }
- }
- cfprintf(cerr, " ");
- }
-
- for (size_t i = 0; i < darray_length(ctx->paths); ++i) {
- const char *path = ctx->paths[i];
- char c = path[0];
- if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') {
- cfprintf(cerr, "${cyn}-f${rs} ");
- }
- cfprintf(cerr, "${mag}%s${rs} ", path);
- }
-
- if (ctx->cout->colors) {
- cfprintf(cerr, "${blu}-color${rs} ");
- } else {
- cfprintf(cerr, "${blu}-nocolor${rs} ");
- }
- if (ctx->flags & BFTW_POST_ORDER) {
- cfprintf(cerr, "${blu}-depth${rs} ");
- }
- if (ctx->ignore_races) {
- cfprintf(cerr, "${blu}-ignore_readdir_race${rs} ");
- }
- if (ctx->mindepth != 0) {
- cfprintf(cerr, "${blu}-mindepth${rs} ${bld}%d${rs} ", ctx->mindepth);
- }
- if (ctx->maxdepth != INT_MAX) {
- cfprintf(cerr, "${blu}-maxdepth${rs} ${bld}%d${rs} ", ctx->maxdepth);
- }
- if (ctx->flags & BFTW_SKIP_MOUNTS) {
- cfprintf(cerr, "${blu}-mount${rs} ");
- }
- if (ctx->status) {
- cfprintf(cerr, "${blu}-status${rs} ");
- }
- if (ctx->unique) {
- cfprintf(cerr, "${blu}-unique${rs} ");
- }
- if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) {
- cfprintf(cerr, "${blu}-xdev${rs} ");
- }
-
- if (flag == DEBUG_RATES) {
- if (ctx->exclude != &expr_false) {
- cfprintf(cerr, "(${red}-exclude${rs} %pE) ", ctx->exclude);
- }
- cfprintf(cerr, "%pE", ctx->expr);
- } else {
- if (ctx->exclude != &expr_false) {
- cfprintf(cerr, "(${red}-exclude${rs} %pe) ", ctx->exclude);
- }
- cfprintf(cerr, "%pe", ctx->expr);
- }
-
- fputs("\n", stderr);
-}
-
-/**
- * Dump the estimated costs.
- */
-static void dump_costs(const struct bfs_ctx *ctx) {
- const struct expr *expr = ctx->expr;
- bfs_debug(ctx, DEBUG_COST, " Cost: ~${ylw}%g${rs}\n", expr->cost);
- bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0*expr->probability);
-}
-
-/**
- * Get the current time.
- */
-static int parse_gettime(const struct bfs_ctx *ctx, struct timespec *ts) {
-#if _POSIX_TIMERS > 0
- int ret = clock_gettime(CLOCK_REALTIME, ts);
- if (ret != 0) {
- bfs_perror(ctx, "clock_gettime()");
- }
- return ret;
-#else
- struct timeval tv;
- int ret = gettimeofday(&tv, NULL);
- if (ret == 0) {
- ts->tv_sec = tv.tv_sec;
- ts->tv_nsec = tv.tv_usec * 1000L;
- } else {
- bfs_perror(ctx, "gettimeofday()");
- }
- return ret;
-#endif
-}
-
-struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
- struct bfs_ctx *ctx = bfs_ctx_new();
- if (!ctx) {
- perror("bfs_new_ctx()");
- goto fail;
- }
-
- static char* default_argv[] = {"bfs", NULL};
- if (argc < 1) {
- argc = 1;
- argv = default_argv;
- }
-
- ctx->argv = malloc((argc + 1)*sizeof(*ctx->argv));
- if (!ctx->argv) {
- perror("malloc()");
- goto fail;
- }
- for (int i = 0; i <= argc; ++i) {
- ctx->argv[i] = argv[i];
- }
-
- enum use_color use_color = COLOR_AUTO;
- if (getenv("NO_COLOR")) {
- // https://no-color.org/
- use_color = COLOR_NEVER;
- }
-
- ctx->colors = parse_colors(getenv("LS_COLORS"));
- if (!ctx->colors) {
- ctx->colors_error = errno;
- }
-
- ctx->cerr = cfdup(stderr, use_color ? ctx->colors : NULL);
- if (!ctx->cerr) {
- perror("cfdup()");
- goto fail;
- }
-
- ctx->cout = cfdup(stdout, use_color ? ctx->colors : NULL);
- if (!ctx->cout) {
- bfs_perror(ctx, "cfdup()");
- goto fail;
- }
-
- if (!bfs_ctx_dedup(ctx, ctx->cout, NULL) || !bfs_ctx_dedup(ctx, ctx->cerr, NULL)) {
- bfs_perror(ctx, "bfs_ctx_dedup()");
- goto fail;
- }
-
- bool stdin_tty = isatty(STDIN_FILENO);
- bool stdout_tty = isatty(STDOUT_FILENO);
- bool stderr_tty = isatty(STDERR_FILENO);
-
- if (getenv("POSIXLY_CORRECT")) {
- ctx->posixly_correct = true;
- } else {
- ctx->warn = stdin_tty;
- }
-
- struct parser_state state = {
- .ctx = ctx,
- .argv = ctx->argv + 1,
- .command = ctx->argv[0],
- .regex_flags = 0,
- .stdout_tty = stdout_tty,
- .interactive = stdin_tty && stderr_tty,
- .stdin_consumed = false,
- .use_color = use_color,
- .implicit_print = true,
- .implicit_root = true,
- .non_option_seen = false,
- .just_info = false,
- .excluding = false,
- .last_arg = NULL,
- .depth_arg = NULL,
- .prune_arg = NULL,
- .mount_arg = NULL,
- .xdev_arg = NULL,
- .ok_arg = NULL,
- };
-
- if (strcmp(xbasename(state.command), "find") == 0) {
- // Operate depth-first when invoked as "find"
- ctx->strategy = BFTW_DFS;
- }
-
- if (parse_gettime(ctx, &state.now) != 0) {
- goto fail;
- }
-
- ctx->exclude = &expr_false;
- ctx->expr = parse_whole_expr(&state);
- if (!ctx->expr) {
- if (state.just_info) {
- goto done;
- } else {
- goto fail;
- }
- }
-
- if (bfs_optimize(ctx) != 0) {
- goto fail;
- }
-
- if (darray_length(ctx->paths) == 0) {
- if (!state.implicit_root) {
- parse_error(&state, "No root paths specified.\n");
- goto fail;
- } else if (parse_root(&state, ".") != 0) {
- goto fail;
- }
- }
-
- if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) {
- // We need bftw() to detect cycles unless -unique does it for us
- ctx->flags |= BFTW_DETECT_CYCLES;
- }
-
- bfs_ctx_dump(ctx, DEBUG_TREE);
- dump_costs(ctx);
-
-done:
- return ctx;
-
-fail:
- bfs_ctx_free(ctx);
- return NULL;
-}
diff --git a/parse.h b/parse.h
deleted file mode 100644
index 7e29a03..0000000
--- a/parse.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * bfs command line parsing.
- */
-
-#ifndef BFS_PARSE_H
-#define BFS_PARSE_H
-
-/**
- * Parse the command line.
- *
- * @param argc
- * The number of arguments.
- * @param argv
- * The arguments to parse.
- * @return
- * A new bfs context, or NULL on failure.
- */
-struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]);
-
-#endif // BFS_PARSE_H
diff --git a/printf.c b/printf.c
deleted file mode 100644
index b1b0d59..0000000
--- a/printf.c
+++ /dev/null
@@ -1,881 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "printf.h"
-#include "bftw.h"
-#include "ctx.h"
-#include "diag.h"
-#include "dir.h"
-#include "dstring.h"
-#include "mtab.h"
-#include "pwcache.h"
-#include "stat.h"
-#include "time.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <grp.h>
-#include <pwd.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-
-typedef int bfs_printf_fn(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf);
-
-struct bfs_printf {
- /** The printing function to invoke. */
- bfs_printf_fn *fn;
- /** String data associated with this directive. */
- char *str;
- /** The stat field to print. */
- enum bfs_stat_field stat_field;
- /** Character data associated with this directive. */
- char c;
- /** Some data used by the directive. */
- const void *ptr;
- /** The next printf directive in the chain. */
- struct bfs_printf *next;
-};
-
-/** Print some text as-is. */
-static int bfs_printf_literal(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- size_t len = dstrlen(directive->str);
- if (fwrite(directive->str, 1, len, file) == len) {
- return 0;
- } else {
- return -1;
- }
-}
-
-/** \c: flush */
-static int bfs_printf_flush(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fflush(file);
-}
-
-/**
- * Print a value to a temporary buffer before formatting it.
- */
-#define BFS_PRINTF_BUF(buf, format, ...) \
- char buf[256]; \
- int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \
- assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
- (void)ret
-
-/** %a, %c, %t: ctime() */
-static int bfs_printf_ctime(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- // Not using ctime() itself because GNU find adds nanoseconds
- static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
- static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field);
- if (!ts) {
- return -1;
- }
-
- struct tm tm;
- if (xlocaltime(&ts->tv_sec, &tm) != 0) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d",
- days[tm.tm_wday],
- months[tm.tm_mon],
- tm.tm_mday,
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec,
- 1900 + tm.tm_year);
-
- return fprintf(file, directive->str, buf);
-}
-
-/** %A, %B/%W, %C, %T: strftime() */
-static int bfs_printf_strftime(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field);
- if (!ts) {
- return -1;
- }
-
- struct tm tm;
- if (xlocaltime(&ts->tv_sec, &tm) != 0) {
- return -1;
- }
-
- int ret;
- char buf[256];
- char format[] = "% ";
- switch (directive->c) {
- // Non-POSIX strftime() features
- case '@':
- ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec);
- break;
- case '+':
- ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0",
- 1900 + tm.tm_year,
- tm.tm_mon + 1,
- tm.tm_mday,
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec);
- break;
- case 'k':
- ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour);
- break;
- case 'l':
- ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11)%12 + 1);
- break;
- case 's':
- ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec);
- break;
- case 'S':
- ret = snprintf(buf, sizeof(buf), "%.2d.%09ld0", tm.tm_sec, (long)ts->tv_nsec);
- break;
- case 'T':
- ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0",
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec);
- break;
-
- // POSIX strftime() features
- default:
- format[1] = directive->c;
- ret = strftime(buf, sizeof(buf), format, &tm);
- break;
- }
-
- assert(ret >= 0 && (size_t)ret < sizeof(buf));
- (void)ret;
-
- return fprintf(file, directive->str, buf);
-}
-
-/** %b: blocks */
-static int bfs_printf_b(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 511)/512;
- BFS_PRINTF_BUF(buf, "%ju", blocks);
- return fprintf(file, directive->str, buf);
-}
-
-/** %d: depth */
-static int bfs_printf_d(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fprintf(file, directive->str, (intmax_t)ftwbuf->depth);
-}
-
-/** %D: device */
-static int bfs_printf_D(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
- return fprintf(file, directive->str, buf);
-}
-
-/** %f: file name */
-static int bfs_printf_f(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fprintf(file, directive->str, ftwbuf->path + ftwbuf->nameoff);
-}
-
-/** %F: file system type */
-static int bfs_printf_F(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- const char *type = bfs_fstype(directive->ptr, statbuf);
- return fprintf(file, directive->str, type);
-}
-
-/** %G: gid */
-static int bfs_printf_G(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
- return fprintf(file, directive->str, buf);
-}
-
-/** %g: group name */
-static int bfs_printf_g(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- const struct bfs_groups *groups = directive->ptr;
- const struct group *grp = groups ? bfs_getgrgid(groups, statbuf->gid) : NULL;
- if (!grp) {
- return bfs_printf_G(file, directive, ftwbuf);
- }
-
- return fprintf(file, directive->str, grp->gr_name);
-}
-
-/** %h: leading directories */
-static int bfs_printf_h(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- char *copy = NULL;
- const char *buf;
-
- if (ftwbuf->nameoff > 0) {
- size_t len = ftwbuf->nameoff;
- if (len > 1) {
- --len;
- }
-
- buf = copy = strndup(ftwbuf->path, len);
- } else if (ftwbuf->path[0] == '/') {
- buf = "/";
- } else {
- buf = ".";
- }
-
- if (!buf) {
- return -1;
- }
-
- int ret = fprintf(file, directive->str, buf);
- free(copy);
- return ret;
-}
-
-/** %H: current root */
-static int bfs_printf_H(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fprintf(file, directive->str, ftwbuf->root);
-}
-
-/** %i: inode */
-static int bfs_printf_i(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
- return fprintf(file, directive->str, buf);
-}
-
-/** %k: 1K blocks */
-static int bfs_printf_k(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024;
- BFS_PRINTF_BUF(buf, "%ju", blocks);
- return fprintf(file, directive->str, buf);
-}
-
-/** %l: link target */
-static int bfs_printf_l(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- char *buf = NULL;
- const char *target = "";
-
- if (ftwbuf->type == BFS_LNK) {
- const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
- size_t len = statbuf ? statbuf->size : 0;
-
- target = buf = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
- if (!target) {
- return -1;
- }
- }
-
- int ret = fprintf(file, directive->str, target);
- free(buf);
- return ret;
-}
-
-/** %m: mode */
-static int bfs_printf_m(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- return fprintf(file, directive->str, (unsigned int)(statbuf->mode & 07777));
-}
-
-/** %M: symbolic mode */
-static int bfs_printf_M(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- char buf[11];
- xstrmode(statbuf->mode, buf);
- return fprintf(file, directive->str, buf);
-}
-
-/** %n: link count */
-static int bfs_printf_n(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
- return fprintf(file, directive->str, buf);
-}
-
-/** %p: full path */
-static int bfs_printf_p(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fprintf(file, directive->str, ftwbuf->path);
-}
-
-/** %P: path after root */
-static int bfs_printf_P(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const char *path = ftwbuf->path + strlen(ftwbuf->root);
- if (path[0] == '/') {
- ++path;
- }
- return fprintf(file, directive->str, path);
-}
-
-/** %s: size */
-static int bfs_printf_s(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
- return fprintf(file, directive->str, buf);
-}
-
-/** %S: sparseness */
-static int bfs_printf_S(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- double sparsity;
- if (statbuf->size == 0 && statbuf->blocks == 0) {
- sparsity = 1.0;
- } else {
- sparsity = (double)BFS_STAT_BLKSIZE*statbuf->blocks/statbuf->size;
- }
- return fprintf(file, directive->str, sparsity);
-}
-
-/** %U: uid */
-static int bfs_printf_U(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
- return fprintf(file, directive->str, buf);
-}
-
-/** %u: user name */
-static int bfs_printf_u(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
- if (!statbuf) {
- return -1;
- }
-
- const struct bfs_users *users = directive->ptr;
- const struct passwd *pwd = users ? bfs_getpwuid(users, statbuf->uid) : NULL;
- if (!pwd) {
- return bfs_printf_U(file, directive, ftwbuf);
- }
-
- return fprintf(file, directive->str, pwd->pw_name);
-}
-
-static const char *bfs_printf_type(enum bfs_type type) {
- switch (type) {
- case BFS_BLK:
- return "b";
- case BFS_CHR:
- return "c";
- case BFS_DIR:
- return "d";
- case BFS_DOOR:
- return "D";
- case BFS_FIFO:
- return "p";
- case BFS_LNK:
- return "l";
- case BFS_REG:
- return "f";
- case BFS_SOCK:
- return "s";
- default:
- return "U";
- }
-}
-
-/** %y: type */
-static int bfs_printf_y(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- const char *type = bfs_printf_type(ftwbuf->type);
- return fprintf(file, directive->str, type);
-}
-
-/** %Y: target type */
-static int bfs_printf_Y(FILE *file, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- int error = 0;
-
- if (ftwbuf->type != BFS_LNK) {
- return bfs_printf_y(file, directive, ftwbuf);
- }
-
- const char *type = "U";
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW);
- if (statbuf) {
- type = bfs_printf_type(bfs_mode_to_type(statbuf->mode));
- } else {
- switch (errno) {
- case ELOOP:
- type = "L";
- break;
- case ENOENT:
- case ENOTDIR:
- type = "N";
- break;
- default:
- type = "?";
- error = errno;
- break;
- }
- }
-
- int ret = fprintf(file, directive->str, type);
- if (error != 0) {
- ret = -1;
- errno = error;
- }
- return ret;
-}
-
-/**
- * Free a printf directive.
- */
-static void free_directive(struct bfs_printf *directive) {
- if (directive) {
- dstrfree(directive->str);
- free(directive);
- }
-}
-
-/**
- * Create a new printf directive.
- */
-static struct bfs_printf *new_directive(const struct bfs_ctx *ctx, bfs_printf_fn *fn) {
- struct bfs_printf *directive = malloc(sizeof(*directive));
- if (!directive) {
- bfs_perror(ctx, "malloc()");
- goto error;
- }
-
- directive->fn = fn;
- directive->str = dstralloc(2);
- if (!directive->str) {
- bfs_perror(ctx, "dstralloc()");
- goto error;
- }
- directive->stat_field = 0;
- directive->c = 0;
- directive->ptr = NULL;
- directive->next = NULL;
- return directive;
-
-error:
- free_directive(directive);
- return NULL;
-}
-
-/**
- * Append a printf directive to the chain.
- */
-static struct bfs_printf **append_directive(struct bfs_printf **tail, struct bfs_printf *directive) {
- assert(directive);
- *tail = directive;
- return &directive->next;
-}
-
-/**
- * Append a literal string to the chain.
- */
-static struct bfs_printf **append_literal(struct bfs_printf **tail, struct bfs_printf **literal) {
- struct bfs_printf *directive = *literal;
- if (directive && dstrlen(directive->str) > 0) {
- *literal = NULL;
- return append_directive(tail, directive);
- } else {
- return tail;
- }
-}
-
-struct bfs_printf *bfs_printf_parse(const struct bfs_ctx *ctx, const char *format) {
- struct bfs_printf *head = NULL;
- struct bfs_printf **tail = &head;
-
- struct bfs_printf *literal = new_directive(ctx, bfs_printf_literal);
- if (!literal) {
- goto error;
- }
-
- for (const char *i = format; *i; ++i) {
- char c = *i;
-
- if (c == '\\') {
- c = *++i;
-
- if (c >= '0' && c < '8') {
- c = 0;
- for (int j = 0; j < 3 && *i >= '0' && *i < '8'; ++i, ++j) {
- c *= 8;
- c += *i - '0';
- }
- --i;
- goto one_char;
- }
-
- switch (c) {
- case 'a': c = '\a'; break;
- case 'b': c = '\b'; break;
- case 'f': c = '\f'; break;
- case 'n': c = '\n'; break;
- case 'r': c = '\r'; break;
- case 't': c = '\t'; break;
- case 'v': c = '\v'; break;
- case '\\': c = '\\'; break;
-
- case 'c':
- tail = append_literal(tail, &literal);
- struct bfs_printf *directive = new_directive(ctx, bfs_printf_flush);
- if (!directive) {
- goto error;
- }
- tail = append_directive(tail, directive);
- goto done;
-
- case '\0':
- bfs_error(ctx, "'%s': Incomplete escape sequence '\\'.\n", format);
- goto error;
-
- default:
- bfs_error(ctx, "'%s': Unrecognized escape sequence '\\%c'.\n", format, c);
- goto error;
- }
- } else if (c == '%') {
- if (i[1] == '%') {
- c = *++i;
- goto one_char;
- }
-
- struct bfs_printf *directive = new_directive(ctx, NULL);
- if (!directive) {
- goto directive_error;
- }
- if (dstrapp(&directive->str, c) != 0) {
- bfs_perror(ctx, "dstrapp()");
- goto directive_error;
- }
-
- const char *specifier = "s";
-
- // Parse any flags
- bool must_be_numeric = false;
- while (true) {
- c = *++i;
-
- switch (c) {
- case '#':
- case '0':
- case '+':
- must_be_numeric = true;
- BFS_FALLTHROUGH;
- case ' ':
- case '-':
- if (strchr(directive->str, c)) {
- bfs_error(ctx, "'%s': Duplicate flag '%c'.\n", format, c);
- goto directive_error;
- }
- if (dstrapp(&directive->str, c) != 0) {
- bfs_perror(ctx, "dstrapp()");
- goto directive_error;
- }
- continue;
- }
-
- break;
- }
-
- // Parse the field width
- while (c >= '0' && c <= '9') {
- if (dstrapp(&directive->str, c) != 0) {
- bfs_perror(ctx, "dstrapp()");
- goto directive_error;
- }
- c = *++i;
- }
-
- // Parse the precision
- if (c == '.') {
- do {
- if (dstrapp(&directive->str, c) != 0) {
- bfs_perror(ctx, "dstrapp()");
- goto directive_error;
- }
- c = *++i;
- } while (c >= '0' && c <= '9');
- }
-
- switch (c) {
- case 'a':
- directive->fn = bfs_printf_ctime;
- directive->stat_field = BFS_STAT_ATIME;
- break;
- case 'b':
- directive->fn = bfs_printf_b;
- break;
- case 'c':
- directive->fn = bfs_printf_ctime;
- directive->stat_field = BFS_STAT_CTIME;
- break;
- case 'd':
- directive->fn = bfs_printf_d;
- specifier = "jd";
- break;
- case 'D':
- directive->fn = bfs_printf_D;
- break;
- case 'f':
- directive->fn = bfs_printf_f;
- break;
- case 'F':
- directive->ptr = bfs_ctx_mtab(ctx);
- if (!directive->ptr) {
- bfs_error(ctx, "Couldn't parse the mount table: %m.\n");
- goto directive_error;
- }
- directive->fn = bfs_printf_F;
- break;
- case 'g':
- directive->ptr = bfs_ctx_groups(ctx);
- if (!directive->ptr) {
- bfs_error(ctx, "Couldn't parse the group table: %m.\n");
- goto directive_error;
- }
- directive->fn = bfs_printf_g;
- break;
- case 'G':
- directive->fn = bfs_printf_G;
- break;
- case 'h':
- directive->fn = bfs_printf_h;
- break;
- case 'H':
- directive->fn = bfs_printf_H;
- break;
- case 'i':
- directive->fn = bfs_printf_i;
- break;
- case 'k':
- directive->fn = bfs_printf_k;
- break;
- case 'l':
- directive->fn = bfs_printf_l;
- break;
- case 'm':
- directive->fn = bfs_printf_m;
- specifier = "o";
- break;
- case 'M':
- directive->fn = bfs_printf_M;
- break;
- case 'n':
- directive->fn = bfs_printf_n;
- break;
- case 'p':
- directive->fn = bfs_printf_p;
- break;
- case 'P':
- directive->fn = bfs_printf_P;
- break;
- case 's':
- directive->fn = bfs_printf_s;
- break;
- case 'S':
- directive->fn = bfs_printf_S;
- specifier = "g";
- break;
- case 't':
- directive->fn = bfs_printf_ctime;
- directive->stat_field = BFS_STAT_MTIME;
- break;
- case 'u':
- directive->ptr = bfs_ctx_users(ctx);
- if (!directive->ptr) {
- bfs_error(ctx, "Couldn't parse the user table: %m.\n");
- goto directive_error;
- }
- directive->fn = bfs_printf_u;
- break;
- case 'U':
- directive->fn = bfs_printf_U;
- break;
- case 'w':
- directive->fn = bfs_printf_ctime;
- directive->stat_field = BFS_STAT_BTIME;
- break;
- case 'y':
- directive->fn = bfs_printf_y;
- break;
- case 'Y':
- directive->fn = bfs_printf_Y;
- break;
-
- case 'A':
- directive->stat_field = BFS_STAT_ATIME;
- goto directive_strftime;
- case 'B':
- case 'W':
- directive->stat_field = BFS_STAT_BTIME;
- goto directive_strftime;
- case 'C':
- directive->stat_field = BFS_STAT_CTIME;
- goto directive_strftime;
- case 'T':
- directive->stat_field = BFS_STAT_MTIME;
- goto directive_strftime;
-
- directive_strftime:
- directive->fn = bfs_printf_strftime;
- c = *++i;
- if (!c) {
- bfs_error(ctx, "'%s': Incomplete time specifier '%s%c'.\n", format, directive->str, i[-1]);
- goto directive_error;
- } else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) {
- directive->c = c;
- } else {
- bfs_error(ctx, "'%s': Unrecognized time specifier '%%%c%c'.\n", format, i[-1], c);
- goto directive_error;
- }
- break;
-
- case '\0':
- bfs_error(ctx, "'%s': Incomplete format specifier '%s'.\n", format, directive->str);
- goto directive_error;
-
- default:
- bfs_error(ctx, "'%s': Unrecognized format specifier '%%%c'.\n", format, c);
- goto directive_error;
- }
-
- if (must_be_numeric && strcmp(specifier, "s") == 0) {
- bfs_error(ctx, "'%s': Invalid flags '%s' for string format '%%%c'.\n", format, directive->str + 1, c);
- goto directive_error;
- }
-
- if (dstrcat(&directive->str, specifier) != 0) {
- bfs_perror(ctx, "dstrcat()");
- goto directive_error;
- }
-
- tail = append_literal(tail, &literal);
- tail = append_directive(tail, directive);
-
- if (!literal) {
- literal = new_directive(ctx, bfs_printf_literal);
- if (!literal) {
- goto error;
- }
- }
-
- continue;
-
- directive_error:
- free_directive(directive);
- goto error;
- }
-
- one_char:
- if (dstrapp(&literal->str, c) != 0) {
- bfs_perror(ctx, "dstrapp()");
- goto error;
- }
- }
-
-done:
- tail = append_literal(tail, &literal);
- if (head) {
- free_directive(literal);
- return head;
- } else {
- return literal;
- }
-
-error:
- free_directive(literal);
- bfs_printf_free(head);
- return NULL;
-}
-
-int bfs_printf(FILE *file, const struct bfs_printf *format, const struct BFTW *ftwbuf) {
- int ret = 0, error = 0;
-
- for (const struct bfs_printf *directive = format; directive; directive = directive->next) {
- if (directive->fn(file, directive, ftwbuf) < 0) {
- ret = -1;
- error = errno;
- }
- }
-
- errno = error;
- return ret;
-}
-
-void bfs_printf_free(struct bfs_printf *format) {
- while (format) {
- struct bfs_printf *next = format->next;
- free_directive(format);
- format = next;
- }
-}
diff --git a/printf.h b/printf.h
deleted file mode 100644
index 91cf187..0000000
--- a/printf.h
+++ /dev/null
@@ -1,65 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Implementation of -printf/-fprintf.
- */
-
-#ifndef BFS_PRINTF_H
-#define BFS_PRINTF_H
-
-#include <stdio.h>
-
-struct BFTW;
-struct bfs_ctx;
-
-/**
- * A printf command, the result of parsing a single format string.
- */
-struct bfs_printf;
-
-/**
- * Parse a -printf format string.
- *
- * @param ctx
- * The bfs context.
- * @param format
- * The format string to parse.
- * @return
- * The parsed printf command, or NULL on failure.
- */
-struct bfs_printf *bfs_printf_parse(const struct bfs_ctx *ctx, const char *format);
-
-/**
- * Evaluate a parsed format string.
- *
- * @param file
- * The FILE to print to.
- * @param format
- * The parsed printf format.
- * @param ftwbuf
- * The bftw() data for the current file.
- * @return
- * 0 on success, -1 on failure.
- */
-int bfs_printf(FILE *file, const struct bfs_printf *format, const struct BFTW *ftwbuf);
-
-/**
- * Free a parsed format string.
- */
-void bfs_printf_free(struct bfs_printf *format);
-
-#endif // BFS_PRINTF_H
diff --git a/pwcache.c b/pwcache.c
deleted file mode 100644
index 7812b50..0000000
--- a/pwcache.c
+++ /dev/null
@@ -1,293 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "pwcache.h"
-#include "darray.h"
-#include "trie.h"
-#include <errno.h>
-#include <grp.h>
-#include <pwd.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <string.h>
-
-struct bfs_users {
- /** The array of passwd entries. */
- struct passwd *entries;
- /** A map from usernames to entries. */
- struct trie by_name;
- /** A map from UIDs to entries. */
- struct trie by_uid;
-};
-
-struct bfs_users *bfs_users_parse(void) {
- int error;
-
- struct bfs_users *users = malloc(sizeof(*users));
- if (!users) {
- return NULL;
- }
-
- users->entries = NULL;
- trie_init(&users->by_name);
- trie_init(&users->by_uid);
-
- setpwent();
-
- while (true) {
- errno = 0;
- struct passwd *ent = getpwent();
- if (!ent) {
- if (errno) {
- error = errno;
- goto fail_end;
- } else {
- break;
- }
- }
-
- if (DARRAY_PUSH(&users->entries, ent) != 0) {
- error = errno;
- goto fail_end;
- }
-
- ent = users->entries + darray_length(users->entries) - 1;
- ent->pw_name = strdup(ent->pw_name);
- ent->pw_dir = strdup(ent->pw_dir);
- ent->pw_shell = strdup(ent->pw_shell);
- if (!ent->pw_name || !ent->pw_dir || !ent->pw_shell) {
- error = ENOMEM;
- goto fail_end;
- }
- }
-
- endpwent();
-
- for (size_t i = 0; i < darray_length(users->entries); ++i) {
- struct passwd *entry = users->entries + i;
- struct trie_leaf *leaf = trie_insert_str(&users->by_name, entry->pw_name);
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
-
- leaf = trie_insert_mem(&users->by_uid, &entry->pw_uid, sizeof(entry->pw_uid));
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
- }
-
- return users;
-
-fail_end:
- endpwent();
-fail_free:
- bfs_users_free(users);
- errno = error;
- return NULL;
-}
-
-const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name) {
- const struct trie_leaf *leaf = trie_find_str(&users->by_name, name);
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
-}
-
-const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid) {
- const struct trie_leaf *leaf = trie_find_mem(&users->by_uid, &uid, sizeof(uid));
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
-}
-
-void bfs_users_free(struct bfs_users *users) {
- if (users) {
- trie_destroy(&users->by_uid);
- trie_destroy(&users->by_name);
-
- for (size_t i = 0; i < darray_length(users->entries); ++i) {
- struct passwd *entry = users->entries + i;
- free(entry->pw_shell);
- free(entry->pw_dir);
- free(entry->pw_name);
- }
- darray_free(users->entries);
-
- free(users);
- }
-}
-
-struct bfs_groups {
- /** The array of group entries. */
- struct group *entries;
- /** A map from group names to entries. */
- struct trie by_name;
- /** A map from GIDs to entries. */
- struct trie by_gid;
-};
-
-/**
- * struct group::gr_mem isn't properly aligned on macOS, so do this to avoid
- * ASAN warnings.
- */
-static char *next_gr_mem(void **gr_mem) {
- char *mem;
- memcpy(&mem, *gr_mem, sizeof(mem));
- *gr_mem = (char *)*gr_mem + sizeof(mem);
- return mem;
-}
-
-struct bfs_groups *bfs_groups_parse(void) {
- int error;
-
- struct bfs_groups *groups = malloc(sizeof(*groups));
- if (!groups) {
- return NULL;
- }
-
- groups->entries = NULL;
- trie_init(&groups->by_name);
- trie_init(&groups->by_gid);
-
- setgrent();
-
- while (true) {
- errno = 0;
- struct group *ent = getgrent();
- if (!ent) {
- if (errno) {
- error = errno;
- goto fail_end;
- } else {
- break;
- }
- }
-
- if (DARRAY_PUSH(&groups->entries, ent) != 0) {
- error = errno;
- goto fail_end;
- }
- ent = groups->entries + darray_length(groups->entries) - 1;
-
- void *members = ent->gr_mem;
- ent->gr_mem = NULL;
-
- ent->gr_name = strdup(ent->gr_name);
- if (!ent->gr_name) {
- error = errno;
- goto fail_end;
- }
-
- for (char *mem = next_gr_mem(&members); mem; mem = next_gr_mem(&members)) {
- char *dup = strdup(mem);
- if (!dup) {
- error = errno;
- goto fail_end;
- }
-
- if (DARRAY_PUSH(&ent->gr_mem, &dup) != 0) {
- error = errno;
- free(dup);
- goto fail_end;
- }
- }
- }
-
- endgrent();
-
- for (size_t i = 0; i < darray_length(groups->entries); ++i) {
- struct group *entry = groups->entries + i;
- struct trie_leaf *leaf = trie_insert_str(&groups->by_name, entry->gr_name);
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
-
- leaf = trie_insert_mem(&groups->by_gid, &entry->gr_gid, sizeof(entry->gr_gid));
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
- }
-
- return groups;
-
-fail_end:
- endgrent();
-fail_free:
- bfs_groups_free(groups);
- errno = error;
- return NULL;
-}
-
-const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name) {
- const struct trie_leaf *leaf = trie_find_str(&groups->by_name, name);
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
-}
-
-const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid) {
- const struct trie_leaf *leaf = trie_find_mem(&groups->by_gid, &gid, sizeof(gid));
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
-}
-
-void bfs_groups_free(struct bfs_groups *groups) {
- if (groups) {
- trie_destroy(&groups->by_gid);
- trie_destroy(&groups->by_name);
-
- for (size_t i = 0; i < darray_length(groups->entries); ++i) {
- struct group *entry = groups->entries + i;
- for (size_t j = 0; j < darray_length(entry->gr_mem); ++j) {
- free(entry->gr_mem[j]);
- }
- darray_free(entry->gr_mem);
- free(entry->gr_name);
- }
- darray_free(groups->entries);
-
- free(groups);
- }
-}
diff --git a/pwcache.h b/pwcache.h
deleted file mode 100644
index f1a1db3..0000000
--- a/pwcache.h
+++ /dev/null
@@ -1,117 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A caching wrapper for /etc/{passwd,group}.
- */
-
-#ifndef BFS_PWCACHE_H
-#define BFS_PWCACHE_H
-
-#include <grp.h>
-#include <pwd.h>
-
-/**
- * The user table.
- */
-struct bfs_users;
-
-/**
- * Parse the user table.
- *
- * @return
- * The parsed user table, or NULL on failure.
- */
-struct bfs_users *bfs_users_parse(void);
-
-/**
- * Get a user entry by name.
- *
- * @param users
- * The user table.
- * @param name
- * The username to look up.
- * @return
- * The matching user, or NULL if not found.
- */
-const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name);
-
-/**
- * Get a user entry by ID.
- *
- * @param users
- * The user table.
- * @param uid
- * The ID to look up.
- * @return
- * The matching user, or NULL if not found.
- */
-const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid);
-
-/**
- * Free a user table.
- *
- * @param users
- * The user table to free.
- */
-void bfs_users_free(struct bfs_users *users);
-
-/**
- * The group table.
- */
-struct bfs_groups;
-
-/**
- * Parse the group table.
- *
- * @return
- * The parsed group table, or NULL on failure.
- */
-struct bfs_groups *bfs_groups_parse(void);
-
-/**
- * Get a group entry by name.
- *
- * @param groups
- * The group table.
- * @param name
- * The group name to look up.
- * @return
- * The matching group, or NULL if not found.
- */
-const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name);
-
-/**
- * Get a group entry by ID.
- *
- * @param groups
- * The group table.
- * @param uid
- * The ID to look up.
- * @return
- * The matching group, or NULL if not found.
- */
-const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid);
-
-/**
- * Free a group table.
- *
- * @param groups
- * The group table to free.
- */
-void bfs_groups_free(struct bfs_groups *groups);
-
-#endif // BFS_PWCACHE_H
diff --git a/spawn.c b/spawn.c
deleted file mode 100644
index d4ff4fd..0000000
--- a/spawn.c
+++ /dev/null
@@ -1,321 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "spawn.h"
-#include "util.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-/**
- * Types of spawn actions.
- */
-enum bfs_spawn_op {
- BFS_SPAWN_CLOSE,
- BFS_SPAWN_DUP2,
- BFS_SPAWN_FCHDIR,
- BFS_SPAWN_SETRLIMIT,
-};
-
-/**
- * A spawn action.
- */
-struct bfs_spawn_action {
- struct bfs_spawn_action *next;
-
- enum bfs_spawn_op op;
- int in_fd;
- int out_fd;
- int resource;
- struct rlimit rlimit;
-};
-
-int bfs_spawn_init(struct bfs_spawn *ctx) {
- ctx->flags = 0;
- ctx->actions = NULL;
- ctx->tail = &ctx->actions;
- return 0;
-}
-
-int bfs_spawn_destroy(struct bfs_spawn *ctx) {
- struct bfs_spawn_action *action = ctx->actions;
- while (action) {
- struct bfs_spawn_action *next = action->next;
- free(action);
- action = next;
- }
- return 0;
-}
-
-int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) {
- ctx->flags = flags;
- return 0;
-}
-
-/** Add a spawn action to the chain. */
-static struct bfs_spawn_action *bfs_spawn_add(struct bfs_spawn *ctx, enum bfs_spawn_op op) {
- struct bfs_spawn_action *action = malloc(sizeof(*action));
- if (action) {
- action->next = NULL;
- action->op = op;
- action->in_fd = -1;
- action->out_fd = -1;
-
- *ctx->tail = action;
- ctx->tail = &action->next;
- }
- return action;
-}
-
-int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) {
- if (fd < 0) {
- errno = EBADF;
- return -1;
- }
-
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE);
- if (action) {
- action->out_fd = fd;
- return 0;
- } else {
- return -1;
- }
-}
-
-int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
- if (oldfd < 0 || newfd < 0) {
- errno = EBADF;
- return -1;
- }
-
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_DUP2);
- if (action) {
- action->in_fd = oldfd;
- action->out_fd = newfd;
- return 0;
- } else {
- return -1;
- }
-}
-
-int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
- if (fd < 0) {
- errno = EBADF;
- return -1;
- }
-
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR);
- if (action) {
- action->in_fd = fd;
- return 0;
- } else {
- return -1;
- }
-}
-
-int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) {
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_SETRLIMIT);
- if (action) {
- action->resource = resource;
- action->rlimit = *rl;
- return 0;
- } else {
- return -1;
- }
-}
-
-/** Actually exec() the new process. */
-static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) {
- int error;
- const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL;
-
- close(pipefd[0]);
-
- for (const struct bfs_spawn_action *action = actions; action; action = action->next) {
- // Move the error-reporting pipe out of the way if necessary...
- if (action->out_fd == pipefd[1]) {
- int fd = dup_cloexec(pipefd[1]);
- if (fd < 0) {
- goto fail;
- }
- close(pipefd[1]);
- pipefd[1] = fd;
- }
-
- // ... and pretend the pipe doesn't exist
- if (action->in_fd == pipefd[1]) {
- errno = EBADF;
- goto fail;
- }
-
- switch (action->op) {
- case BFS_SPAWN_CLOSE:
- if (close(action->out_fd) != 0) {
- goto fail;
- }
- break;
- case BFS_SPAWN_DUP2:
- if (dup2(action->in_fd, action->out_fd) < 0) {
- goto fail;
- }
- break;
- case BFS_SPAWN_FCHDIR:
- if (fchdir(action->in_fd) != 0) {
- goto fail;
- }
- break;
- case BFS_SPAWN_SETRLIMIT:
- if (setrlimit(action->resource, &action->rlimit) != 0) {
- goto fail;
- }
- break;
- }
- }
-
- execve(exe, argv, envp);
-
-fail:
- error = errno;
-
- // In case of a write error, the parent will still see that we exited
- // unsuccessfully, but won't know why
- (void) xwrite(pipefd[1], &error, sizeof(error));
-
- close(pipefd[1]);
- _Exit(127);
-}
-
-pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
- extern char **environ;
- if (!envp) {
- envp = environ;
- }
-
- enum bfs_spawn_flags flags = ctx ? ctx->flags : 0;
- char *resolved = NULL;
- if (flags & BFS_SPAWN_USEPATH) {
- exe = resolved = bfs_spawn_resolve(exe);
- if (!resolved) {
- return -1;
- }
- }
-
- // Use a pipe to report errors from the child
- int pipefd[2];
- if (pipe_cloexec(pipefd) != 0) {
- free(resolved);
- return -1;
- }
-
- int error;
- pid_t pid = fork();
-
- if (pid < 0) {
- error = errno;
- close(pipefd[1]);
- close(pipefd[0]);
- free(resolved);
- errno = error;
- return -1;
- } else if (pid == 0) {
- // Child
- bfs_spawn_exec(exe, ctx, argv, envp, pipefd);
- }
-
- // Parent
- close(pipefd[1]);
- free(resolved);
-
- ssize_t nbytes = xread(pipefd[0], &error, sizeof(error));
- close(pipefd[0]);
- if (nbytes == sizeof(error)) {
- int wstatus;
- waitpid(pid, &wstatus, 0);
- errno = error;
- return -1;
- }
-
- return pid;
-}
-
-char *bfs_spawn_resolve(const char *exe) {
- if (strchr(exe, '/')) {
- return strdup(exe);
- }
-
- const char *path = getenv("PATH");
-
- char *confpath = NULL;
- if (!path) {
- path = confpath = xconfstr(_CS_PATH);
- if (!path) {
- return NULL;
- }
- }
-
- size_t cap = 0;
- char *ret = NULL;
- while (true) {
- const char *end = strchr(path, ':');
- size_t len = end ? (size_t)(end - path) : strlen(path);
-
- // POSIX 8.3: "A zero-length prefix is a legacy feature that
- // indicates the current working directory."
- if (len == 0) {
- path = ".";
- len = 1;
- }
-
- size_t total = len + 1 + strlen(exe) + 1;
- if (cap < total) {
- char *grown = realloc(ret, total);
- if (!grown) {
- goto fail;
- }
- ret = grown;
- cap = total;
- }
-
- memcpy(ret, path, len);
- if (ret[len - 1] != '/') {
- ret[len++] = '/';
- }
- strcpy(ret + len, exe);
-
- if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) {
- break;
- }
-
- if (!end) {
- errno = ENOENT;
- goto fail;
- }
-
- path = end + 1;
- }
-
- free(confpath);
- return ret;
-
-fail:
- free(confpath);
- free(ret);
- return NULL;
-}
diff --git a/spawn.h b/spawn.h
deleted file mode 100644
index 7bae89c..0000000
--- a/spawn.h
+++ /dev/null
@@ -1,123 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A process-spawning library inspired by posix_spawn().
- */
-
-#ifndef BFS_SPAWN_H
-#define BFS_SPAWN_H
-
-#include <sys/resource.h>
-#include <sys/types.h>
-
-/**
- * bfs_spawn() flags.
- */
-enum bfs_spawn_flags {
- /** Use the PATH variable to resolve the executable (like execvp()). */
- BFS_SPAWN_USEPATH = 1 << 0,
-};
-
-/**
- * bfs_spawn() attributes, controlling the context of the new process.
- */
-struct bfs_spawn {
- enum bfs_spawn_flags flags;
- struct bfs_spawn_action *actions;
- struct bfs_spawn_action **tail;
-};
-
-/**
- * Create a new bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_init(struct bfs_spawn *ctx);
-
-/**
- * Destroy a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_destroy(struct bfs_spawn *ctx);
-
-/**
- * Set the flags for a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags);
-
-/**
- * Add a close() action to a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd);
-
-/**
- * Add a dup2() action to a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd);
-
-/**
- * Add an fchdir() action to a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd);
-
-/**
- * Add a setrlimit() action to a bfs_spawn() context.
- *
- * @return 0 on success, -1 on failure.
- */
-int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl);
-
-/**
- * Spawn a new process.
- *
- * @param exe
- * The executable to run.
- * @param ctx
- * The context for the new process.
- * @param argv
- * The arguments for the new process.
- * @param envp
- * The environment variables for the new process (NULL for the current
- * environment.
- * @return
- * The PID of the new process, or -1 on error.
- */
-pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp);
-
-/**
- * Look up an executable in the current PATH, as BFS_SPAWN_USEPATH or execvp()
- * would do.
- *
- * @param exe
- * The name of the binary to execute. Bare names without a '/' will be
- * searched on the provided PATH.
- * @return
- * The full path to the executable, which should be free()'d, or NULL on
- * failure.
- */
-char *bfs_spawn_resolve(const char *exe);
-
-#endif // BFS_SPAWN_H
diff --git a/src/alloc.c b/src/alloc.c
new file mode 100644
index 0000000..f505eda
--- /dev/null
+++ b/src/alloc.c
@@ -0,0 +1,382 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "alloc.h"
+
+#include "bfs.h"
+#include "bit.h"
+#include "diag.h"
+#include "sanity.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+/** The largest possible allocation size. */
+#if PTRDIFF_MAX < SIZE_MAX / 2
+# define ALLOC_MAX ((size_t)PTRDIFF_MAX)
+#else
+# define ALLOC_MAX (SIZE_MAX / 2)
+#endif
+
+/** posix_memalign() wrapper. */
+static void *xmemalign(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(align >= sizeof(void *));
+
+ // Since https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2072.htm,
+ // aligned_alloc() doesn't require the size to be a multiple of align.
+ // But the sanitizers don't know about that yet, so always use
+ // posix_memalign().
+ void *ptr = NULL;
+ errno = posix_memalign(&ptr, align, size);
+ return ptr;
+}
+
+void *alloc(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+
+ if (size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return malloc(size);
+ } else {
+ return xmemalign(align, size);
+ }
+}
+
+void *zalloc(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+
+ if (size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return calloc(1, size);
+ }
+
+ void *ret = xmemalign(align, size);
+ if (ret) {
+ memset(ret, 0, size);
+ }
+ return ret;
+}
+
+void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size) {
+ bfs_assert(has_single_bit(align));
+
+ if (new_size == 0) {
+ free(ptr);
+ return NULL;
+ } else if (new_size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return realloc(ptr, new_size);
+ }
+
+ // There is no aligned_realloc(), so reallocate and copy manually
+ void *ret = xmemalign(align, new_size);
+ if (!ret) {
+ return NULL;
+ }
+
+ size_t min_size = old_size < new_size ? old_size : new_size;
+ if (min_size) {
+ memcpy(ret, ptr, min_size);
+ }
+
+ free(ptr);
+ return ret;
+}
+
+void *reserve(void *ptr, size_t align, size_t size, size_t count) {
+ // No need to overflow-check the current size
+ size_t old_size = size * count;
+
+ // Capacity is doubled every power of two, from 0→1, 1→2, 2→4, etc.
+ // If we stayed within the same size class, reuse ptr.
+ if (count & (count - 1)) {
+ // Tell sanitizers about the new array element
+ sanitize_resize(ptr, old_size, old_size + size, bit_ceil(count) * size);
+ errno = 0;
+ return ptr;
+ }
+
+ // No need to overflow-check; xrealloc() will fail before we overflow
+ size_t new_size = count ? 2 * old_size : size;
+ void *ret = xrealloc(ptr, align, old_size, new_size);
+ if (!ret) {
+ // errno is used to communicate success/failure to the RESERVE() macro
+ bfs_assert(errno != 0);
+ return ptr;
+ }
+
+ // Pretend we only allocated one more element
+ sanitize_resize(ret, new_size, old_size + size, new_size);
+ errno = 0;
+ return ret;
+}
+
+/**
+ * An arena allocator chunk.
+ */
+union chunk {
+ /**
+ * Free chunks are stored in a singly linked list. The pointer to the
+ * next chunk is represented by an offset from the chunk immediately
+ * after this one in memory, so that zalloc() correctly initializes a
+ * linked list of chunks (except for the last one).
+ */
+ uintptr_t next;
+
+ // char object[];
+};
+
+/** Decode the next chunk. */
+static union chunk *chunk_next(const struct arena *arena, const union chunk *chunk) {
+ uintptr_t base = (uintptr_t)chunk + arena->size;
+ return (union chunk *)(base + chunk->next);
+}
+
+/** Encode the next chunk. */
+static void chunk_set_next(const struct arena *arena, union chunk *chunk, union chunk *next) {
+ uintptr_t base = (uintptr_t)chunk + arena->size;
+ chunk->next = (uintptr_t)next - base;
+}
+
+void arena_init(struct arena *arena, size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(is_aligned(align, size));
+
+ if (align < alignof(union chunk)) {
+ align = alignof(union chunk);
+ }
+ if (size < sizeof(union chunk)) {
+ size = sizeof(union chunk);
+ }
+ bfs_assert(is_aligned(align, size));
+
+ arena->chunks = NULL;
+ arena->nslabs = 0;
+ arena->slabs = NULL;
+ arena->align = align;
+ arena->size = size;
+}
+
+/** Allocate a new slab. */
+_cold
+static int slab_alloc(struct arena *arena) {
+ // Make the initial allocation size ~4K
+ size_t size = 4096;
+ if (size < arena->size) {
+ size = arena->size;
+ }
+ // Trim off the excess
+ size -= size % arena->size;
+ // Double the size for every slab
+ size <<= arena->nslabs;
+
+ // Allocate the slab
+ void *slab = zalloc(arena->align, size);
+ if (!slab) {
+ return -1;
+ }
+
+ // Grow the slab array
+ void **pslab = RESERVE(void *, &arena->slabs, &arena->nslabs);
+ if (!pslab) {
+ free(slab);
+ return -1;
+ }
+
+ // Fix the last chunk->next offset
+ void *last = (char *)slab + size - arena->size;
+ chunk_set_next(arena, last, arena->chunks);
+
+ // We can rely on zero-initialized slabs, but others shouldn't
+ sanitize_uninit(slab, size);
+
+ arena->chunks = *pslab = slab;
+ return 0;
+}
+
+void *arena_alloc(struct arena *arena) {
+ if (!arena->chunks && slab_alloc(arena) != 0) {
+ return NULL;
+ }
+
+ union chunk *chunk = arena->chunks;
+ sanitize_alloc(chunk, arena->size);
+
+ sanitize_init(chunk);
+ arena->chunks = chunk_next(arena, chunk);
+ sanitize_uninit(chunk, arena->size);
+
+ return chunk;
+}
+
+void arena_free(struct arena *arena, void *ptr) {
+ union chunk *chunk = ptr;
+ chunk_set_next(arena, chunk, arena->chunks);
+ arena->chunks = chunk;
+ sanitize_uninit(chunk, arena->size);
+ sanitize_free(chunk, arena->size);
+}
+
+void arena_clear(struct arena *arena) {
+ for (size_t i = 0; i < arena->nslabs; ++i) {
+ free(arena->slabs[i]);
+ }
+ free(arena->slabs);
+
+ arena->chunks = NULL;
+ arena->nslabs = 0;
+ arena->slabs = NULL;
+}
+
+void arena_destroy(struct arena *arena) {
+ arena_clear(arena);
+ sanitize_uninit(arena);
+}
+
+void varena_init(struct varena *varena, size_t align, size_t offset, size_t size) {
+ varena->align = align;
+ varena->offset = offset;
+ varena->size = size;
+ varena->narenas = 0;
+ varena->arenas = NULL;
+
+ // The smallest size class is at least as many as fit in the smallest
+ // aligned allocation size
+ size_t min_count = (flex_size(align, offset, size, 1) - offset + size - 1) / size;
+ varena->shift = bit_width(min_count - 1);
+}
+
+/** Get the size class for the given array length. */
+static size_t varena_size_class(struct varena *varena, size_t count) {
+ // Since powers of two are common array lengths, make them the
+ // (inclusive) upper bound for each size class
+ return bit_width((count - !!count) >> varena->shift);
+}
+
+/** Get the exact size of a flexible struct. */
+static size_t varena_exact_size(const struct varena *varena, size_t count) {
+ return flex_size(varena->align, varena->offset, varena->size, count);
+}
+
+/** Get the arena for the given array length. */
+static struct arena *varena_get(struct varena *varena, size_t count) {
+ size_t i = varena_size_class(varena, count);
+
+ while (i >= varena->narenas) {
+ size_t j = varena->narenas;
+ struct arena *arena = RESERVE(struct arena, &varena->arenas, &varena->narenas);
+ if (!arena) {
+ return NULL;
+ }
+
+ size_t shift = j + varena->shift;
+ size_t size = varena_exact_size(varena, (size_t)1 << shift);
+ arena_init(arena, varena->align, size);
+ }
+
+ return &varena->arenas[i];
+}
+
+void *varena_alloc(struct varena *varena, size_t count) {
+ struct arena *arena = varena_get(varena, count);
+ if (!arena) {
+ return NULL;
+ }
+
+ void *ret = arena_alloc(arena);
+ if (!ret) {
+ return NULL;
+ }
+
+ // Tell the sanitizers the exact size of the allocated struct
+ sanitize_resize(ret, arena->size, varena_exact_size(varena, count), arena->size);
+
+ return ret;
+}
+
+void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count) {
+ struct arena *new_arena = varena_get(varena, new_count);
+ struct arena *old_arena = varena_get(varena, old_count);
+ if (!new_arena) {
+ return NULL;
+ }
+
+ size_t old_size = old_arena->size;
+ size_t new_size = new_arena->size;
+
+ if (new_arena == old_arena) {
+ sanitize_resize(ptr,
+ varena_exact_size(varena, old_count),
+ varena_exact_size(varena, new_count),
+ new_size);
+ return ptr;
+ }
+
+ void *ret = arena_alloc(new_arena);
+ if (!ret) {
+ return NULL;
+ }
+
+ // Non-sanitized builds don't bother computing exact sizes, and just use
+ // the potentially-larger arena size for each size class instead. To
+ // allow the below memcpy() to work with the less-precise sizes, expand
+ // the old allocation to its full capacity.
+ sanitize_resize(ptr, varena_exact_size(varena, old_count), old_size, old_size);
+
+ size_t min_size = new_size < old_size ? new_size : old_size;
+ memcpy(ret, ptr, min_size);
+
+ arena_free(old_arena, ptr);
+
+ sanitize_resize(ret, new_size, varena_exact_size(varena, new_count), new_size);
+ return ret;
+}
+
+void *varena_grow(struct varena *varena, void *ptr, size_t *count) {
+ size_t old_count = *count;
+
+ // Round up to the limit of the current size class. If we're already at
+ // the limit, go to the next size class.
+ size_t new_shift = varena_size_class(varena, old_count + 1) + varena->shift;
+ size_t new_count = (size_t)1 << new_shift;
+
+ ptr = varena_realloc(varena, ptr, old_count, new_count);
+ if (ptr) {
+ *count = new_count;
+ }
+ return ptr;
+}
+
+void varena_free(struct varena *varena, void *ptr, size_t count) {
+ struct arena *arena = varena_get(varena, count);
+ arena_free(arena, ptr);
+}
+
+void varena_clear(struct varena *varena) {
+ for (size_t i = 0; i < varena->narenas; ++i) {
+ arena_clear(&varena->arenas[i]);
+ }
+}
+
+void varena_destroy(struct varena *varena) {
+ for (size_t i = 0; i < varena->narenas; ++i) {
+ arena_destroy(&varena->arenas[i]);
+ }
+ free(varena->arenas);
+ sanitize_uninit(varena);
+}
diff --git a/src/alloc.h b/src/alloc.h
new file mode 100644
index 0000000..1fafbab
--- /dev/null
+++ b/src/alloc.h
@@ -0,0 +1,401 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Memory allocation.
+ */
+
+#ifndef BFS_ALLOC_H
+#define BFS_ALLOC_H
+
+#include "bfs.h"
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#define IS_ALIGNED(align, size) \
+ (((size) & ((align) - 1)) == 0)
+
+/** Check if a size is properly aligned. */
+static inline bool is_aligned(size_t align, size_t size) {
+ return IS_ALIGNED(align, size);
+}
+
+#define ALIGN_FLOOR(align, size) \
+ ((size) & ~((align) - 1))
+
+/** Round down to a multiple of an alignment. */
+static inline size_t align_floor(size_t align, size_t size) {
+ return ALIGN_FLOOR(align, size);
+}
+
+#define ALIGN_CEIL(align, size) \
+ ((((size) - 1) | ((align) - 1)) + 1)
+
+/** Round up to a multiple of an alignment. */
+static inline size_t align_ceil(size_t align, size_t size) {
+ return ALIGN_CEIL(align, size);
+}
+
+/**
+ * Saturating size addition.
+ */
+static inline size_t size_add(size_t lhs, size_t rhs) {
+ size_t ret = lhs + rhs;
+ return ret >= lhs ? ret : (size_t)-1;
+}
+
+/**
+ * Saturating size multiplication.
+ */
+static inline size_t size_mul(size_t size, size_t count) {
+ size_t ret = size * count;
+ return ret / size == count ? ret : (size_t)-1;
+}
+
+/** Saturating array sizeof. */
+#define sizeof_array(type, count) \
+ size_mul(sizeof(type), count)
+
+/** Size of a struct/union field. */
+#define sizeof_member(type, member) \
+ sizeof(((type *)NULL)->member)
+
+/**
+ * @internal
+ * Our flexible struct size calculations assume that structs have the minimum
+ * trailing padding to align the type properly. A pathological ABI that adds
+ * extra padding would result in us under-allocating space for those structs,
+ * so we static_assert() that no such padding exists.
+ */
+#define ASSERT_FLEX_ABI(type, member) \
+ ASSERT_FLEX_ABI_( \
+ ALIGN_CEIL(alignof(type), offsetof(type, member)) >= sizeof(type), \
+ "Unexpected tail padding in " #type)
+
+/**
+ * @internal
+ * The contortions here allow static_assert() to be used in expressions, rather
+ * than just declarations.
+ */
+#define ASSERT_FLEX_ABI_(...) \
+ ((void)sizeof(struct { char _; static_assert(__VA_ARGS__); }))
+
+/**
+ * Saturating flexible struct size.
+ *
+ * @align
+ * Struct alignment.
+ * @offset
+ * Flexible array member offset.
+ * @size
+ * Flexible array element size.
+ * @count
+ * Flexible array element count.
+ * @return
+ * The size of the struct with count flexible array elements. Saturates
+ * to the maximum aligned value on overflow.
+ */
+static inline size_t flex_size(size_t align, size_t offset, size_t size, size_t count) {
+ size_t ret = size_mul(size, count);
+ ret = size_add(ret, offset + align - 1);
+ ret = align_floor(align, ret);
+ return ret;
+}
+
+/**
+ * Computes the size of a flexible struct.
+ *
+ * @type
+ * The type of the struct containing the flexible array.
+ * @member
+ * The name of the flexible array member.
+ * @count
+ * The length of the flexible array.
+ * @return
+ * The size of the struct with count flexible array elements. Saturates
+ * to the maximum aligned value on overflow.
+ */
+#define sizeof_flex(type, member, count) \
+ (ASSERT_FLEX_ABI(type, member), flex_size( \
+ alignof(type), offsetof(type, member), sizeof_member(type, member[0]), count))
+
+/**
+ * General memory allocator.
+ *
+ * @align
+ * The required alignment.
+ * @size
+ * The size of the allocation.
+ * @return
+ * The allocated memory, or NULL on failure.
+ */
+_malloc(free, 1)
+_aligned_alloc(1, 2)
+void *alloc(size_t align, size_t size);
+
+/**
+ * Zero-initialized memory allocator.
+ *
+ * @align
+ * The required alignment.
+ * @size
+ * The size of the allocation.
+ * @return
+ * The allocated memory, or NULL on failure.
+ */
+_malloc(free, 1)
+_aligned_alloc(1, 2)
+void *zalloc(size_t align, size_t size);
+
+/** Allocate memory for the given type. */
+#define ALLOC(type) \
+ (type *)alloc(alignof(type), sizeof(type))
+
+/** Allocate zeroed memory for the given type. */
+#define ZALLOC(type) \
+ (type *)zalloc(alignof(type), sizeof(type))
+
+/** Allocate memory for an array. */
+#define ALLOC_ARRAY(type, count) \
+ (type *)alloc(alignof(type), sizeof_array(type, count))
+
+/** Allocate zeroed memory for an array. */
+#define ZALLOC_ARRAY(type, count) \
+ (type *)zalloc(alignof(type), sizeof_array(type, count))
+
+/** Allocate memory for a flexible struct. */
+#define ALLOC_FLEX(type, member, count) \
+ (type *)alloc(alignof(type), sizeof_flex(type, member, count))
+
+/** Allocate zeroed memory for a flexible struct. */
+#define ZALLOC_FLEX(type, member, count) \
+ (type *)zalloc(alignof(type), sizeof_flex(type, member, count))
+
+/**
+ * Alignment-aware realloc().
+ *
+ * @ptr
+ * The pointer to reallocate.
+ * @align
+ * The required alignment.
+ * @old_size
+ * The previous allocation size.
+ * @new_size
+ * The new allocation size.
+ * @return
+ * The reallocated memory, or NULL on failure.
+ */
+_aligned_alloc(2, 4)
+_nodiscard
+void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size);
+
+/** Reallocate memory for an array. */
+#define REALLOC_ARRAY(type, ptr, old_count, new_count) \
+ (type *)xrealloc((ptr), alignof(type), sizeof_array(type, old_count), sizeof_array(type, new_count))
+
+/** Reallocate memory for a flexible struct. */
+#define REALLOC_FLEX(type, member, ptr, old_count, new_count) \
+ (type *)xrealloc((ptr), alignof(type), sizeof_flex(type, member, old_count), sizeof_flex(type, member, new_count))
+
+/**
+ * Reserve space for one more element in a dynamic array.
+ *
+ * @ptr
+ * The pointer to reallocate.
+ * @align
+ * The required alignment.
+ * @count
+ * The current size of the array.
+ * @return
+ * The reallocated memory, on both success *and* failure. On success,
+ * errno will be set to zero, and the returned pointer will have room
+ * for (count + 1) elements. On failure, errno will be non-zero, and
+ * ptr will returned unchanged.
+ */
+_nodiscard
+void *reserve(void *ptr, size_t align, size_t size, size_t count);
+
+/**
+ * Convenience macro to grow a dynamic array.
+ *
+ * @type
+ * The array element type.
+ * @type **ptr
+ * A pointer to the array.
+ * @size_t *count
+ * A pointer to the array's size.
+ * @return
+ * On success, a pointer to the newly reserved array element, i.e.
+ * `*ptr + *count++`. On failure, NULL is returned, and both *ptr and
+ * *count remain unchanged.
+ */
+#define RESERVE(type, ptr, count) \
+ ((*ptr) = reserve((*ptr), alignof(type), sizeof(type), (*count)), \
+ errno ? NULL : (*ptr) + (*count)++)
+
+/**
+ * An arena allocator for fixed-size types.
+ *
+ * Arena allocators are intentionally not thread safe.
+ */
+struct arena {
+ /** The list of free chunks. */
+ void *chunks;
+ /** The number of allocated slabs. */
+ size_t nslabs;
+ /** The array of slabs. */
+ void **slabs;
+ /** Chunk alignment. */
+ size_t align;
+ /** Chunk size. */
+ size_t size;
+};
+
+/**
+ * Initialize an arena for chunks of the given size and alignment.
+ */
+void arena_init(struct arena *arena, size_t align, size_t size);
+
+/**
+ * Initialize an arena for the given type.
+ */
+#define ARENA_INIT(arena, type) \
+ arena_init((arena), alignof(type), sizeof(type))
+
+/**
+ * Free an object from the arena.
+ */
+void arena_free(struct arena *arena, void *ptr);
+
+/**
+ * Allocate an object out of the arena.
+ */
+_malloc(arena_free, 2)
+void *arena_alloc(struct arena *arena);
+
+/**
+ * Free all allocations from an arena.
+ */
+void arena_clear(struct arena *arena);
+
+/**
+ * Destroy an arena, freeing all allocations.
+ */
+void arena_destroy(struct arena *arena);
+
+/**
+ * An arena allocator for flexibly-sized types.
+ */
+struct varena {
+ /** The alignment of the struct. */
+ size_t align;
+ /** The offset of the flexible array. */
+ size_t offset;
+ /** The size of the flexible array elements. */
+ size_t size;
+ /** Shift amount for the smallest size class. */
+ size_t shift;
+ /** The number of arenas of different sizes. */
+ size_t narenas;
+ /** The array of differently-sized arenas. */
+ struct arena *arenas;
+};
+
+/**
+ * Initialize a varena for a struct with the given layout.
+ *
+ * @varena
+ * The varena to initialize.
+ * @align
+ * alignof(type)
+ * @offset
+ * offsetof(type, flexible_array)
+ * @size
+ * sizeof(flexible_array[i])
+ */
+void varena_init(struct varena *varena, size_t align, size_t offset, size_t size);
+
+/**
+ * Initialize a varena for the given type and flexible array.
+ *
+ * @varena
+ * The varena to initialize.
+ * @type
+ * A struct type containing a flexible array.
+ * @member
+ * The name of the flexible array member.
+ */
+#define VARENA_INIT(varena, type, member) \
+ (ASSERT_FLEX_ABI(type, member), varena_init( \
+ varena, alignof(type), offsetof(type, member), sizeof_member(type, member[0])))
+
+/**
+ * Free an arena-allocated flexible struct.
+ *
+ * @varena
+ * The that allocated the object.
+ * @ptr
+ * The object to free.
+ * @count
+ * The length of the flexible array.
+ */
+void varena_free(struct varena *varena, void *ptr, size_t count);
+
+/**
+ * Arena-allocate a flexible struct.
+ *
+ * @varena
+ * The varena to allocate from.
+ * @count
+ * The length of the flexible array.
+ * @return
+ * The allocated struct, or NULL on failure.
+ */
+_malloc(varena_free, 2)
+void *varena_alloc(struct varena *varena, size_t count);
+
+/**
+ * Resize a flexible struct.
+ *
+ * @varena
+ * The varena to allocate from.
+ * @ptr
+ * The object to resize.
+ * @old_count
+ * The old array length.
+ * @new_count
+ * The new array length.
+ * @return
+ * The resized struct, or NULL on failure.
+ */
+_nodiscard
+void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count);
+
+/**
+ * Grow a flexible struct by an arbitrary amount.
+ *
+ * @varena
+ * The varena to allocate from.
+ * @ptr
+ * The object to resize.
+ * @count
+ * Pointer to the flexible array length.
+ * @return
+ * The resized struct, or NULL on failure.
+ */
+_nodiscard
+void *varena_grow(struct varena *varena, void *ptr, size_t *count);
+
+/**
+ * Free all allocations from a varena.
+ */
+void varena_clear(struct varena *varena);
+
+/**
+ * Destroy a varena, freeing all allocations.
+ */
+void varena_destroy(struct varena *varena);
+
+#endif // BFS_ALLOC_H
diff --git a/src/atomic.h b/src/atomic.h
new file mode 100644
index 0000000..5c2826f
--- /dev/null
+++ b/src/atomic.h
@@ -0,0 +1,118 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Shorthand for standard C atomic operations.
+ */
+
+#ifndef BFS_ATOMIC_H
+#define BFS_ATOMIC_H
+
+#include "bfs.h"
+
+#include <stdatomic.h>
+
+/**
+ * Prettier spelling of _Atomic.
+ */
+#define atomic _Atomic
+
+/**
+ * Shorthand for atomic_load_explicit().
+ *
+ * @obj
+ * A pointer to the atomic object.
+ * @order
+ * The memory ordering to use, without the memory_order_ prefix.
+ * @return
+ * The loaded value.
+ */
+#define load(obj, order) \
+ atomic_load_explicit(obj, memory_order_##order)
+
+/**
+ * Shorthand for atomic_store_explicit().
+ */
+#define store(obj, value, order) \
+ atomic_store_explicit(obj, value, memory_order_##order)
+
+/**
+ * Shorthand for atomic_exchange_explicit().
+ */
+#define exchange(obj, value, order) \
+ atomic_exchange_explicit(obj, value, memory_order_##order)
+
+/**
+ * Shorthand for atomic_compare_exchange_weak_explicit().
+ */
+#define compare_exchange_weak(obj, expected, desired, succ, fail) \
+ atomic_compare_exchange_weak_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail)
+
+/**
+ * Shorthand for atomic_compare_exchange_strong_explicit().
+ */
+#define compare_exchange_strong(obj, expected, desired, succ, fail) \
+ atomic_compare_exchange_strong_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail)
+
+/**
+ * Shorthand for atomic_fetch_add_explicit().
+ */
+#define fetch_add(obj, arg, order) \
+ atomic_fetch_add_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_sub_explicit().
+ */
+#define fetch_sub(obj, arg, order) \
+ atomic_fetch_sub_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_or_explicit().
+ */
+#define fetch_or(obj, arg, order) \
+ atomic_fetch_or_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_xor_explicit().
+ */
+#define fetch_xor(obj, arg, order) \
+ atomic_fetch_xor_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_and_explicit().
+ */
+#define fetch_and(obj, arg, order) \
+ atomic_fetch_and_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_thread_fence().
+ */
+#if __SANITIZE_THREAD__
+// TSan doesn't support fences: https://github.com/google/sanitizers/issues/1415
+# define thread_fence(obj, order) \
+ fetch_add(obj, 0, order)
+#else
+# define thread_fence(obj, order) \
+ atomic_thread_fence(memory_order_##order)
+#endif
+
+/**
+ * Shorthand for atomic_signal_fence().
+ */
+#define signal_fence(order) \
+ atomic_signal_fence(memory_order_##order)
+
+/**
+ * A hint to the CPU to relax while it spins.
+ */
+#if __has_builtin(__builtin_ia32_pause)
+# define spin_loop() __builtin_ia32_pause()
+#elif __has_builtin(__builtin_arm_yield)
+# define spin_loop() __builtin_arm_yield()
+#elif BFS_HAS_BUILTIN_RISCV_PAUSE
+# define spin_loop() __builtin_riscv_pause()
+#else
+# define spin_loop() ((void)0)
+#endif
+
+#endif // BFS_ATOMIC_H
diff --git a/src/bar.c b/src/bar.c
new file mode 100644
index 0000000..1b0691a
--- /dev/null
+++ b/src/bar.c
@@ -0,0 +1,220 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "bar.h"
+
+#include "alloc.h"
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "bit.h"
+#include "dstring.h"
+#include "sighook.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+struct bfs_bar {
+ int fd;
+ atomic unsigned int width;
+ atomic unsigned int height;
+
+ struct sighook *exit_hook;
+ struct sighook *winch_hook;
+};
+
+/** Get the terminal size, if possible. */
+static int bfs_bar_getsize(struct bfs_bar *bar) {
+ struct winsize ws;
+ if (xtcgetwinsize(bar->fd, &ws) != 0) {
+ return -1;
+ }
+
+ store(&bar->width, ws.ws_col, relaxed);
+ store(&bar->height, ws.ws_row, relaxed);
+ return 0;
+}
+
+/** Write a string to the status bar (async-signal-safe). */
+static int bfs_bar_write(struct bfs_bar *bar, const char *str, size_t len) {
+ return xwrite(bar->fd, str, len) == len ? 0 : -1;
+}
+
+/** Write a string to the status bar (async-signal-safe). */
+static int bfs_bar_puts(struct bfs_bar *bar, const char *str) {
+ return bfs_bar_write(bar, str, strlen(str));
+}
+
+/** Number of decimal digits needed for terminal sizes. */
+#define ITOA_DIGITS ((USHRT_WIDTH + 2) / 3)
+
+/** Async Signal Safe itoa(). */
+static char *ass_itoa(char *str, unsigned int n) {
+ char *end = str + ITOA_DIGITS;
+ *end = '\0';
+
+ char *c = end;
+ do {
+ *--c = '0' + (n % 10);
+ n /= 10;
+ } while (n);
+
+ size_t len = end - c;
+ memmove(str, c, len + 1);
+ return str + len;
+}
+
+/** Reset the scrollable region and hide the bar. */
+static int bfs_bar_reset(struct bfs_bar *bar) {
+ return bfs_bar_puts(bar,
+ "\0337" // DECSC: Save cursor
+ "\033[r" // DECSTBM: Reset scrollable region
+ "\0338" // DECRC: Restore cursor
+ "\033[J" // ED: Erase display from cursor to end
+ );
+}
+
+/** Hide the bar if the terminal is shorter than this. */
+#define BFS_BAR_MIN_HEIGHT 3
+
+/** Update the size of the scrollable region. */
+static int bfs_bar_resize(struct bfs_bar *bar) {
+ unsigned int height = load(&bar->height, relaxed);
+ if (height < BFS_BAR_MIN_HEIGHT) {
+ return bfs_bar_reset(bar);
+ }
+
+ static const char PREFIX[] =
+ "\033D" // IND: Line feed, possibly scrolling
+ "\033[1A" // CUU: Move cursor up 1 row
+ "\0337" // DECSC: Save cursor
+ "\033[;"; // DECSTBM: Set scrollable region
+ static const char SUFFIX[] =
+ "r" // (end of DECSTBM)
+ "\0338" // DECRC: Restore the cursor
+ "\033[J"; // ED: Erase display from cursor to end
+
+ char esc_seq[sizeof(PREFIX) + ITOA_DIGITS + sizeof(SUFFIX)];
+
+ // DECSTBM takes the height as the second argument
+ char *cur = stpcpy(esc_seq, PREFIX);
+ cur = ass_itoa(cur, height - 1);
+ cur = stpcpy(cur, SUFFIX);
+
+ return bfs_bar_write(bar, esc_seq, cur - esc_seq);
+}
+
+#ifdef SIGWINCH
+/** SIGWINCH handler. */
+static void bfs_bar_sigwinch(int sig, siginfo_t *info, void *arg) {
+ struct bfs_bar *bar = arg;
+ bfs_bar_getsize(bar);
+ bfs_bar_resize(bar);
+}
+#endif
+
+/** Signal handler for process-terminating signals. */
+static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) {
+ struct bfs_bar *bar = arg;
+ bfs_bar_reset(bar);
+}
+
+/** printf() to the status bar with a single write(). */
+_printf(2, 3)
+static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ dchar *str = dstrvprintf(format, args);
+ va_end(args);
+
+ if (!str) {
+ return -1;
+ }
+
+ int ret = bfs_bar_write(bar, str, dstrlen(str));
+ dstrfree(str);
+ return ret;
+}
+
+struct bfs_bar *bfs_bar_show(void) {
+ struct bfs_bar *bar = ALLOC(struct bfs_bar);
+ if (!bar) {
+ return NULL;
+ }
+
+ bar->fd = open_cterm(O_RDWR | O_CLOEXEC);
+ if (bar->fd < 0) {
+ goto fail;
+ }
+
+ if (bfs_bar_getsize(bar) != 0) {
+ goto fail_close;
+ }
+
+ bar->exit_hook = atsigexit(bfs_bar_sigexit, bar);
+ if (!bar->exit_hook) {
+ goto fail_close;
+ }
+
+#ifdef SIGWINCH
+ bar->winch_hook = sighook(SIGWINCH, bfs_bar_sigwinch, bar, 0);
+ if (!bar->winch_hook) {
+ goto fail_hook;
+ }
+#endif
+
+ bfs_bar_resize(bar);
+ return bar;
+
+fail_hook:
+ sigunhook(bar->exit_hook);
+fail_close:
+ close_quietly(bar->fd);
+fail:
+ free(bar);
+ return NULL;
+}
+
+unsigned int bfs_bar_width(const struct bfs_bar *bar) {
+ return load(&bar->width, relaxed);
+}
+
+int bfs_bar_update(struct bfs_bar *bar, const char *str) {
+ unsigned int height = load(&bar->height, relaxed);
+ if (height < BFS_BAR_MIN_HEIGHT) {
+ return 0;
+ }
+
+ return bfs_bar_printf(bar,
+ "\0337" // DECSC: Save cursor
+ "\033[%u;0f" // HVP: Move cursor to row, column
+ "\033[K" // EL: Erase line
+ "\033[7m" // SGR reverse video
+ "%s"
+ "\033[27m" // SGR reverse video off
+ "\0338", // DECRC: Restore cursor
+ height,
+ str
+ );
+}
+
+void bfs_bar_hide(struct bfs_bar *bar) {
+ if (!bar) {
+ return;
+ }
+
+ sigunhook(bar->winch_hook);
+ sigunhook(bar->exit_hook);
+
+ bfs_bar_reset(bar);
+
+ xclose(bar->fd);
+ free(bar);
+}
diff --git a/src/bar.h b/src/bar.h
new file mode 100644
index 0000000..ec9e590
--- /dev/null
+++ b/src/bar.h
@@ -0,0 +1,44 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A terminal status bar.
+ */
+
+#ifndef BFS_BAR_H
+#define BFS_BAR_H
+
+/** A terminal status bar. */
+struct bfs_bar;
+
+/**
+ * Create a terminal status bar. Only one status bar is supported at a time.
+ *
+ * @return
+ * A pointer to the new status bar, or NULL on failure.
+ */
+struct bfs_bar *bfs_bar_show(void);
+
+/**
+ * Get the width of the status bar.
+ */
+unsigned int bfs_bar_width(const struct bfs_bar *bar);
+
+/**
+ * Update the status bar message.
+ *
+ * @bar
+ * The status bar to update.
+ * @str
+ * The string to display.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_bar_update(struct bfs_bar *bar, const char *str);
+
+/**
+ * Hide the status bar.
+ */
+void bfs_bar_hide(struct bfs_bar *status);
+
+#endif // BFS_BAR_H
diff --git a/src/bfs.h b/src/bfs.h
new file mode 100644
index 0000000..3cee727
--- /dev/null
+++ b/src/bfs.h
@@ -0,0 +1,241 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Configuration and fundamental utilities.
+ */
+
+#ifndef BFS_H
+#define BFS_H
+
+// Standard versions
+
+/** Possible __STDC_VERSION__ values. */
+#define C95 199409L
+#define C99 199901L
+#define C11 201112L
+#define C17 201710L
+#define C23 202311L
+
+/** Possible _POSIX_C_SOURCE and _POSIX_<OPTION> values. */
+#define POSIX_1990 1
+#define POSIX_1992 2
+#define POSIX_1993 199309L
+#define POSIX_1995 199506L
+#define POSIX_2001 200112L
+#define POSIX_2008 200809L
+#define POSIX_2024 202405L
+
+// Build configuration
+
+#include "config.h"
+
+#ifndef BFS_COMMAND
+# define BFS_COMMAND "bfs"
+#endif
+
+#ifndef BFS_HOMEPAGE
+# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html"
+#endif
+
+#ifndef BFS_LINT
+# define BFS_LINT false
+#endif
+
+// This is a symbol instead of a literal so we don't have to rebuild everything
+// when the version number changes
+extern const char bfs_version[];
+
+extern const char bfs_confflags[];
+extern const char bfs_cc[];
+extern const char bfs_cppflags[];
+extern const char bfs_cflags[];
+extern const char bfs_ldflags[];
+extern const char bfs_ldlibs[];
+
+// Get __GLIBC__
+#include <assert.h>
+
+// Fundamental utilities
+
+/**
+ * Get the length of an array.
+ */
+#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__]))
+
+/**
+ * False sharing/destructive interference/largest cache line size.
+ */
+#ifdef __GCC_DESTRUCTIVE_SIZE
+# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE
+#else
+# define FALSE_SHARING_SIZE 64
+#endif
+
+/**
+ * True sharing/constructive interference/smallest cache line size.
+ */
+#ifdef __GCC_CONSTRUCTIVE_SIZE
+# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE
+#else
+# define TRUE_SHARING_SIZE 64
+#endif
+
+/**
+ * Alignment specifier that avoids false sharing.
+ */
+#define cache_align alignas(FALSE_SHARING_SIZE)
+
+// Wrappers for attributes
+
+/**
+ * Silence warnings about switch/case fall-throughs.
+ */
+#if __has_attribute(fallthrough)
+# define _fallthrough __attribute__((fallthrough))
+#else
+# define _fallthrough ((void)0)
+#endif
+
+/**
+ * Silence warnings about unused declarations.
+ */
+#if __has_attribute(unused)
+# define _maybe_unused __attribute__((unused))
+#else
+# define _maybe_unused
+#endif
+
+/**
+ * Warn if a value is unused.
+ */
+#if __has_attribute(warn_unused_result)
+# define _nodiscard __attribute__((warn_unused_result))
+#else
+# define _nodiscard
+#endif
+
+/**
+ * Hint to avoid inlining a function.
+ */
+#if __has_attribute(noinline)
+# define _noinline __attribute__((noinline))
+#else
+# define _noinline
+#endif
+
+/**
+ * Marks a non-returning function.
+ */
+#if __STDC_VERSION__ >= C23
+# define _noreturn [[noreturn]]
+#else
+# define _noreturn _Noreturn
+#endif
+
+/**
+ * Hint that a function is unlikely to be called.
+ */
+#if __has_attribute(cold)
+# define _cold _noinline __attribute__((cold))
+#else
+# define _cold _noinline
+#endif
+
+/**
+ * Adds compiler warnings for bad printf()-style function calls, if supported.
+ */
+#if __has_attribute(format)
+# define _printf(fmt, args) __attribute__((format(printf, fmt, args)))
+#else
+# define _printf(fmt, args)
+#endif
+
+/**
+ * Annotates functions that potentially modify and return format strings.
+ */
+#if __has_attribute(format_arg)
+# define _format_arg(arg) __attribute__((format_arg(arg)))
+#else
+# define _format_arg(arg)
+#endif
+
+/**
+ * Annotates allocator-like functions.
+ */
+#if __has_attribute(malloc)
+# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC
+# define _malloc(...) _nodiscard __attribute__((malloc(__VA_ARGS__)))
+# else
+# define _malloc(...) _nodiscard __attribute__((malloc))
+# endif
+#else
+# define _malloc(...) _nodiscard
+#endif
+
+/**
+ * Specifies that a function returns allocations with a given alignment.
+ */
+#if __has_attribute(alloc_align)
+# define _alloc_align(param) __attribute__((alloc_align(param)))
+#else
+# define _alloc_align(param)
+#endif
+
+/**
+ * Specifies that a function returns allocations with a given size.
+ */
+#if __has_attribute(alloc_size)
+# define _alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+# define _alloc_size(...)
+#endif
+
+/**
+ * Shorthand for _alloc_align() and _alloc_size().
+ */
+#define _aligned_alloc(align, ...) _alloc_align(align) _alloc_size(__VA_ARGS__)
+
+/**
+ * Check if function multiversioning via GNU indirect functions (ifunc) is supported.
+ *
+ * Disabled on TSan due to https://github.com/google/sanitizers/issues/342.
+ */
+#ifndef BFS_USE_TARGET_CLONES
+# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) && !__SANITIZE_THREAD__
+# define BFS_USE_TARGET_CLONES true
+# else
+# define BFS_USE_TARGET_CLONES false
+# endif
+#endif
+
+/**
+ * Apply the target_clones attribute, if available.
+ */
+#if BFS_USE_TARGET_CLONES
+# define _target_clones(...) __attribute__((target_clones(__VA_ARGS__)))
+#else
+# define _target_clones(...)
+#endif
+
+/**
+ * Mark the size of a flexible array member.
+ */
+#if __has_attribute(counted_by)
+# define _counted_by(...) __attribute__((counted_by(__VA_ARGS__)))
+#else
+# define _counted_by(...)
+#endif
+
+/**
+ * Optimization hint to not unroll a loop.
+ */
+#if BFS_HAS_PRAGMA_NOUNROLL
+# define _nounroll _Pragma("nounroll")
+#elif __GNUC__ && !__clang__
+# define _nounroll _Pragma("GCC unroll 0")
+#else
+# define _nounroll
+#endif
+
+#endif // BFS_H
diff --git a/src/bfstd.c b/src/bfstd.c
new file mode 100644
index 0000000..b78af7a
--- /dev/null
+++ b/src/bfstd.c
@@ -0,0 +1,1270 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "bfstd.h"
+
+#include "bfs.h"
+#include "bit.h"
+#include "diag.h"
+#include "sanity.h"
+#include "thread.h"
+#include "xregex.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <langinfo.h>
+#include <limits.h>
+#include <locale.h>
+#include <nl_types.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#if __has_include(<sys/sysmacros.h>)
+# include <sys/sysmacros.h>
+#elif __has_include(<sys/mkdev.h>)
+# include <sys/mkdev.h>
+#endif
+
+#if __has_include(<util.h>)
+# include <util.h>
+#endif
+
+bool error_is_like(int error, int category) {
+ if (error == category) {
+ return true;
+ }
+
+ switch (category) {
+ case ENOENT:
+ return error == ENOTDIR;
+
+ case ENOSYS:
+ // https://github.com/opencontainers/runc/issues/2151
+ return errno == EPERM;
+
+#if __DragonFly__
+ // https://twitter.com/tavianator/status/1742991411203485713
+ case ENAMETOOLONG:
+ return error == EFAULT;
+#endif
+ }
+
+ return false;
+}
+
+bool errno_is_like(int category) {
+ return error_is_like(errno, category);
+}
+
+int try(int ret) {
+ if (ret >= 0) {
+ return ret;
+ } else {
+ bfs_assert(errno > 0, "errno should be positive, was %d\n", errno);
+ return -errno;
+ }
+}
+
+char *xdirname(const char *path) {
+ size_t i = xbaseoff(path);
+
+ // Skip trailing slashes
+ while (i > 0 && path[i - 1] == '/') {
+ --i;
+ }
+
+ if (i > 0) {
+ return strndup(path, i);
+ } else if (path[i] == '/') {
+ return strdup("/");
+ } else {
+ return strdup(".");
+ }
+}
+
+char *xbasename(const char *path) {
+ size_t i = xbaseoff(path);
+ size_t len = strcspn(path + i, "/");
+ if (len > 0) {
+ return strndup(path + i, len);
+ } else if (path[i] == '/') {
+ return strdup("/");
+ } else {
+ return strdup(".");
+ }
+}
+
+size_t xbaseoff(const char *path) {
+ size_t i = strlen(path);
+
+ // Skip trailing slashes
+ while (i > 0 && path[i - 1] == '/') {
+ --i;
+ }
+
+ // Find the beginning of the name
+ while (i > 0 && path[i - 1] != '/') {
+ --i;
+ }
+
+ // Skip leading slashes
+ while (path[i] == '/' && path[i + 1]) {
+ ++i;
+ }
+
+ return i;
+}
+
+FILE *xfopen(const char *path, int flags) {
+ char mode[4];
+
+ switch (flags & O_ACCMODE) {
+ case O_RDONLY:
+ strcpy(mode, "rb");
+ break;
+ case O_WRONLY:
+ strcpy(mode, "wb");
+ break;
+ case O_RDWR:
+ strcpy(mode, "r+b");
+ break;
+ default:
+ bfs_bug("Invalid access mode");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (flags & O_APPEND) {
+ mode[0] = 'a';
+ }
+
+ int fd;
+ if (flags & O_CREAT) {
+ fd = open(path, flags, 0666);
+ } else {
+ fd = open(path, flags);
+ }
+
+ if (fd < 0) {
+ return NULL;
+ }
+
+ FILE *ret = fdopen(fd, mode);
+ if (!ret) {
+ close_quietly(fd);
+ return NULL;
+ }
+
+ return ret;
+}
+
+char *xgetdelim(FILE *file, char delim) {
+ char *chunk = NULL;
+ size_t n = 0;
+ ssize_t len = getdelim(&chunk, &n, delim, file);
+ if (len >= 0) {
+ if (chunk[len] == delim) {
+ chunk[len] = '\0';
+ }
+ return chunk;
+ } else {
+ free(chunk);
+ if (!ferror(file)) {
+ errno = 0;
+ }
+ return NULL;
+ }
+}
+
+const char *xgetprogname(void) {
+ const char *cmd = NULL;
+#if BFS_HAS_GETPROGNAME
+ cmd = getprogname();
+#elif BFS_HAS_GETPROGNAME_GNU
+ cmd = program_invocation_short_name;
+#endif
+
+ if (!cmd) {
+ cmd = BFS_COMMAND;
+ }
+
+ return cmd;
+}
+
+/** Common prologue for xstrto*() wrappers. */
+static int xstrtox_prologue(const char *str) {
+ // strto*() skips leading spaces, but we want to reject them
+ if (xisspace(str[0])) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ errno = 0;
+ return 0;
+}
+
+/** Common epilogue for xstrto*() wrappers. */
+static int xstrtox_epilogue(const char *str, char **end, char *endp) {
+ if (errno != 0) {
+ return -1;
+ }
+
+ if (end) {
+ *end = endp;
+ }
+
+ // If end is NULL, make sure the entire string is valid
+ if (endp == str || (!end && *endp != '\0')) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ return 0;
+}
+
+int xstrtos(const char *str, char **end, int base, short *value) {
+ long n;
+ if (xstrtol(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n < SHRT_MIN || n > SHRT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+int xstrtoi(const char *str, char **end, int base, int *value) {
+ long n;
+ if (xstrtol(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n < INT_MIN || n > INT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+int xstrtol(const char *str, char **end, int base, long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtol(str, &endp, base);
+ return xstrtox_epilogue(str, end, endp);
+}
+
+int xstrtoll(const char *str, char **end, int base, long long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtoll(str, &endp, base);
+ return xstrtox_epilogue(str, end, endp);
+}
+
+int xstrtof(const char *str, char **end, float *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtof(str, &endp);
+ return xstrtox_epilogue(str, end, endp);
+}
+
+int xstrtod(const char *str, char **end, double *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtod(str, &endp);
+ return xstrtox_epilogue(str, end, endp);
+}
+
+int xstrtous(const char *str, char **end, int base, unsigned short *value) {
+ unsigned long n;
+ if (xstrtoul(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n > USHRT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+int xstrtoui(const char *str, char **end, int base, unsigned int *value) {
+ unsigned long n;
+ if (xstrtoul(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n > UINT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+/** Common epilogue for xstrtou*() wrappers. */
+static int xstrtoux_epilogue(const char *str, char **end, char *endp) {
+ if (xstrtox_epilogue(str, end, endp) != 0) {
+ return -1;
+ }
+
+ if (str[0] == '-') {
+ errno = ERANGE;
+ return -1;
+ }
+
+ return 0;
+}
+
+int xstrtoul(const char *str, char **end, int base, unsigned long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtoul(str, &endp, base);
+ return xstrtoux_epilogue(str, end, endp);
+}
+
+int xstrtoull(const char *str, char **end, int base, unsigned long long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtoull(str, &endp, base);
+ return xstrtoux_epilogue(str, end, endp);
+}
+
+/** Compile and execute a regular expression for xrpmatch(). */
+static int xrpregex(nl_item item, const char *response) {
+ const char *pattern = nl_langinfo(item);
+ if (!pattern) {
+ return -1;
+ }
+
+ struct bfs_regex *regex;
+ int ret = bfs_regcomp(&regex, pattern, BFS_REGEX_POSIX_EXTENDED, 0);
+ if (ret == 0) {
+ ret = bfs_regexec(regex, response, 0);
+ }
+
+ bfs_regfree(regex);
+ return ret;
+}
+
+/** Check if a response is affirmative or negative. */
+static int xrpmatch(const char *response) {
+ int ret = xrpregex(NOEXPR, response);
+ if (ret > 0) {
+ return 0;
+ } else if (ret < 0) {
+ return -1;
+ }
+
+ ret = xrpregex(YESEXPR, response);
+ if (ret > 0) {
+ return 1;
+ } else if (ret < 0) {
+ return -1;
+ }
+
+ // Failsafe: always handle y/n
+ char c = response[0];
+ if (c == 'n' || c == 'N') {
+ return 0;
+ } else if (c == 'y' || c == 'Y') {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+int ynprompt(void) {
+ fflush(stderr);
+
+ char *line = xgetdelim(stdin, '\n');
+ int ret = line ? xrpmatch(line) : -1;
+ free(line);
+ return ret;
+}
+
+void *xmemdup(const void *src, size_t size) {
+ void *ret = malloc(size);
+ if (ret) {
+ memcpy(ret, src, size);
+ }
+ return ret;
+}
+
+char *xstpecpy(char *dest, char *end, const char *src) {
+ return xstpencpy(dest, end, src, SIZE_MAX);
+}
+
+char *xstpencpy(char *dest, char *end, const char *src, size_t n) {
+ size_t space = end - dest;
+ n = space < n ? space : n;
+ n = strnlen(src, n);
+ memcpy(dest, src, n);
+ if (n < space) {
+ dest[n] = '\0';
+ return dest + n;
+ } else {
+ end[-1] = '\0';
+ return end;
+ }
+}
+
+const char *xstrerror(int errnum) {
+ int saved = errno;
+ const char *ret = NULL;
+ static thread_local char buf[256];
+
+ // On FreeBSD with MemorySanitizer, duplocale() triggers
+ // https://github.com/llvm/llvm-project/issues/65532
+#if BFS_HAS_STRERROR_L && !(__FreeBSD__ && __SANITIZE_MEMORY__)
+# if BFS_HAS_USELOCALE
+ locale_t loc = uselocale((locale_t)0);
+# else
+ locale_t loc = LC_GLOBAL_LOCALE;
+# endif
+
+ bool free_loc = false;
+ if (loc == LC_GLOBAL_LOCALE) {
+ loc = duplocale(loc);
+ free_loc = true;
+ }
+
+ if (loc != (locale_t)0) {
+ ret = strerror_l(errnum, loc);
+ if (free_loc) {
+ freelocale(loc);
+ }
+ }
+#elif BFS_HAS_STRERROR_R_POSIX
+ if (strerror_r(errnum, buf, sizeof(buf)) == 0) {
+ ret = buf;
+ }
+#elif BFS_HAS_STRERROR_R_GNU
+ ret = strerror_r(errnum, buf, sizeof(buf));
+#endif
+
+ if (!ret) {
+ // Fallback for strerror_[lr]() or duplocale() failures
+ snprintf(buf, sizeof(buf), "Unknown error %d", errnum);
+ ret = buf;
+ }
+
+ errno = saved;
+ return ret;
+}
+
+const char *errstr(void) {
+ return xstrerror(errno);
+}
+
+/** Get the single character describing the given file type. */
+static char type_char(mode_t mode) {
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ return '-';
+ case S_IFBLK:
+ return 'b';
+ case S_IFCHR:
+ return 'c';
+ case S_IFDIR:
+ return 'd';
+ case S_IFLNK:
+ return 'l';
+ case S_IFIFO:
+ return 'p';
+ case S_IFSOCK:
+ return 's';
+#ifdef S_IFDOOR
+ case S_IFDOOR:
+ return 'D';
+#endif
+#ifdef S_IFPORT
+ case S_IFPORT:
+ return 'P';
+#endif
+#ifdef S_IFWHT
+ case S_IFWHT:
+ return 'w';
+#endif
+ }
+
+ return '?';
+}
+
+void xstrmode(mode_t mode, char str[11]) {
+ strcpy(str, "----------");
+
+ str[0] = type_char(mode);
+
+ if (mode & 00400) {
+ str[1] = 'r';
+ }
+ if (mode & 00200) {
+ str[2] = 'w';
+ }
+ if ((mode & 04100) == 04000) {
+ str[3] = 'S';
+ } else if (mode & 04000) {
+ str[3] = 's';
+ } else if (mode & 00100) {
+ str[3] = 'x';
+ }
+
+ if (mode & 00040) {
+ str[4] = 'r';
+ }
+ if (mode & 00020) {
+ str[5] = 'w';
+ }
+ if ((mode & 02010) == 02000) {
+ str[6] = 'S';
+ } else if (mode & 02000) {
+ str[6] = 's';
+ } else if (mode & 00010) {
+ str[6] = 'x';
+ }
+
+ if (mode & 00004) {
+ str[7] = 'r';
+ }
+ if (mode & 00002) {
+ str[8] = 'w';
+ }
+ if ((mode & 01001) == 01000) {
+ str[9] = 'T';
+ } else if (mode & 01000) {
+ str[9] = 't';
+ } else if (mode & 00001) {
+ str[9] = 'x';
+ }
+}
+
+/** Check if an rlimit value is infinite. */
+static bool rlim_isinf(rlim_t r) {
+ // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite
+ if (r == RLIM_INFINITY) {
+ return true;
+ }
+
+#ifdef RLIM_SAVED_CUR
+ if (r == RLIM_SAVED_CUR) {
+ return true;
+ }
+#endif
+
+#ifdef RLIM_SAVED_MAX
+ if (r == RLIM_SAVED_MAX) {
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+int rlim_cmp(rlim_t a, rlim_t b) {
+ bool a_inf = rlim_isinf(a);
+ bool b_inf = rlim_isinf(b);
+ if (a_inf || b_inf) {
+ return a_inf - b_inf;
+ }
+
+ return (a > b) - (a < b);
+}
+
+dev_t xmakedev(int ma, int mi) {
+#if __QNX__
+ return makedev(0, ma, mi);
+#elif defined(makedev)
+ return makedev(ma, mi);
+#else
+ return (ma << 8) | mi;
+#endif
+}
+
+int xmajor(dev_t dev) {
+#ifdef major
+ return major(dev);
+#else
+ return dev >> 8;
+#endif
+}
+
+int xminor(dev_t dev) {
+#ifdef minor
+ return minor(dev);
+#else
+ return dev & 0xFF;
+#endif
+}
+
+pid_t xwaitpid(pid_t pid, int *status, int flags) {
+ pid_t ret;
+ do {
+ ret = waitpid(pid, status, flags);
+ } while (ret < 0 && errno == EINTR);
+ return ret;
+}
+
+int open_cterm(int flags) {
+ char path[L_ctermid];
+ if (ctermid(path) == NULL || strlen(path) == 0) {
+ errno = ENOTTY;
+ return -1;
+ }
+
+ return open(path, flags);
+}
+
+int xtcgetwinsize(int fd, struct winsize *ws) {
+#if BFS_HAS_TCGETWINSIZE
+ return tcgetwinsize(fd, ws);
+#else
+ return ioctl(fd, TIOCGWINSZ, ws);
+#endif
+}
+
+int xtcsetwinsize(int fd, const struct winsize *ws) {
+#if BFS_HAS_TCSETWINSIZE
+ return tcsetwinsize(fd, ws);
+#else
+ return ioctl(fd, TIOCSWINSZ, ws);
+#endif
+}
+
+int dup_cloexec(int fd) {
+#ifdef F_DUPFD_CLOEXEC
+ return fcntl(fd, F_DUPFD_CLOEXEC, 0);
+#else
+ int ret = dup(fd);
+ if (ret < 0) {
+ return -1;
+ }
+
+ if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) {
+ close_quietly(ret);
+ return -1;
+ }
+
+ return ret;
+#endif
+}
+
+int pipe_cloexec(int pipefd[2]) {
+#if BFS_HAS_PIPE2
+ return pipe2(pipefd, O_CLOEXEC);
+#else
+ if (pipe(pipefd) != 0) {
+ return -1;
+ }
+
+ if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) {
+ close_quietly(pipefd[1]);
+ close_quietly(pipefd[0]);
+ return -1;
+ }
+
+ return 0;
+#endif
+}
+
+size_t xread(int fd, void *buf, size_t nbytes) {
+ size_t count = 0;
+
+ while (count < nbytes) {
+ ssize_t ret = read(fd, (char *)buf + count, nbytes - count);
+ if (ret < 0) {
+ if (errno == EINTR) {
+ continue;
+ } else {
+ break;
+ }
+ } else if (ret == 0) {
+ // EOF
+ errno = 0;
+ break;
+ } else {
+ count += ret;
+ }
+ }
+
+ return count;
+}
+
+size_t xwrite(int fd, const void *buf, size_t nbytes) {
+ size_t count = 0;
+
+ while (count < nbytes) {
+ ssize_t ret = write(fd, (const char *)buf + count, nbytes - count);
+ if (ret < 0) {
+ if (errno == EINTR) {
+ continue;
+ } else {
+ break;
+ }
+ } else if (ret == 0) {
+ // EOF?
+ errno = 0;
+ break;
+ } else {
+ count += ret;
+ }
+ }
+
+ return count;
+}
+
+void close_quietly(int fd) {
+ int error = errno;
+ xclose(fd);
+ errno = error;
+}
+
+int xclose(int fd) {
+ int ret = close(fd);
+ if (ret != 0) {
+ bfs_verify(errno != EBADF);
+ }
+ return ret;
+}
+
+int xfaccessat(int fd, const char *path, int amode) {
+ int ret = faccessat(fd, path, amode, 0);
+
+#ifdef AT_EACCESS
+ // Some platforms, like Hurd, only support AT_EACCESS. Other platforms,
+ // like Android, don't support AT_EACCESS at all.
+ if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) {
+ ret = faccessat(fd, path, amode, AT_EACCESS);
+ }
+#endif
+
+ return ret;
+}
+
+char *xconfstr(int name) {
+#if BFS_HAS_CONFSTR
+ size_t len = confstr(name, NULL, 0);
+ if (len == 0) {
+ return NULL;
+ }
+
+ char *str = malloc(len);
+ if (!str) {
+ return NULL;
+ }
+
+ if (confstr(name, str, len) != len) {
+ free(str);
+ return NULL;
+ }
+
+ return str;
+#else
+ errno = ENOTSUP;
+ return NULL;
+#endif
+}
+
+char *xreadlinkat(int fd, const char *path, size_t size) {
+ ssize_t len;
+ char *name = NULL;
+
+ if (size == 0) {
+ size = 64;
+ } else {
+ ++size; // NUL terminator
+ }
+
+ while (true) {
+ char *new_name = realloc(name, size);
+ if (!new_name) {
+ goto error;
+ }
+ name = new_name;
+
+ len = readlinkat(fd, path, name, size);
+ if (len < 0) {
+ goto error;
+ } else if ((size_t)len >= size) {
+ size *= 2;
+ } else {
+ break;
+ }
+ }
+
+ name[len] = '\0';
+ return name;
+
+error:
+ free(name);
+ return NULL;
+}
+
+#if BFS_HAS_STRTOFFLAGS
+# define BFS_STRTOFFLAGS strtofflags
+#elif BFS_HAS_STRING_TO_FLAGS
+# define BFS_STRTOFFLAGS string_to_flags
+#endif
+
+int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
+#ifdef BFS_STRTOFFLAGS
+ char *str_arg = (char *)*str;
+
+#if __OpenBSD__
+ typedef uint32_t bfs_fflags_t;
+#else
+ typedef unsigned long bfs_fflags_t;
+#endif
+ bfs_fflags_t set_arg = 0;
+ bfs_fflags_t clear_arg = 0;
+
+ int ret = BFS_STRTOFFLAGS(&str_arg, &set_arg, &clear_arg);
+
+ *str = str_arg;
+ *set = set_arg;
+ *clear = clear_arg;
+
+ if (ret != 0) {
+ errno = EINVAL;
+ }
+ return ret;
+#else // !BFS_STRTOFFLAGS
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+long xsysconf(int name) {
+#if __FreeBSD__ && __SANITIZE_MEMORY__
+ // Work around https://github.com/llvm/llvm-project/issues/88163
+ __msan_scoped_disable_interceptor_checks();
+#endif
+
+ long ret = sysconf(name);
+
+#if __FreeBSD__ && __SANITIZE_MEMORY__
+ __msan_scoped_enable_interceptor_checks();
+#endif
+
+ return ret;
+}
+
+#if BFS_HAS_SCHED_GETAFFINITY
+/** Get the CPU count in an affinity mask of the given size. */
+static long bfs_sched_getaffinity(size_t size) {
+ cpu_set_t set, *pset = &set;
+
+ if (size > sizeof(set)) {
+ pset = malloc(size);
+ if (!pset) {
+ return -1;
+ }
+ }
+
+ long ret = -1;
+ if (sched_getaffinity(0, size, pset) == 0) {
+# ifdef CPU_COUNT_S
+ ret = CPU_COUNT_S(size, pset);
+# else
+ bfs_assert(size <= sizeof(set));
+ ret = CPU_COUNT(pset);
+# endif
+ }
+
+ if (pset != &set) {
+ free(pset);
+ }
+ return ret;
+}
+#endif
+
+long nproc(void) {
+ long ret = 0;
+
+#if BFS_HAS_SCHED_GETAFFINITY
+ size_t size = sizeof(cpu_set_t);
+ do {
+ ret = bfs_sched_getaffinity(size);
+
+# ifdef CPU_COUNT_S
+ // On Linux, sched_getaffinity(2) says:
+ //
+ // When working on systems with large kernel CPU affinity masks, one must
+ // dynamically allocate the mask argument (see CPU_ALLOC(3)). Currently,
+ // the only way to do this is by probing for the size of the required mask
+ // using sched_getaffinity() calls with increasing mask sizes (until the
+ // call does not fail with the error EINVAL).
+ size *= 2;
+# else
+ // No support for dynamically-sized CPU masks
+ break;
+# endif
+ } while (ret < 0 && errno == EINVAL);
+#endif
+
+ if (ret < 1) {
+ ret = xsysconf(_SC_NPROCESSORS_ONLN);
+ }
+
+ if (ret < 1) {
+ ret = 1;
+ }
+
+ return ret;
+}
+
+size_t asciilen(const char *str) {
+ return asciinlen(str, strlen(str));
+}
+
+size_t asciinlen(const char *str, size_t n) {
+ const unsigned char *ustr = (const unsigned char *)str;
+ size_t i = 0;
+
+ // Word-at-a-time isascii()
+#define CHUNK(n) CHUNK_(uint##n##_t, load8_leu##n)
+#define CHUNK_(type, load8) \
+ (n - i >= sizeof(type)) { \
+ type word = load8(ustr + i); \
+ type mask = (((type)-1) / 0xFF) << 7; /* 0x808080.. */ \
+ word &= mask; \
+ i += trailing_zeros(word) / 8; \
+ if (word) { \
+ return i; \
+ } \
+ }
+
+#if SIZE_WIDTH >= 64
+ while CHUNK(64);
+ if CHUNK(32);
+#else
+ while CHUNK(32);
+#endif
+ if CHUNK(16);
+ if CHUNK(8);
+
+#undef CHUNK_
+#undef CHUNK
+
+ return i;
+}
+
+wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb) {
+ wchar_t wc;
+ size_t mblen = mbrtowc(&wc, str + *i, len - *i, mb);
+ switch (mblen) {
+ case -1: // Invalid byte sequence
+ case -2: // Incomplete byte sequence
+ *i += 1;
+ *mb = (mbstate_t){0};
+ return WEOF;
+ default:
+ *i += mblen;
+ return wc;
+ }
+}
+
+size_t xstrwidth(const char *str) {
+ size_t len = strlen(str);
+ size_t ret = 0;
+
+ size_t asclen = asciinlen(str, len);
+ size_t i;
+ for (i = 0; i < asclen; ++i) {
+ // Assume all ASCII printables have width 1
+ if (xisprint(str[i])) {
+ ++ret;
+ }
+ }
+
+ mbstate_t mb = {0};
+ while (i < len) {
+ wint_t wc = xmbrtowc(str, &i, len, &mb);
+ if (wc == WEOF) {
+ // Assume a single-width '?'
+ ++ret;
+ continue;
+ }
+
+ int width = xwcwidth(wc);
+ if (width > 0) {
+ ret += width;
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * Character type flags.
+ */
+enum ctype {
+ IS_PRINT = 1 << 0,
+ IS_SPACE = 1 << 1,
+};
+
+/** Cached ctypes. */
+static unsigned char ctype_cache[UCHAR_MAX + 1];
+
+/** Initialize the ctype cache. */
+static void char_cache_init(void) {
+ for (size_t c = 0; c <= UCHAR_MAX; ++c) {
+ if (xisprint(c)) {
+ ctype_cache[c] |= IS_PRINT;
+ }
+ if (xisspace(c)) {
+ ctype_cache[c] |= IS_SPACE;
+ }
+ }
+}
+
+/** Check if a character is printable. */
+static bool wesc_isprint(unsigned char c, enum wesc_flags flags) {
+ if (ctype_cache[c] & IS_PRINT) {
+ return true;
+ }
+
+ // Technically a literal newline is safe inside single quotes, but $'\n'
+ // is much nicer than '
+ // '
+ if (!(flags & WESC_SHELL) && (ctype_cache[c] & IS_SPACE)) {
+ return true;
+ }
+
+ return false;
+}
+
+/** Check if a wide character is printable. */
+static bool wesc_iswprint(wchar_t c, enum wesc_flags flags) {
+ if (xiswprint(c)) {
+ return true;
+ }
+
+ if (!(flags & WESC_SHELL) && xiswspace(c)) {
+ return true;
+ }
+
+ return false;
+}
+
+/** Get the length of the longest printable prefix of a string. */
+static size_t printable_len(const char *str, size_t len, enum wesc_flags flags) {
+ static pthread_once_t once = PTHREAD_ONCE_INIT;
+ invoke_once(&once, char_cache_init);
+
+ // Fast path: avoid multibyte checks
+ size_t asclen = asciinlen(str, len);
+ size_t i;
+ for (i = 0; i < asclen; ++i) {
+ if (!wesc_isprint(str[i], flags)) {
+ return i;
+ }
+ }
+
+ mbstate_t mb = {0};
+ for (size_t j = i; i < len; i = j) {
+ wint_t wc = xmbrtowc(str, &j, len, &mb);
+ if (wc == WEOF) {
+ break;
+ }
+ if (!wesc_iswprint(wc, flags)) {
+ break;
+ }
+ }
+
+ return i;
+}
+
+/** Convert a special char into a well-known escape sequence like "\n". */
+static const char *dollar_esc(char c) {
+ // https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
+ switch (c) {
+ case '\a':
+ return "\\a";
+ case '\b':
+ return "\\b";
+ case '\033':
+ return "\\e";
+ case '\f':
+ return "\\f";
+ case '\n':
+ return "\\n";
+ case '\r':
+ return "\\r";
+ case '\t':
+ return "\\t";
+ case '\v':
+ return "\\v";
+ case '\'':
+ return "\\'";
+ case '\\':
+ return "\\\\";
+ default:
+ return NULL;
+ }
+}
+
+/** $'Quote' a string for the shell. */
+static char *dollar_quote(char *dest, char *end, const char *str, size_t len, enum wesc_flags flags) {
+ dest = xstpecpy(dest, end, "$'");
+
+ mbstate_t mb = {0};
+ for (size_t i = 0; i < len;) {
+ size_t start = i;
+ bool safe = false;
+
+ wint_t wc = xmbrtowc(str, &i, len, &mb);
+ if (wc != WEOF) {
+ safe = wesc_iswprint(wc, flags);
+ }
+
+ for (size_t j = start; safe && j < i; ++j) {
+ if (str[j] == '\'' || str[j] == '\\') {
+ safe = false;
+ }
+ }
+
+ if (safe) {
+ dest = xstpencpy(dest, end, str + start, i - start);
+ } else {
+ for (size_t j = start; j < i; ++j) {
+ unsigned char byte = str[j];
+ const char *esc = dollar_esc(byte);
+ if (esc) {
+ dest = xstpecpy(dest, end, esc);
+ } else {
+ static const char *hex[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};
+ dest = xstpecpy(dest, end, "\\x");
+ dest = xstpecpy(dest, end, hex[byte / 0x10]);
+ dest = xstpecpy(dest, end, hex[byte % 0x10]);
+ }
+ }
+ }
+ }
+
+ return xstpecpy(dest, end, "'");
+}
+
+/** How much of this string is safe as a bare word? */
+static size_t bare_len(const char *str, size_t len) {
+ // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02
+ size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}");
+ return ret < len ? ret : len;
+}
+
+/** How much of this string is safe to double-quote? */
+static size_t quotable_len(const char *str, size_t len) {
+ // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_03
+ size_t ret = strcspn(str, "`$\\\"!");
+ return ret < len ? ret : len;
+}
+
+/** "Quote" a string for the shell. */
+static char *double_quote(char *dest, char *end, const char *str, size_t len) {
+ dest = xstpecpy(dest, end, "\"");
+ dest = xstpencpy(dest, end, str, len);
+ return xstpecpy(dest, end, "\"");
+}
+
+/** 'Quote' a string for the shell. */
+static char *single_quote(char *dest, char *end, const char *str, size_t len) {
+ bool open = false;
+
+ while (len > 0) {
+ size_t chunk = strcspn(str, "'");
+ chunk = chunk < len ? chunk : len;
+ if (chunk > 0) {
+ if (!open) {
+ dest = xstpecpy(dest, end, "'");
+ open = true;
+ }
+ dest = xstpencpy(dest, end, str, chunk);
+ str += chunk;
+ len -= chunk;
+ }
+
+ while (len > 0 && *str == '\'') {
+ if (open) {
+ dest = xstpecpy(dest, end, "'");
+ open = false;
+ }
+ dest = xstpecpy(dest, end, "\\'");
+ ++str;
+ --len;
+ }
+ }
+
+ if (open) {
+ dest = xstpecpy(dest, end, "'");
+ }
+
+ return dest;
+}
+
+char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags) {
+ return wordnesc(dest, end, str, SIZE_MAX, flags);
+}
+
+char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags) {
+ size_t len = strnlen(str, n);
+ char *start = dest;
+
+ if (printable_len(str, len, flags) < len) {
+ // String contains unprintable chars, use $'this\x7Fsyntax'
+ dest = dollar_quote(dest, end, str, len, flags);
+ } else if (!(flags & WESC_SHELL) || bare_len(str, len) == len) {
+ // Whole string is safe as a bare word
+ dest = xstpencpy(dest, end, str, len);
+ } else if (quotable_len(str, len) == len) {
+ // Whole string is safe to double-quote
+ dest = double_quote(dest, end, str, len);
+ } else {
+ // Single-quote the whole string
+ dest = single_quote(dest, end, str, len);
+ }
+
+ if (dest == start) {
+ dest = xstpecpy(dest, end, "\"\"");
+ }
+
+ return dest;
+}
diff --git a/src/bfstd.h b/src/bfstd.h
new file mode 100644
index 0000000..15dd949
--- /dev/null
+++ b/src/bfstd.h
@@ -0,0 +1,619 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Standard library wrappers and polyfills.
+ */
+
+#ifndef BFS_BFSTD_H
+#define BFS_BFSTD_H
+
+#include "bfs.h"
+
+#include <stddef.h>
+
+#include <ctype.h>
+
+/**
+ * Work around https://github.com/llvm/llvm-project/issues/65532 by forcing a
+ * function, not a macro, to be called.
+ */
+#if __FreeBSD__ && __SANITIZE_MEMORY__
+# define BFS_INTERCEPT(fn) (fn)
+#else
+# define BFS_INTERCEPT(fn) fn
+#endif
+
+/**
+ * Wrap isalpha()/isdigit()/etc.
+ */
+#define BFS_ISCTYPE(fn, c) BFS_INTERCEPT(fn)((unsigned char)(c))
+
+#define xisalnum(c) BFS_ISCTYPE(isalnum, c)
+#define xisalpha(c) BFS_ISCTYPE(isalpha, c)
+#define xisascii(c) BFS_ISCTYPE(isascii, c)
+#define xiscntrl(c) BFS_ISCTYPE(iscntrl, c)
+#define xisdigit(c) BFS_ISCTYPE(isdigit, c)
+#define xislower(c) BFS_ISCTYPE(islower, c)
+#define xisgraph(c) BFS_ISCTYPE(isgraph, c)
+#define xisprint(c) BFS_ISCTYPE(isprint, c)
+#define xispunct(c) BFS_ISCTYPE(ispunct, c)
+#define xisspace(c) BFS_ISCTYPE(isspace, c)
+#define xisupper(c) BFS_ISCTYPE(isupper, c)
+#define xisxdigit(c) BFS_ISCTYPE(isxdigit, c)
+
+// #include <errno.h>
+
+/**
+ * Check if an error code is "like" another one. For example, ENOTDIR is
+ * like ENOENT because they can both be triggered by non-existent paths.
+ *
+ * @error
+ * The error code to check.
+ * @category
+ * The category to test for. Known categories include ENOENT and
+ * ENAMETOOLONG.
+ * @return
+ * Whether the error belongs to the given category.
+ */
+bool error_is_like(int error, int category);
+
+/**
+ * Equivalent to error_is_like(errno, category).
+ */
+bool errno_is_like(int category);
+
+/**
+ * Apply the "negative errno" convention.
+ *
+ * @ret
+ * The return value of the attempted operation.
+ * @return
+ * ret, if non-negative, otherwise -errno.
+ */
+int try(int ret);
+
+#include <fcntl.h>
+
+#ifndef O_EXEC
+# ifdef O_PATH
+# define O_EXEC O_PATH
+# else
+# define O_EXEC O_RDONLY
+# endif
+#endif
+
+#ifndef O_SEARCH
+# ifdef O_PATH
+# define O_SEARCH O_PATH
+# else
+# define O_SEARCH O_RDONLY
+# endif
+#endif
+
+#ifndef O_DIRECTORY
+# define O_DIRECTORY 0
+#endif
+
+#include <fnmatch.h>
+
+#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE)
+# define FNM_CASEFOLD FNM_IGNORECASE
+#endif
+
+// #include <libgen.h>
+
+/**
+ * Re-entrant dirname() variant that always allocates a copy.
+ *
+ * @path
+ * The path in question.
+ * @return
+ * The parent directory of the path.
+ */
+char *xdirname(const char *path);
+
+/**
+ * Re-entrant basename() variant that always allocates a copy.
+ *
+ * @path
+ * The path in question.
+ * @return
+ * The final component of the path.
+ */
+char *xbasename(const char *path);
+
+/**
+ * Find the offset of the final component of a path.
+ *
+ * @path
+ * The path in question.
+ * @return
+ * The offset of the basename.
+ */
+size_t xbaseoff(const char *path);
+
+#include <stdio.h>
+
+/**
+ * fopen() variant that takes open() style flags.
+ *
+ * @path
+ * The path to open.
+ * @flags
+ * Flags to pass to open().
+ */
+FILE *xfopen(const char *path, int flags);
+
+/**
+ * Convenience wrapper for getdelim().
+ *
+ * @file
+ * The file to read.
+ * @delim
+ * The delimiter character to split on.
+ * @return
+ * The read chunk (without the delimiter), allocated with malloc().
+ * NULL is returned on error (errno != 0) or end of file (errno == 0).
+ */
+char *xgetdelim(FILE *file, char delim);
+
+// #include <stdlib.h>
+
+/**
+ * Wrapper for getprogname() or equivalent functionality.
+ *
+ * @return
+ * The basename of the currently running program.
+ */
+const char *xgetprogname(void);
+
+/**
+ * Like xstrtol(), but for short.
+ */
+int xstrtos(const char *str, char **end, int base, short *value);
+
+/**
+ * Like xstrtol(), but for int.
+ */
+int xstrtoi(const char *str, char **end, int base, int *value);
+
+/**
+ * Wrapper for strtol() that forbids leading spaces.
+ */
+int xstrtol(const char *str, char **end, int base, long *value);
+
+/**
+ * Wrapper for strtoll() that forbids leading spaces.
+ */
+int xstrtoll(const char *str, char **end, int base, long long *value);
+
+/**
+ * Like xstrtoul(), but for unsigned short.
+ */
+int xstrtous(const char *str, char **end, int base, unsigned short *value);
+
+/**
+ * Like xstrtoul(), but for unsigned int.
+ */
+int xstrtoui(const char *str, char **end, int base, unsigned int *value);
+
+/**
+ * Wrapper for strtoul() that forbids leading spaces, negatives.
+ */
+int xstrtoul(const char *str, char **end, int base, unsigned long *value);
+
+/**
+ * Wrapper for strtoull() that forbids leading spaces, negatives.
+ */
+int xstrtoull(const char *str, char **end, int base, unsigned long long *value);
+
+/**
+ * Wrapper for strtof() that forbids leading spaces.
+ */
+int xstrtof(const char *str, char **end, float *value);
+
+/**
+ * Wrapper for strtod() that forbids leading spaces.
+ */
+int xstrtod(const char *str, char **end, double *value);
+
+/**
+ * Process a yes/no prompt.
+ *
+ * @return 1 for yes, 0 for no, and -1 for unknown.
+ */
+int ynprompt(void);
+
+// #include <string.h>
+
+/**
+ * Get the length of the pure-ASCII prefix of a string.
+ */
+size_t asciilen(const char *str);
+
+/**
+ * Get the length of the pure-ASCII prefix of a string.
+ *
+ * @str
+ * The string to check.
+ * @n
+ * The maximum prefix length.
+ */
+size_t asciinlen(const char *str, size_t n);
+
+/**
+ * Allocate a copy of a region of memory.
+ *
+ * @src
+ * The memory region to copy.
+ * @size
+ * The size of the memory region.
+ * @return
+ * A copy of the region, allocated with malloc(), or NULL on failure.
+ */
+void *xmemdup(const void *src, size_t size);
+
+/**
+ * A nice string copying function.
+ *
+ * @dest
+ * The NUL terminator of the destination string, or `end` if it is
+ * already truncated.
+ * @end
+ * The end of the destination buffer.
+ * @src
+ * The string to copy from.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *xstpecpy(char *dest, char *end, const char *src);
+
+/**
+ * A nice string copying function.
+ *
+ * @dest
+ * The NUL terminator of the destination string, or `end` if it is
+ * already truncated.
+ * @end
+ * The end of the destination buffer.
+ * @src
+ * The string to copy from.
+ * @n
+ * The maximum number of characters to copy.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *xstpencpy(char *dest, char *end, const char *src, size_t n);
+
+/**
+ * Thread-safe strerror().
+ *
+ * @errnum
+ * An error number.
+ * @return
+ * A string describing that error, which remains valid until the next
+ * xstrerror() call in the same thread.
+ */
+const char *xstrerror(int errnum);
+
+/**
+ * Shorthand for xstrerror(errno).
+ */
+const char *errstr(void);
+
+/**
+ * Format a mode like ls -l (e.g. -rw-r--r--).
+ *
+ * @mode
+ * The mode to format.
+ * @str
+ * The string to hold the formatted mode.
+ */
+void xstrmode(mode_t mode, char str[11]);
+
+#include <sys/resource.h>
+
+/**
+ * Compare two rlim_t values, accounting for infinite limits.
+ */
+int rlim_cmp(rlim_t a, rlim_t b);
+
+#include <sys/types.h>
+
+/**
+ * Portable version of makedev().
+ */
+dev_t xmakedev(int ma, int mi);
+
+/**
+ * Portable version of major().
+ */
+int xmajor(dev_t dev);
+
+/**
+ * Portable version of minor().
+ */
+int xminor(dev_t dev);
+
+// #include <sys/stat.h>
+
+/**
+ * Get the access/change/modification time from a struct stat.
+ */
+#if BFS_HAS_ST_ACMTIM
+# define ST_ATIM(sb) (sb).st_atim
+# define ST_CTIM(sb) (sb).st_ctim
+# define ST_MTIM(sb) (sb).st_mtim
+#elif BFS_HAS_ST_ACMTIMESPEC
+# define ST_ATIM(sb) (sb).st_atimespec
+# define ST_CTIM(sb) (sb).st_ctimespec
+# define ST_MTIM(sb) (sb).st_mtimespec
+#else
+# define ST_ATIM(sb) ((struct timespec) { .tv_sec = (sb).st_atime })
+# define ST_CTIM(sb) ((struct timespec) { .tv_sec = (sb).st_ctime })
+# define ST_MTIM(sb) ((struct timespec) { .tv_sec = (sb).st_mtime })
+#endif
+
+// #include <sys/wait.h>
+
+/**
+ * waitpid() wrapper that handles EINTR.
+ */
+pid_t xwaitpid(pid_t pid, int *status, int flags);
+
+#include <sys/ioctl.h> // May be necessary for struct winsize
+#include <termios.h>
+
+/**
+ * Open the controlling terminal.
+ *
+ * @flags
+ * The open() flags.
+ * @return
+ * An open file descriptor, or -1 on failure.
+ */
+int open_cterm(int flags);
+
+/**
+ * tcgetwinsize()/ioctl(TIOCGWINSZ) wrapper.
+ */
+int xtcgetwinsize(int fd, struct winsize *ws);
+
+/**
+ * tcsetwinsize()/ioctl(TIOCSWINSZ) wrapper.
+ */
+int xtcsetwinsize(int fd, const struct winsize *ws);
+
+// #include <unistd.h>
+
+/**
+ * Like dup(), but set the FD_CLOEXEC flag.
+ *
+ * @fd
+ * The file descriptor to duplicate.
+ * @return
+ * A duplicated file descriptor, or -1 on failure.
+ */
+int dup_cloexec(int fd);
+
+/**
+ * Like pipe(), but set the FD_CLOEXEC flag.
+ *
+ * @pipefd
+ * The array to hold the two file descriptors.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int pipe_cloexec(int pipefd[2]);
+
+/**
+ * A safe version of read() that handles interrupted system calls and partial
+ * reads.
+ *
+ * @return
+ * The number of bytes read. A value != nbytes indicates an error
+ * (errno != 0) or end of file (errno == 0).
+ */
+size_t xread(int fd, void *buf, size_t nbytes);
+
+/**
+ * A safe version of write() that handles interrupted system calls and partial
+ * writes.
+ *
+ * @return
+ * The number of bytes written. A value != nbytes indicates an error.
+ */
+size_t xwrite(int fd, const void *buf, size_t nbytes);
+
+/**
+ * close() variant that preserves errno.
+ *
+ * @fd
+ * The file descriptor to close.
+ */
+void close_quietly(int fd);
+
+/**
+ * close() wrapper that asserts the file descriptor is valid.
+ *
+ * @fd
+ * The file descriptor to close.
+ * @return
+ * 0 on success, or -1 on error.
+ */
+int xclose(int fd);
+
+/**
+ * Wrapper for faccessat() that handles some portability issues.
+ */
+int xfaccessat(int fd, const char *path, int amode);
+
+/**
+ * readlinkat() wrapper that dynamically allocates the result.
+ *
+ * @fd
+ * The base directory descriptor.
+ * @path
+ * The path to the link, relative to fd.
+ * @size
+ * An estimate for the size of the link name (pass 0 if unknown).
+ * @return
+ * The target of the link, allocated with malloc(), or NULL on failure.
+ */
+char *xreadlinkat(int fd, const char *path, size_t size);
+
+/**
+ * Wrapper for confstr() that allocates with malloc().
+ *
+ * @name
+ * The ID of the confstr to look up.
+ * @return
+ * The value of the confstr, or NULL on failure.
+ */
+char *xconfstr(int name);
+
+/**
+ * Portability wrapper for strtofflags().
+ *
+ * @str
+ * The string to parse. The pointee will be advanced to the first
+ * invalid position on error.
+ * @set
+ * The flags that are set in the string.
+ * @clear
+ * The flags that are cleared in the string.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear);
+
+/**
+ * Wrapper for sysconf() that works around an MSan bug.
+ */
+long xsysconf(int name);
+
+/**
+ * Check for a POSIX option[1] at runtime.
+ *
+ * [1]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap02.html#tag_02_01_06
+ *
+ * @name
+ * The symbolic name of the POSIX option (e.g. SPAWN).
+ * @return
+ * The value of the option, either -1 or a date like 202405.
+ */
+#define sysoption(name) \
+ (_POSIX_##name == 0 ? xsysconf(_SC_##name) : _POSIX_##name)
+
+/**
+ * Get the number of CPU threads available to the current process.
+ */
+long nproc(void);
+
+#include <wchar.h>
+
+/**
+ * Error-recovering mbrtowc() wrapper.
+ *
+ * @str
+ * The string to convert.
+ * @i
+ * The current index.
+ * @len
+ * The length of the string.
+ * @mb
+ * The multi-byte decoding state.
+ * @return
+ * The wide character at index *i, or WEOF if decoding fails. In either
+ * case, *i will be advanced to the next multi-byte character.
+ */
+wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb);
+
+/**
+ * wcswidth() variant that works on narrow strings.
+ *
+ * @str
+ * The string to measure.
+ * @return
+ * The likely width of that string in a terminal.
+ */
+size_t xstrwidth(const char *str);
+
+/**
+ * wcwidth() wrapper that works around LLVM bug #65532.
+ */
+#define xwcwidth BFS_INTERCEPT(wcwidth)
+
+#include <wctype.h>
+
+/**
+ * Wrap iswalpha()/iswdigit()/etc.
+ */
+#define BFS_ISWCTYPE(fn, c) BFS_INTERCEPT(fn)(c)
+
+#define xiswalnum(c) BFS_ISWCTYPE(iswalnum, c)
+#define xiswalpha(c) BFS_ISWCTYPE(iswalpha, c)
+#define xiswcntrl(c) BFS_ISWCTYPE(iswcntrl, c)
+#define xiswdigit(c) BFS_ISWCTYPE(iswdigit, c)
+#define xiswlower(c) BFS_ISWCTYPE(iswlower, c)
+#define xiswgraph(c) BFS_ISWCTYPE(iswgraph, c)
+#define xiswprint(c) BFS_ISWCTYPE(iswprint, c)
+#define xiswpunct(c) BFS_ISWCTYPE(iswpunct, c)
+#define xiswspace(c) BFS_ISWCTYPE(iswspace, c)
+#define xiswupper(c) BFS_ISWCTYPE(iswupper, c)
+#define xiswxdigit(c) BFS_ISWCTYPE(iswxdigit, c)
+
+// #include <wordexp.h>
+
+/**
+ * Flags for wordesc().
+ */
+enum wesc_flags {
+ /**
+ * Escape special characters so that the shell will treat the escaped
+ * string as a single word.
+ */
+ WESC_SHELL = 1 << 0,
+ /**
+ * Escape special characters so that the escaped string is safe to print
+ * to a TTY.
+ */
+ WESC_TTY = 1 << 1,
+};
+
+/**
+ * Escape a string as a single shell word.
+ *
+ * @dest
+ * The destination string to fill.
+ * @end
+ * The end of the destination buffer.
+ * @src
+ * The string to escape.
+ * @flags
+ * Controls which characters to escape.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags);
+
+/**
+ * Escape a string as a single shell word.
+ *
+ * @dest
+ * The destination string to fill.
+ * @end
+ * The end of the destination buffer.
+ * @src
+ * The string to escape.
+ * @n
+ * The maximum length of the string.
+ * @flags
+ * Controls which characters to escape.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags);
+
+#endif // BFS_BFSTD_H
diff --git a/src/bftw.c b/src/bftw.c
new file mode 100644
index 0000000..0ca6f34
--- /dev/null
+++ b/src/bftw.c
@@ -0,0 +1,2344 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * The bftw() implementation consists of the following components:
+ *
+ * - struct bftw_file: A file that has been encountered during the traversal.
+ * They have reference-counted links to their parents in the directory tree.
+ *
+ * - struct bftw_list: A linked list of bftw_file's.
+ *
+ * - struct bftw_queue: A multi-stage queue of bftw_file's.
+ *
+ * - struct bftw_cache: An LRU list of bftw_file's with open file descriptors,
+ * used for openat() to minimize the amount of path re-traversals.
+ *
+ * - struct bftw_state: Represents the current state of the traversal, allowing
+ * various helper functions to take fewer parameters.
+ */
+
+#include "bftw.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "dir.h"
+#include "dstring.h"
+#include "ioq.h"
+#include "list.h"
+#include "mtab.h"
+#include "stat.h"
+#include "trie.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+/** Initialize a bftw_stat cache. */
+static void bftw_stat_init(struct bftw_stat *bufs, struct bfs_stat *stat_buf, struct bfs_stat *lstat_buf) {
+ bufs->stat_buf = stat_buf;
+ bufs->lstat_buf = lstat_buf;
+ bufs->stat_err = -1;
+ bufs->lstat_err = -1;
+}
+
+/** Fill a bftw_stat cache from another one. */
+static void bftw_stat_fill(struct bftw_stat *dest, const struct bftw_stat *src) {
+ if (dest->stat_err < 0 && src->stat_err >= 0) {
+ dest->stat_buf = src->stat_buf;
+ dest->stat_err = src->stat_err;
+ }
+
+ if (dest->lstat_err < 0 && src->lstat_err >= 0) {
+ dest->lstat_buf = src->lstat_buf;
+ dest->lstat_err = src->lstat_err;
+ }
+}
+
+/** Cache a bfs_stat() result. */
+static void bftw_stat_cache(struct bftw_stat *bufs, enum bfs_stat_flags flags, const struct bfs_stat *buf, int err) {
+ if (flags & BFS_STAT_NOFOLLOW) {
+ bufs->lstat_buf = buf;
+ bufs->lstat_err = err;
+ if (err || !S_ISLNK(buf->mode)) {
+ // Non-link, so share stat info
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+ } else if (flags & BFS_STAT_TRYFOLLOW) {
+ if (err) {
+ bufs->stat_err = err;
+ } else if (S_ISLNK(buf->mode)) {
+ bufs->lstat_buf = buf;
+ bufs->lstat_err = err;
+ bufs->stat_err = ENOENT;
+ } else {
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+ } else {
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+}
+
+/** Caching bfs_stat(). */
+static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+ struct bfs_stat *buf;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ buf = (struct bfs_stat *)bufs->lstat_buf;
+ if (bufs->lstat_err == 0) {
+ return buf;
+ } else if (bufs->lstat_err > 0) {
+ errno = bufs->lstat_err;
+ return NULL;
+ }
+ } else {
+ buf = (struct bfs_stat *)bufs->stat_buf;
+ if (bufs->stat_err == 0) {
+ return buf;
+ } else if (bufs->stat_err > 0) {
+ errno = bufs->stat_err;
+ return NULL;
+ }
+ }
+
+ struct bfs_stat *ret;
+ int err;
+ if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, buf) == 0) {
+ ret = buf;
+ err = 0;
+#ifdef S_IFWHT
+ } else if (errno == ENOENT && ftwbuf->type == BFS_WHT) {
+ // This matches the behavior of FTS_WHITEOUT on BSD
+ ret = memset(buf, 0, sizeof(*buf));
+ ret->mode = S_IFWHT;
+ err = 0;
+#endif
+ } else {
+ ret = NULL;
+ err = errno;
+ }
+
+ bftw_stat_cache(bufs, flags, ret, err);
+ return ret;
+}
+
+const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
+ const struct bfs_stat *ret;
+
+ if (flags & BFS_STAT_TRYFOLLOW) {
+ ret = bftw_stat_impl(mutbuf, BFS_STAT_FOLLOW);
+ if (!ret && errno_is_like(ENOENT)) {
+ ret = bftw_stat_impl(mutbuf, BFS_STAT_NOFOLLOW);
+ }
+ } else {
+ ret = bftw_stat_impl(mutbuf, flags);
+ }
+
+ return ret;
+}
+
+const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ const struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ if (bufs->lstat_err == 0) {
+ return bufs->lstat_buf;
+ }
+ } else if (bufs->stat_err == 0) {
+ return bufs->stat_buf;
+ } else if ((flags & BFS_STAT_TRYFOLLOW) && error_is_like(bufs->stat_err, ENOENT)) {
+ if (bufs->lstat_err == 0) {
+ return bufs->lstat_buf;
+ }
+ }
+
+ return NULL;
+}
+
+enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ if (flags & BFS_STAT_NOFOLLOW) {
+ if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
+ return ftwbuf->type;
+ }
+ } else if (flags & BFS_STAT_TRYFOLLOW) {
+ if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) {
+ return ftwbuf->type;
+ }
+ } else {
+ if (ftwbuf->type != BFS_LNK) {
+ return ftwbuf->type;
+ } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) {
+ return BFS_ERROR;
+ }
+ }
+
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
+ if (statbuf) {
+ return bfs_mode_to_type(statbuf->mode);
+ } else {
+ return BFS_ERROR;
+ }
+}
+
+/**
+ * A file.
+ */
+struct bftw_file {
+ /** The parent directory, if any. */
+ struct bftw_file *parent;
+ /** The root under which this file was found. */
+ struct bftw_file *root;
+
+ /**
+ * List node for:
+ *
+ * bftw_queue::buffer
+ * bftw_queue::waiting
+ * bftw_file_open()::parents
+ */
+ struct bftw_file *next;
+
+ /**
+ * List node for:
+ *
+ * bftw_queue::ready
+ * bftw_state::to_close
+ */
+ struct { struct bftw_file *next; } ready;
+
+ /**
+ * List node for bftw_cache.
+ */
+ struct {
+ struct bftw_file *prev;
+ struct bftw_file *next;
+ } lru;
+
+ /** This file's depth in the walk. */
+ size_t depth;
+ /** Reference count (for ->parent). */
+ size_t refcount;
+
+ /** Pin count (for ->fd). */
+ size_t pincount;
+ /** An open descriptor to this file, or -1. */
+ int fd;
+ /** Whether this file has a pending ioq request. */
+ bool ioqueued;
+ /** An open directory for this file, if any. */
+ struct bfs_dir *dir;
+
+ /** This file's type, if known. */
+ enum bfs_type type;
+ /** The device number, for cycle detection. */
+ dev_t dev;
+ /** The inode number, for cycle detection. */
+ ino_t ino;
+
+ /** Cached bfs_stat() info. */
+ struct bftw_stat stat_bufs;
+
+ /** The offset of this file in the full path. */
+ size_t nameoff;
+ /** The length of the file's name. */
+ size_t namelen;
+ /** The file's name. */
+ char name[]; // _counted_by(namelen + 1)
+};
+
+/**
+ * A linked list of bftw_file's.
+ */
+struct bftw_list {
+ struct bftw_file *head;
+ struct bftw_file **tail;
+};
+
+/**
+ * bftw_queue flags.
+ */
+enum bftw_qflags {
+ /** Track the sync/async service balance. */
+ BFTW_QBALANCE = 1 << 0,
+ /** Buffer files before adding them to the queue. */
+ BFTW_QBUFFER = 1 << 1,
+ /** Use LIFO (stack/DFS) ordering. */
+ BFTW_QLIFO = 1 << 2,
+ /** Maintain a strict order. */
+ BFTW_QORDER = 1 << 3,
+};
+
+/**
+ * A queue of bftw_file's that may be serviced asynchronously.
+ *
+ * A bftw_queue comprises three linked lists each tracking different stages.
+ * When BFTW_QBUFFER is set, files are initially pushed to the buffer:
+ *
+ * â•”â•â•â•â•— â•”â•â•â•╦â•â•â•â•—
+ * buffer: ║ 𘩠║ ║ 𘩠║ 𘪠║
+ * â• â•â•â•╬â•â•â•╦â•â•â•â•— â• â•â•â•╬â•â•â•╬â•â•â•â•—
+ * waiting: ║ e ║ f ║ g ║ → ║ e ║ f ║ g ║
+ * â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•â•— â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•â•—
+ * ready: â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘ â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘
+ * ╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•╠╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•
+ *
+ * When bftw_queue_flush() is called, the files in the buffer are appended to
+ * the waiting list (or prepended, if BFTW_QLIFO is set):
+ *
+ * â•”â•â•—
+ * buffer: â•‘ â•‘
+ * â• â•â•©â•╦â•â•â•╦â•â•â•╦â•â•â•╦â•â•â•â•—
+ * waiting: â•‘ e â•‘ f â•‘ g â•‘ h â•‘ i â•‘
+ * â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•â•
+ * ready: â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘
+ * ╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•
+ *
+ * Using the buffer gives a more natural ordering for BFTW_QLIFO, and allows
+ * files to be sorted before adding them to the waiting list. If BFTW_QBUFFER
+ * is not set, files are pushed directly to the waiting list instead.
+ *
+ * Files on the waiting list are waiting to be "serviced" asynchronously by the
+ * ioq (for example, by an ioq_opendir() or ioq_stat() call). While they are
+ * being serviced, they are detached from the queue by bftw_queue_detach() and
+ * are not tracked by the queue at all:
+ *
+ * â•”â•â•—
+ * buffer: â•‘ â•‘
+ * â• â•â•©â•╦â•â•â•╦â•â•â•â•— ⎛ ┌───┬───┠⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𓮠│ 𓯠│ ⎟
+ * â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•â•— ⎠└───┴───┘ ⎠
+ * ready: â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘
+ * ╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•
+ *
+ * When their async service is complete, files are reattached to the queue by
+ * bftw_queue_attach(), this time on the ready list:
+ *
+ * â•”â•â•—
+ * buffer: â•‘ â•‘
+ * â• â•â•©â•╦â•â•â•╦â•â•â•â•— ⎛ ┌───┠⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𓮠│ ⎟
+ * â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•╦â•â•â•â•— ⎠└───┘ ⎠
+ * ready: â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘ ð•— â•‘
+ * ╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•
+ *
+ * Files are added to the ready list in the order they are finished by the ioq.
+ * bftw_queue_pop() pops a file from the ready list if possible. Otherwise, it
+ * pops from the waiting list, and the file must be serviced synchronously.
+ *
+ * However, if BFTW_QORDER is set, files must be popped in the exact order they
+ * are added to the waiting list (to maintain sorted order). In this case,
+ * files are added to the waiting and ready lists at the same time. The
+ * file->ioqueued flag is set while it is in-service, so that bftw() can wait
+ * for it to be truly ready before using it.
+ *
+ * â•”â•â•—
+ * buffer: â•‘ â•‘
+ * â• â•â•©â•╦â•â•â•╦â•â•â•â•— ⎛ ┌───┠⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𓮠│ ⎟
+ * â• â•â•â•╬â•â•â•╬â•â•â•╬â•â•â•╦â•â•â•╦â•â•â•╦â•â•â•╦â•â•â•╦â•â•â•â•— ⎠└───┘ ⎠
+ * ready: â•‘ ð•’ â•‘ ð•“ â•‘ ð•” â•‘ ð•• â•‘ ð“® â•‘ ð•— â•‘ g â•‘ h â•‘ i â•‘
+ * ╚â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•©â•â•â•â•
+ *
+ * If BFTW_QBALANCE is set, queue->imbalance tracks the delta between async
+ * service (negative) and synchronous service (positive). The queue is
+ * considered "balanced" when this number is non-negative. Only a balanced
+ * queue will perform any async service, ensuring work is fairly distributed
+ * between the main thread and the ioq.
+ *
+ * BFTW_QBALANCE is only set for single-threaded ioqs. When an ioq has multiple
+ * threads, it is faster to wait for the ioq to complete an operation than it is
+ * to perform it on the main thread.
+ */
+struct bftw_queue {
+ /** Queue flags. */
+ enum bftw_qflags flags;
+ /** A buffer of files to be enqueued together. */
+ struct bftw_list buffer;
+ /** A list of files which are waiting to be serviced. */
+ struct bftw_list waiting;
+ /** A list of already-serviced files. */
+ struct bftw_list ready;
+ /** The current size of the queue. */
+ size_t size;
+ /** The number of files currently in-service. */
+ size_t ioqueued;
+ /** Tracks the imbalance between synchronous and async service. */
+ unsigned long imbalance;
+};
+
+/** Initialize a queue. */
+static void bftw_queue_init(struct bftw_queue *queue, enum bftw_qflags flags) {
+ queue->flags = flags;
+ SLIST_INIT(&queue->buffer);
+ SLIST_INIT(&queue->waiting);
+ SLIST_INIT(&queue->ready);
+ queue->size = 0;
+ queue->ioqueued = 0;
+ queue->imbalance = 0;
+}
+
+/** Add a file to the queue. */
+static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
+ if (queue->flags & BFTW_QBUFFER) {
+ SLIST_APPEND(&queue->buffer, file);
+ } else if (queue->flags & BFTW_QLIFO) {
+ SLIST_PREPEND(&queue->waiting, file);
+ if (queue->flags & BFTW_QORDER) {
+ SLIST_PREPEND(&queue->ready, file, ready);
+ }
+ } else {
+ SLIST_APPEND(&queue->waiting, file);
+ if (queue->flags & BFTW_QORDER) {
+ SLIST_APPEND(&queue->ready, file, ready);
+ }
+ }
+
+ ++queue->size;
+}
+
+/** Add any buffered files to the queue. */
+static void bftw_queue_flush(struct bftw_queue *queue) {
+ if (!(queue->flags & BFTW_QBUFFER)) {
+ return;
+ }
+
+ if (queue->flags & BFTW_QORDER) {
+ // When sorting, add files to the ready list at the same time
+ // (and in the same order) as they are added to the waiting list
+ struct bftw_file **cursor = (queue->flags & BFTW_QLIFO)
+ ? &queue->ready.head
+ : queue->ready.tail;
+ for_slist (struct bftw_file, file, &queue->buffer) {
+ cursor = SLIST_INSERT(&queue->ready, cursor, file, ready);
+ }
+ }
+
+ if (queue->flags & BFTW_QLIFO) {
+ SLIST_EXTEND(&queue->buffer, &queue->waiting);
+ }
+
+ SLIST_EXTEND(&queue->waiting, &queue->buffer);
+}
+
+/** Check if the queue is properly balanced for async work. */
+static bool bftw_queue_balanced(const struct bftw_queue *queue) {
+ if (queue->flags & BFTW_QBALANCE) {
+ return (long)queue->imbalance >= 0;
+ } else {
+ return true;
+ }
+}
+
+/** Update the queue balance for (a)sync service. */
+static void bftw_queue_rebalance(struct bftw_queue *queue, bool async) {
+ if (async) {
+ --queue->imbalance;
+ } else {
+ ++queue->imbalance;
+ }
+}
+
+/** Detach the next waiting file. */
+static void bftw_queue_detach(struct bftw_queue *queue, struct bftw_file *file, bool async) {
+ bfs_assert(!file->ioqueued);
+
+ if (file == SLIST_HEAD(&queue->buffer)) {
+ // To maintain order, we can't detach any files until they're
+ // added to the waiting/ready lists
+ bfs_assert(!(queue->flags & BFTW_QORDER));
+ SLIST_POP(&queue->buffer);
+ } else if (file == SLIST_HEAD(&queue->waiting)) {
+ SLIST_POP(&queue->waiting);
+ } else {
+ bfs_bug("Detached file was not buffered or waiting");
+ }
+
+ if (async) {
+ file->ioqueued = true;
+ ++queue->ioqueued;
+ bftw_queue_rebalance(queue, true);
+ }
+}
+
+/** Reattach a serviced file to the queue. */
+static void bftw_queue_attach(struct bftw_queue *queue, struct bftw_file *file, bool async) {
+ if (async) {
+ bfs_assert(file->ioqueued);
+ file->ioqueued = false;
+ --queue->ioqueued;
+ } else {
+ bfs_assert(!file->ioqueued);
+ }
+
+ if (!(queue->flags & BFTW_QORDER)) {
+ SLIST_APPEND(&queue->ready, file, ready);
+ }
+}
+
+/** Make a file ready immediately. */
+static void bftw_queue_skip(struct bftw_queue *queue, struct bftw_file *file) {
+ bftw_queue_detach(queue, file, false);
+ bftw_queue_attach(queue, file, false);
+}
+
+/** Get the next waiting file. */
+static struct bftw_file *bftw_queue_waiting(const struct bftw_queue *queue) {
+ if (!(queue->flags & BFTW_QBUFFER)) {
+ return SLIST_HEAD(&queue->waiting);
+ }
+
+ if (queue->flags & BFTW_QORDER) {
+ // Don't detach files until they're on the waiting/ready lists
+ return SLIST_HEAD(&queue->waiting);
+ }
+
+ const struct bftw_list *prefix = &queue->waiting;
+ const struct bftw_list *suffix = &queue->buffer;
+ if (queue->flags & BFTW_QLIFO) {
+ prefix = &queue->buffer;
+ suffix = &queue->waiting;
+ }
+
+ struct bftw_file *file = SLIST_HEAD(prefix);
+ if (!file) {
+ file = SLIST_HEAD(suffix);
+ }
+ return file;
+}
+
+/** Get the next ready file. */
+static struct bftw_file *bftw_queue_ready(const struct bftw_queue *queue) {
+ return SLIST_HEAD(&queue->ready);
+}
+
+/** Pop a file from the queue. */
+static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) {
+ // Don't pop until we've had a chance to sort the buffer
+ bfs_assert(SLIST_EMPTY(&queue->buffer));
+
+ struct bftw_file *file = SLIST_POP(&queue->ready, ready);
+
+ if (!file || file == SLIST_HEAD(&queue->waiting)) {
+ // If no files are ready, try the waiting list. Or, if
+ // BFTW_QORDER is set, we may need to pop from both lists.
+ file = SLIST_POP(&queue->waiting);
+ }
+
+ if (file) {
+ --queue->size;
+ }
+
+ return file;
+}
+
+/**
+ * A cache of open directories.
+ */
+struct bftw_cache {
+ /** The head of the LRU list. */
+ struct bftw_file *head;
+ /** The tail of the LRU list. */
+ struct bftw_file *tail;
+ /** The insertion target for the LRU list. */
+ struct bftw_file *target;
+ /** The remaining capacity of the LRU list. */
+ size_t capacity;
+
+ /** bftw_file arena. */
+ struct varena files;
+
+ /** bfs_dir arena. */
+ struct arena dirs;
+ /** Remaining bfs_dir capacity. */
+ int dir_limit;
+
+ /** bfs_stat arena. */
+ struct arena stat_bufs;
+};
+
+/** Initialize a cache. */
+static void bftw_cache_init(struct bftw_cache *cache, size_t capacity) {
+ LIST_INIT(cache);
+ cache->target = NULL;
+ cache->capacity = capacity;
+
+ VARENA_INIT(&cache->files, struct bftw_file, name);
+
+ bfs_dir_arena(&cache->dirs);
+
+ if (cache->capacity > 1024) {
+ cache->dir_limit = 1024;
+ } else {
+ cache->dir_limit = capacity - 1;
+ }
+
+ ARENA_INIT(&cache->stat_bufs, struct bfs_stat);
+}
+
+/** Allocate a directory. */
+static struct bfs_dir *bftw_allocdir(struct bftw_cache *cache, bool force) {
+ if (!force && cache->dir_limit <= 0) {
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ struct bfs_dir *dir = arena_alloc(&cache->dirs);
+ if (dir) {
+ --cache->dir_limit;
+ }
+ return dir;
+}
+
+/** Free a directory. */
+static void bftw_freedir(struct bftw_cache *cache, struct bfs_dir *dir) {
+ ++cache->dir_limit;
+ arena_free(&cache->dirs, dir);
+}
+
+/** Remove a bftw_file from the LRU list. */
+static void bftw_lru_remove(struct bftw_cache *cache, struct bftw_file *file) {
+ if (cache->target == file) {
+ cache->target = file->lru.prev;
+ }
+
+ LIST_REMOVE(cache, file, lru);
+}
+
+/** Remove a bftw_file from the cache. */
+static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
+ bftw_lru_remove(cache, file);
+ ++cache->capacity;
+}
+
+/** Close a bftw_file. */
+static void bftw_file_close(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount == 0);
+
+ if (file->dir) {
+ bfs_closedir(file->dir);
+ bftw_freedir(cache, file->dir);
+ file->dir = NULL;
+ } else {
+ xclose(file->fd);
+ }
+
+ file->fd = -1;
+ bftw_cache_remove(cache, file);
+}
+
+/** Pop the least recently used directory from the cache. */
+static int bftw_cache_pop(struct bftw_cache *cache) {
+ struct bftw_file *file = cache->tail;
+ if (!file) {
+ return -1;
+ }
+
+ bftw_file_close(cache, file);
+ return 0;
+}
+
+/** Add a bftw_file to the LRU list. */
+static void bftw_lru_add(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ LIST_INSERT(cache, cache->target, file, lru);
+
+ // Prefer to keep the root paths open by keeping them at the head of the list
+ if (file->depth == 0) {
+ cache->target = file;
+ }
+}
+
+/** Add a bftw_file to the cache. */
+static int bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ if (cache->capacity == 0 && bftw_cache_pop(cache) != 0) {
+ bftw_file_close(cache, file);
+ errno = EMFILE;
+ return -1;
+ }
+
+ bfs_assert(cache->capacity > 0);
+ --cache->capacity;
+
+ bftw_lru_add(cache, file);
+ return 0;
+}
+
+/** Pin a cache entry so it won't be closed. */
+static void bftw_cache_pin(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ if (file->pincount++ == 0) {
+ bftw_lru_remove(cache, file);
+ }
+}
+
+/** Unpin a cache entry. */
+static void bftw_cache_unpin(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount > 0);
+
+ if (--file->pincount == 0) {
+ bftw_lru_add(cache, file);
+ }
+}
+
+/** Compute the name offset of a child path. */
+static size_t bftw_child_nameoff(const struct bftw_file *parent) {
+ size_t ret = parent->nameoff + parent->namelen;
+ if (parent->name[parent->namelen - 1] != '/') {
+ ++ret;
+ }
+ return ret;
+}
+
+/** Destroy a cache. */
+static void bftw_cache_destroy(struct bftw_cache *cache) {
+ bfs_assert(LIST_EMPTY(cache));
+ bfs_assert(!cache->target);
+
+ arena_destroy(&cache->stat_bufs);
+ arena_destroy(&cache->dirs);
+ varena_destroy(&cache->files);
+}
+
+/** Create a new bftw_file. */
+static struct bftw_file *bftw_file_new(struct bftw_cache *cache, struct bftw_file *parent, const char *name) {
+ size_t namelen = strlen(name);
+ struct bftw_file *file = varena_alloc(&cache->files, namelen + 1);
+ if (!file) {
+ return NULL;
+ }
+
+ file->parent = parent;
+
+ if (parent) {
+ file->root = parent->root;
+ file->depth = parent->depth + 1;
+ file->nameoff = bftw_child_nameoff(parent);
+ ++parent->refcount;
+ } else {
+ file->root = file;
+ file->depth = 0;
+ file->nameoff = 0;
+ }
+
+ SLIST_ITEM_INIT(file);
+ SLIST_ITEM_INIT(file, ready);
+ LIST_ITEM_INIT(file, lru);
+
+ file->refcount = 1;
+ file->pincount = 0;
+ file->fd = -1;
+ file->ioqueued = false;
+ file->dir = NULL;
+
+ file->type = BFS_UNKNOWN;
+ file->dev = -1;
+ file->ino = -1;
+
+ bftw_stat_init(&file->stat_bufs, NULL, NULL);
+
+ file->namelen = namelen;
+ memcpy(file->name, name, namelen + 1);
+
+ return file;
+}
+
+/** Associate an open directory with a bftw_file. */
+static void bftw_file_set_dir(struct bftw_cache *cache, struct bftw_file *file, struct bfs_dir *dir) {
+ bfs_assert(!file->dir);
+ file->dir = dir;
+
+ if (file->fd >= 0) {
+ bfs_assert(file->fd == bfs_dirfd(dir));
+ } else {
+ file->fd = bfs_dirfd(dir);
+ bftw_cache_add(cache, file);
+ }
+}
+
+/** Free a file's cached stat() buffers. */
+static void bftw_stat_recycle(struct bftw_cache *cache, struct bftw_file *file) {
+ struct bftw_stat *bufs = &file->stat_bufs;
+
+ struct bfs_stat *stat_buf = (struct bfs_stat *)bufs->stat_buf;
+ struct bfs_stat *lstat_buf = (struct bfs_stat *)bufs->lstat_buf;
+ if (stat_buf) {
+ arena_free(&cache->stat_bufs, stat_buf);
+ } else if (lstat_buf) {
+ arena_free(&cache->stat_bufs, lstat_buf);
+ }
+
+ bftw_stat_init(bufs, NULL, NULL);
+}
+
+/** Free a bftw_file. */
+static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->refcount == 0);
+
+ if (file->fd >= 0) {
+ bftw_file_close(cache, file);
+ }
+
+ bftw_stat_recycle(cache, file);
+
+ varena_free(&cache->files, file, file->namelen + 1);
+}
+
+/**
+ * Holds the current state of the bftw() traversal.
+ */
+struct bftw_state {
+ /** The path(s) to start from. */
+ const char **paths;
+ /** The number of starting paths. */
+ size_t npaths;
+ /** bftw() callback. */
+ bftw_callback *callback;
+ /** bftw() callback data. */
+ void *ptr;
+ /** bftw() flags. */
+ enum bftw_flags flags;
+ /** Search strategy. */
+ enum bftw_strategy strategy;
+ /** The mount table. */
+ const struct bfs_mtab *mtab;
+ /** bfs_opendir() flags. */
+ enum bfs_dir_flags dir_flags;
+
+ /** The appropriate errno value, if any. */
+ int error;
+
+ /** The cache of open directories. */
+ struct bftw_cache cache;
+
+ /** The async I/O queue. */
+ struct ioq *ioq;
+ /** The number of I/O threads. */
+ size_t nthreads;
+
+ /** The queue of unpinned directories to unwrap. */
+ struct bftw_list to_close;
+ /** The queue of files to visit. */
+ struct bftw_queue fileq;
+ /** The queue of directories to open/read. */
+ struct bftw_queue dirq;
+
+ /** The current path. */
+ dchar *path;
+ /** The current file. */
+ struct bftw_file *file;
+ /** The previous file. */
+ struct bftw_file *previous;
+
+ /** The currently open directory. */
+ struct bfs_dir *dir;
+ /** The current directory entry. */
+ struct bfs_dirent *de;
+ /** Storage for the directory entry. */
+ struct bfs_dirent de_storage;
+ /** Any error encountered while reading the directory. */
+ int direrror;
+
+ /** Extra data about the current file. */
+ struct BFTW ftwbuf;
+ /** stat() buffer storage. */
+ struct bfs_stat stat_buf;
+ /** lstat() buffer storage. */
+ struct bfs_stat lstat_buf;
+};
+
+/** Check if we have to buffer files before visiting them. */
+static bool bftw_must_buffer(const struct bftw_state *state) {
+ if (state->flags & BFTW_SORT) {
+ // Have to buffer the files to sort them
+ return true;
+ }
+
+ if (state->strategy == BFTW_DFS && state->nthreads == 0) {
+ // Without buffering, we would get a not-quite-depth-first
+ // ordering:
+ //
+ // a
+ // b
+ // a/c
+ // a/c/d
+ // b/e
+ // b/e/f
+ //
+ // This is okay for iterative deepening, since the caller only
+ // sees files at the target depth. We also deem it okay for
+ // parallel searches, since the order is unpredictable anyway.
+ return true;
+ }
+
+ if ((state->flags & BFTW_STAT) && state->nthreads > 1) {
+ // We will be buffering every file anyway for ioq_stat()
+ return true;
+ }
+
+ return false;
+}
+
+/** Initialize the bftw() state. */
+static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) {
+ state->paths = args->paths;
+ state->npaths = args->npaths;
+ state->callback = args->callback;
+ state->ptr = args->ptr;
+ state->flags = args->flags;
+ state->strategy = args->strategy;
+ state->mtab = args->mtab;
+ state->dir_flags = 0;
+ state->error = 0;
+
+ if (args->nopenfd < 2) {
+ errno = EMFILE;
+ return -1;
+ }
+
+ size_t nopenfd = args->nopenfd;
+ size_t qdepth = 4096;
+ size_t nthreads = args->nthreads;
+
+#if BFS_WITH_LIBURING
+ // io_uring uses one fd per ring, ioq uses one ring per thread
+ if (nthreads >= nopenfd - 1) {
+ nthreads = nopenfd - 2;
+ }
+ nopenfd -= nthreads;
+#endif
+
+ bftw_cache_init(&state->cache, nopenfd);
+
+ if (nthreads > 0) {
+ state->ioq = ioq_create(qdepth, nthreads);
+ if (!state->ioq) {
+ return -1;
+ }
+ } else {
+ state->ioq = NULL;
+ }
+ state->nthreads = nthreads;
+
+ if (bftw_must_buffer(state)) {
+ state->flags |= BFTW_BUFFER;
+ }
+
+ if (state->flags & BFTW_WHITEOUTS) {
+ state->dir_flags |= BFS_DIR_WHITEOUTS;
+ }
+
+ SLIST_INIT(&state->to_close);
+
+ enum bftw_qflags qflags = 0;
+ if (state->strategy != BFTW_BFS) {
+ qflags |= BFTW_QBUFFER | BFTW_QLIFO;
+ }
+ if (state->flags & BFTW_BUFFER) {
+ qflags |= BFTW_QBUFFER;
+ }
+ if (state->flags & BFTW_SORT) {
+ qflags |= BFTW_QORDER;
+ } else if (nthreads == 1) {
+ qflags |= BFTW_QBALANCE;
+ }
+ bftw_queue_init(&state->fileq, qflags);
+
+ if (state->strategy == BFTW_BFS || (state->flags & BFTW_BUFFER)) {
+ // In breadth-first mode, or if we're already buffering files,
+ // directories can be queued in FIFO order
+ qflags &= ~(BFTW_QBUFFER | BFTW_QLIFO);
+ }
+ bftw_queue_init(&state->dirq, qflags);
+
+ state->path = NULL;
+ state->file = NULL;
+ state->previous = NULL;
+
+ state->dir = NULL;
+ state->de = NULL;
+ state->direrror = 0;
+
+ return 0;
+}
+
+/** Queue a directory for unwrapping. */
+static void bftw_delayed_unwrap(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->dir);
+
+ if (!SLIST_ATTACHED(&state->to_close, file, ready)) {
+ SLIST_APPEND(&state->to_close, file, ready);
+ }
+}
+
+/** Unpin a file's parent. */
+static void bftw_unpin_parent(struct bftw_state *state, struct bftw_file *file, bool unwrap) {
+ struct bftw_file *parent = file->parent;
+ if (!parent) {
+ return;
+ }
+
+ bftw_cache_unpin(&state->cache, parent);
+
+ if (unwrap && parent->dir && parent->pincount == 0) {
+ bftw_delayed_unwrap(state, parent);
+ }
+}
+
+/** Pop a response from the I/O queue. */
+static int bftw_ioq_pop(struct bftw_state *state, bool block) {
+ struct bftw_cache *cache = &state->cache;
+ struct ioq *ioq = state->ioq;
+ if (!ioq) {
+ return -1;
+ }
+
+ ioq_submit(ioq);
+ struct ioq_ent *ent = ioq_pop(ioq, block);
+ if (!ent) {
+ return -1;
+ }
+
+ struct bftw_file *file = ent->ptr;
+ if (file) {
+ bftw_unpin_parent(state, file, true);
+ }
+
+ enum ioq_op op = ent->op;
+ switch (op) {
+ case IOQ_CLOSE:
+ ++cache->capacity;
+ break;
+
+ case IOQ_CLOSEDIR:
+ ++cache->capacity;
+ bftw_freedir(cache, ent->closedir.dir);
+ break;
+
+ case IOQ_OPENDIR:
+ ++cache->capacity;
+
+ if (ent->result >= 0) {
+ bftw_file_set_dir(cache, file, ent->opendir.dir);
+ } else {
+ bftw_freedir(cache, ent->opendir.dir);
+ }
+
+ bftw_queue_attach(&state->dirq, file, true);
+ break;
+
+ case IOQ_STAT:
+ if (ent->result >= 0) {
+ bftw_stat_cache(&file->stat_bufs, ent->stat.flags, ent->stat.buf, 0);
+ } else {
+ arena_free(&cache->stat_bufs, ent->stat.buf);
+ bftw_stat_cache(&file->stat_bufs, ent->stat.flags, NULL, -ent->result);
+ }
+
+ bftw_queue_attach(&state->fileq, file, true);
+ break;
+
+ default:
+ bfs_bug("Unexpected ioq op %d", (int)op);
+ break;
+ }
+
+ ioq_free(ioq, ent);
+ return op;
+}
+
+/** Try to reserve space in the I/O queue. */
+static int bftw_ioq_reserve(struct bftw_state *state) {
+ struct ioq *ioq = state->ioq;
+ if (!ioq) {
+ return -1;
+ }
+
+ if (ioq_capacity(ioq) > 0) {
+ return 0;
+ }
+
+ // With more than one background thread, it's faster to wait on
+ // background I/O than it is to do it on the main thread
+ bool block = state->nthreads > 1;
+ if (bftw_ioq_pop(state, block) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Try to reserve space in the cache. */
+static int bftw_cache_reserve(struct bftw_state *state) {
+ struct bftw_cache *cache = &state->cache;
+ if (cache->capacity > 0) {
+ return 0;
+ }
+
+ while (bftw_ioq_pop(state, true) >= 0) {
+ if (cache->capacity > 0) {
+ return 0;
+ }
+ }
+
+ if (bftw_cache_pop(cache) != 0) {
+ errno = EMFILE;
+ return -1;
+ }
+
+ bfs_assert(cache->capacity > 0);
+ return 0;
+}
+
+/** Open a bftw_file relative to another one. */
+static int bftw_file_openat(struct bftw_state *state, struct bftw_file *file, struct bftw_file *base, const char *at_path) {
+ bfs_assert(file->fd < 0);
+
+ struct bftw_cache *cache = &state->cache;
+
+ int at_fd = AT_FDCWD;
+ if (base) {
+ bftw_cache_pin(cache, base);
+ at_fd = base->fd;
+ }
+
+ int fd = -1;
+ if (bftw_cache_reserve(state) != 0) {
+ goto unpin;
+ }
+
+ int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY;
+ fd = openat(at_fd, at_path, flags);
+
+ if (fd < 0 && errno == EMFILE) {
+ if (bftw_cache_pop(cache) == 0) {
+ fd = openat(at_fd, at_path, flags);
+ }
+ cache->capacity = 1;
+ }
+
+unpin:
+ if (base) {
+ bftw_cache_unpin(cache, base);
+ }
+
+ if (fd >= 0) {
+ file->fd = fd;
+ bftw_cache_add(cache, file);
+ }
+
+ return fd;
+}
+
+/** Open a bftw_file. */
+static int bftw_file_open(struct bftw_state *state, struct bftw_file *file, const char *path) {
+ // Find the nearest open ancestor
+ struct bftw_file *base = file;
+ do {
+ base = base->parent;
+ } while (base && base->fd < 0);
+
+ const char *at_path = path;
+ if (base) {
+ at_path += bftw_child_nameoff(base);
+ }
+
+ int fd = bftw_file_openat(state, file, base, at_path);
+ if (fd >= 0 || !errno_is_like(ENAMETOOLONG)) {
+ return fd;
+ }
+
+ // Handle ENAMETOOLONG by manually traversing the path component-by-component
+ struct bftw_list parents;
+ SLIST_INIT(&parents);
+
+ // Reverse the chain of parents
+ for (struct bftw_file *cur = file; cur != base; cur = cur->parent) {
+ SLIST_PREPEND(&parents, cur);
+ }
+
+ // Open each component relative to its parent
+ drain_slist (struct bftw_file, cur, &parents) {
+ if (!cur->parent || cur->parent->fd >= 0) {
+ bftw_file_openat(state, cur, cur->parent, cur->name);
+ }
+ }
+
+ return file->fd;
+}
+
+/** Close a directory, asynchronously if possible. */
+static int bftw_ioq_closedir(struct bftw_state *state, struct bfs_dir *dir) {
+ if (bftw_ioq_reserve(state) == 0) {
+ if (ioq_closedir(state->ioq, dir, NULL) == 0) {
+ return 0;
+ }
+ }
+
+ struct bftw_cache *cache = &state->cache;
+ int ret = bfs_closedir(dir);
+ bftw_freedir(cache, dir);
+ ++cache->capacity;
+ return ret;
+}
+
+/** Close a file descriptor, asynchronously if possible. */
+static int bftw_ioq_close(struct bftw_state *state, int fd) {
+ if (bftw_ioq_reserve(state) == 0) {
+ if (ioq_close(state->ioq, fd, NULL) == 0) {
+ return 0;
+ }
+ }
+
+ struct bftw_cache *cache = &state->cache;
+ int ret = xclose(fd);
+ ++cache->capacity;
+ return ret;
+}
+
+/** Close a file, asynchronously if possible. */
+static int bftw_close(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount == 0);
+
+ struct bfs_dir *dir = file->dir;
+ int fd = file->fd;
+
+ bftw_lru_remove(&state->cache, file);
+ file->dir = NULL;
+ file->fd = -1;
+
+ if (dir) {
+ return bftw_ioq_closedir(state, dir);
+ } else {
+ return bftw_ioq_close(state, fd);
+ }
+}
+
+/** Free an open directory. */
+static int bftw_unwrapdir(struct bftw_state *state, struct bftw_file *file) {
+ struct bfs_dir *dir = file->dir;
+ if (!dir) {
+ return 0;
+ }
+
+ struct bftw_cache *cache = &state->cache;
+
+ // Try to keep an open fd if any children exist
+ bool reffed = file->refcount > 1;
+ // Keep the fd the same if it's pinned
+ bool pinned = file->pincount > 0;
+
+#if BFS_USE_UNWRAPDIR
+ if (reffed || pinned) {
+ bfs_unwrapdir(dir);
+ bftw_freedir(cache, dir);
+ file->dir = NULL;
+ return 0;
+ }
+#else
+ if (pinned) {
+ return -1;
+ }
+#endif
+
+ if (!reffed) {
+ return bftw_close(state, file);
+ }
+
+ // Make room for dup()
+ bftw_cache_pin(cache, file);
+ int ret = bftw_cache_reserve(state);
+ bftw_cache_unpin(cache, file);
+ if (ret != 0) {
+ return ret;
+ }
+
+ int fd = dup_cloexec(file->fd);
+ if (fd < 0) {
+ return -1;
+ }
+ --cache->capacity;
+
+ file->dir = NULL;
+ file->fd = fd;
+ return bftw_ioq_closedir(state, dir);
+}
+
+/** Try to pin a file's parent. */
+static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) {
+ struct bftw_file *parent = file->parent;
+ if (!parent) {
+ return AT_FDCWD;
+ }
+
+ int fd = parent->fd;
+ if (fd < 0) {
+ // Don't confuse failures with AT_FDCWD
+ return (int)AT_FDCWD == -1 ? -2 : -1;
+ }
+
+ bftw_cache_pin(&state->cache, parent);
+ return fd;
+}
+
+/** Open a directory asynchronously. */
+static int bftw_ioq_opendir(struct bftw_state *state, struct bftw_file *file) {
+ struct bftw_cache *cache = &state->cache;
+
+ if (bftw_ioq_reserve(state) != 0) {
+ goto fail;
+ }
+
+ int dfd = bftw_pin_parent(state, file);
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
+ goto fail;
+ }
+
+ if (bftw_cache_reserve(state) != 0) {
+ goto unpin;
+ }
+
+ struct bfs_dir *dir = bftw_allocdir(cache, false);
+ if (!dir) {
+ goto unpin;
+ }
+
+ if (ioq_opendir(state->ioq, dir, dfd, file->name, state->dir_flags, file) != 0) {
+ goto free;
+ }
+
+ --cache->capacity;
+ return 0;
+
+free:
+ bftw_freedir(cache, dir);
+unpin:
+ bftw_unpin_parent(state, file, false);
+fail:
+ return -1;
+}
+
+/** Open a batch of directories asynchronously. */
+static void bftw_ioq_opendirs(struct bftw_state *state) {
+ while (bftw_queue_balanced(&state->dirq)) {
+ struct bftw_file *dir = bftw_queue_waiting(&state->dirq);
+ if (!dir) {
+ break;
+ }
+
+ if (bftw_ioq_opendir(state, dir) == 0) {
+ bftw_queue_detach(&state->dirq, dir, true);
+ } else {
+ break;
+ }
+ }
+}
+
+/** Push a directory onto the queue. */
+static void bftw_push_dir(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->type == BFS_DIR);
+ bftw_queue_push(&state->dirq, file);
+ bftw_ioq_opendirs(state);
+}
+
+/** Pop a file from a queue, then activate it. */
+static bool bftw_pop(struct bftw_state *state, struct bftw_queue *queue) {
+ if (queue->size == 0) {
+ return false;
+ }
+
+ while (!bftw_queue_ready(queue) && queue->ioqueued > 0) {
+ bool block = true;
+ if (bftw_queue_waiting(queue) && state->nthreads == 1) {
+ // With only one background thread, balance the work
+ // between it and the main thread
+ block = false;
+ }
+
+ if (bftw_ioq_pop(state, block) < 0) {
+ break;
+ }
+ }
+
+ struct bftw_file *file = bftw_queue_pop(queue);
+ if (!file) {
+ return false;
+ }
+
+ while (file->ioqueued) {
+ bftw_ioq_pop(state, true);
+ }
+
+ state->file = file;
+ return true;
+}
+
+/** Pop a directory to read from the queue. */
+static bool bftw_pop_dir(struct bftw_state *state) {
+ bfs_assert(!state->file);
+
+ if (state->flags & BFTW_SORT) {
+ // Keep strict breadth-first order when sorting
+ if (state->strategy == BFTW_BFS && bftw_queue_ready(&state->fileq)) {
+ return false;
+ }
+ } else if (!bftw_queue_ready(&state->dirq)) {
+ // Don't block if we have files ready to visit
+ if (bftw_queue_ready(&state->fileq)) {
+ return false;
+ }
+ }
+
+ return bftw_pop(state, &state->dirq);
+}
+
+/** Figure out bfs_stat() flags. */
+static enum bfs_stat_flags bftw_stat_flags(const struct bftw_state *state, size_t depth) {
+ enum bftw_flags mask = BFTW_FOLLOW_ALL;
+ if (depth == 0) {
+ mask |= BFTW_FOLLOW_ROOTS;
+ }
+
+ if (state->flags & mask) {
+ return BFS_STAT_TRYFOLLOW;
+ } else {
+ return BFS_STAT_NOFOLLOW;
+ }
+}
+
+/** Check if a stat() call is necessary. */
+static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bfs_type type, const char *name) {
+ if (state->flags & BFTW_STAT) {
+ return true;
+ }
+
+ switch (type) {
+ case BFS_UNKNOWN:
+ return true;
+
+ case BFS_DIR:
+ return state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS);
+
+ case BFS_LNK:
+ if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) {
+ return true;
+ }
+ _fallthrough;
+
+ default:
+#if __linux__
+ if (state->mtab && bfs_might_be_mount(state->mtab, name)) {
+ return true;
+ }
+#endif
+ return false;
+ }
+}
+
+/** stat() a file asynchronously. */
+static int bftw_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
+ if (bftw_ioq_reserve(state) != 0) {
+ goto fail;
+ }
+
+ int dfd = bftw_pin_parent(state, file);
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
+ goto fail;
+ }
+
+ struct bftw_cache *cache = &state->cache;
+ struct bfs_stat *buf = arena_alloc(&cache->stat_bufs);
+ if (!buf) {
+ goto unpin;
+ }
+
+ enum bfs_stat_flags flags = bftw_stat_flags(state, file->depth);
+ if (ioq_stat(state->ioq, dfd, file->name, flags, buf, file) != 0) {
+ goto free;
+ }
+
+ return 0;
+
+free:
+ arena_free(&cache->stat_bufs, buf);
+unpin:
+ bftw_unpin_parent(state, file, false);
+fail:
+ return -1;
+}
+
+/** Check if we should stat() a file asynchronously. */
+static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
+ // POSIX wants the root paths to be processed in order
+ // See https://www.austingroupbugs.net/view.php?id=1859
+ if (file->depth == 0) {
+ return false;
+ }
+
+#ifdef S_IFWHT
+ // ioq_stat() does not do whiteout emulation like bftw_stat_impl()
+ if (file->type == BFS_WHT) {
+ return false;
+ }
+#endif
+
+ return bftw_must_stat(state, file->depth, file->type, file->name);
+}
+
+/** Call stat() on files that need it. */
+static void bftw_stat_files(struct bftw_state *state) {
+ while (true) {
+ struct bftw_file *file = bftw_queue_waiting(&state->fileq);
+ if (!file) {
+ break;
+ }
+
+ if (!bftw_should_ioq_stat(state, file)) {
+ bftw_queue_skip(&state->fileq, file);
+ continue;
+ }
+
+ if (!bftw_queue_balanced(&state->fileq)) {
+ break;
+ }
+
+ if (bftw_ioq_stat(state, file) == 0) {
+ bftw_queue_detach(&state->fileq, file, true);
+ } else {
+ break;
+ }
+ }
+}
+
+/** Push a file onto the queue. */
+static void bftw_push_file(struct bftw_state *state, struct bftw_file *file) {
+ bftw_queue_push(&state->fileq, file);
+ bftw_stat_files(state);
+}
+
+/** Pop a file to visit from the queue. */
+static bool bftw_pop_file(struct bftw_state *state) {
+ bfs_assert(!state->file);
+ return bftw_pop(state, &state->fileq);
+}
+
+/** 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 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;
+ }
+
+ // Try to find a common ancestor with the existing path
+ const struct bftw_file *ancestor = state->previous;
+ while (ancestor && ancestor->depth > file->depth) {
+ ancestor = ancestor->parent;
+ }
+
+ // Build the path backwards
+ if (name) {
+ bftw_prepend_path(state->path, nameoff, namelen, name);
+ }
+ while (file && file != ancestor) {
+ bftw_prepend_path(state->path, file->nameoff, file->namelen, file->name);
+
+ if (ancestor && ancestor->depth == file->depth) {
+ ancestor = ancestor->parent;
+ }
+ file = file->parent;
+ }
+
+ state->previous = state->file;
+ return 0;
+}
+
+/** Open a bftw_file as a directory. */
+static struct bfs_dir *bftw_file_opendir(struct bftw_state *state, struct bftw_file *file, const char *path) {
+ int fd = bftw_file_open(state, file, path);
+ if (fd < 0) {
+ return NULL;
+ }
+
+ struct bftw_cache *cache = &state->cache;
+ struct bfs_dir *dir = bftw_allocdir(cache, true);
+ if (!dir) {
+ return NULL;
+ }
+
+ if (bfs_opendir(dir, fd, NULL, state->dir_flags) != 0) {
+ bftw_freedir(cache, dir);
+ return NULL;
+ }
+
+ bftw_file_set_dir(cache, file, dir);
+ return dir;
+}
+
+/** Open the current directory. */
+static int bftw_opendir(struct bftw_state *state) {
+ bfs_assert(!state->dir);
+ bfs_assert(!state->de);
+
+ state->direrror = 0;
+
+ struct bftw_file *file = state->file;
+ state->dir = file->dir;
+ if (state->dir) {
+ goto pin;
+ }
+
+ if (bftw_build_path(state, NULL) != 0) {
+ return -1;
+ }
+
+ bftw_queue_rebalance(&state->dirq, false);
+
+ state->dir = bftw_file_opendir(state, file, state->path);
+ if (!state->dir) {
+ state->direrror = errno;
+ return 0;
+ }
+
+pin:
+ bftw_cache_pin(&state->cache, file);
+ return 0;
+}
+
+/** Read an entry from the current directory. */
+static int bftw_readdir(struct bftw_state *state) {
+ if (!state->dir) {
+ return -1;
+ }
+
+ int ret = bfs_readdir(state->dir, &state->de_storage);
+ if (ret > 0) {
+ state->de = &state->de_storage;
+ } else if (ret == 0) {
+ state->de = NULL;
+ } else {
+ state->de = NULL;
+ state->direrror = errno;
+ }
+
+ return ret;
+}
+
+/** Open a file if necessary. */
+static int bftw_ensure_open(struct bftw_state *state, struct bftw_file *file, const char *path) {
+ int ret = file->fd;
+
+ if (ret < 0) {
+ char *copy = strndup(path, file->nameoff + file->namelen);
+ if (!copy) {
+ return -1;
+ }
+
+ ret = bftw_file_open(state, file, copy);
+ free(copy);
+ }
+
+ return ret;
+}
+
+/** Initialize the buffers with data about the current path. */
+static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
+ struct bftw_file *file = state->file;
+ const struct bfs_dirent *de = state->de;
+
+ struct BFTW *ftwbuf = &state->ftwbuf;
+ ftwbuf->path = state->path;
+ ftwbuf->root = file ? file->root->name : ftwbuf->path;
+ ftwbuf->depth = 0;
+ 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);
+
+ struct bftw_file *parent = NULL;
+ if (de) {
+ parent = file;
+ ftwbuf->depth = file->depth + 1;
+ ftwbuf->type = de->type;
+ ftwbuf->nameoff = bftw_child_nameoff(file);
+ } else if (file) {
+ parent = file->parent;
+ ftwbuf->depth = file->depth;
+ ftwbuf->type = file->type;
+ ftwbuf->nameoff = file->nameoff;
+ bftw_stat_fill(&ftwbuf->stat_bufs, &file->stat_bufs);
+ }
+
+ if (parent) {
+ // Try to ensure the immediate parent is open, to avoid ENAMETOOLONG
+ if (bftw_ensure_open(state, parent, state->path) >= 0) {
+ ftwbuf->at_fd = parent->fd;
+ ftwbuf->at_path += ftwbuf->nameoff;
+ } else {
+ ftwbuf->error = errno;
+ }
+ }
+
+ if (ftwbuf->depth == 0) {
+ // Compute the name offset for root paths like "foo/bar"
+ ftwbuf->nameoff = xbaseoff(ftwbuf->path);
+ }
+
+ ftwbuf->stat_flags = bftw_stat_flags(state, ftwbuf->depth);
+
+ if (ftwbuf->error != 0) {
+ ftwbuf->type = BFS_ERROR;
+ return;
+ }
+
+ const struct bfs_stat *statbuf = NULL;
+ if (bftw_must_stat(state, ftwbuf->depth, ftwbuf->type, ftwbuf->path + ftwbuf->nameoff)) {
+ statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (statbuf) {
+ ftwbuf->type = bfs_mode_to_type(statbuf->mode);
+ } else {
+ ftwbuf->type = BFS_ERROR;
+ ftwbuf->error = errno;
+ return;
+ }
+ }
+
+ if (ftwbuf->type == BFS_DIR && (state->flags & BFTW_DETECT_CYCLES)) {
+ for (const struct bftw_file *ancestor = parent; ancestor; ancestor = ancestor->parent) {
+ if (ancestor->dev == statbuf->dev && ancestor->ino == statbuf->ino) {
+ ftwbuf->type = BFS_ERROR;
+ ftwbuf->error = ELOOP;
+ ftwbuf->loopoff = ancestor->nameoff + ancestor->namelen;
+ return;
+ }
+ }
+ }
+}
+
+/** Check if the current file is a mount point. */
+static bool bftw_is_mount(struct bftw_state *state, const char *name) {
+ const struct bftw_file *file = state->file;
+ if (!file) {
+ return false;
+ }
+
+ const struct bftw_file *parent = name ? file : file->parent;
+ if (!parent) {
+ return false;
+ }
+
+ const struct BFTW *ftwbuf = &state->ftwbuf;
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ return statbuf && statbuf->dev != parent->dev;
+}
+
+/** Check if bfs_stat() was called from the main thread. */
+static bool bftw_stat_was_sync(const struct bftw_state *state, const struct bfs_stat *buf) {
+ return buf == &state->stat_buf || buf == &state->lstat_buf;
+}
+
+/** Invoke the callback. */
+static enum bftw_action bftw_call_back(struct bftw_state *state, const char *name, enum bftw_visit visit) {
+ if (visit == BFTW_POST && !(state->flags & BFTW_POST_ORDER)) {
+ return BFTW_PRUNE;
+ }
+
+ if (bftw_build_path(state, name) != 0) {
+ return BFTW_STOP;
+ }
+
+ const struct BFTW *ftwbuf = &state->ftwbuf;
+ bftw_init_ftwbuf(state, visit);
+
+ // Never give the callback BFS_ERROR unless BFTW_RECOVER is specified
+ if (ftwbuf->type == BFS_ERROR && !(state->flags & BFTW_RECOVER)) {
+ state->error = ftwbuf->error;
+ return BFTW_STOP;
+ }
+
+ enum bftw_action ret = BFTW_PRUNE;
+ if ((state->flags & BFTW_SKIP_MOUNTS) && bftw_is_mount(state, name)) {
+ goto done;
+ }
+
+ ret = state->callback(ftwbuf, state->ptr);
+ switch (ret) {
+ case BFTW_CONTINUE:
+ if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) {
+ ret = BFTW_PRUNE;
+ } else if (state->flags & BFTW_PRUNE_MOUNTS) {
+ if (bftw_is_mount(state, name)) {
+ ret = BFTW_PRUNE;
+ }
+ }
+ break;
+
+ case BFTW_PRUNE:
+ case BFTW_STOP:
+ break;
+
+ default:
+ state->error = EINVAL;
+ return BFTW_STOP;
+ }
+
+done:
+ if (state->fileq.flags & BFTW_QBALANCE) {
+ // Detect any main-thread stat() calls to rebalance the queue
+ const struct bfs_stat *buf = bftw_cached_stat(ftwbuf, BFS_STAT_FOLLOW);
+ const struct bfs_stat *lbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
+ if (bftw_stat_was_sync(state, buf) || bftw_stat_was_sync(state, lbuf)) {
+ bftw_queue_rebalance(&state->fileq, false);
+ }
+ }
+
+ return ret;
+}
+
+/**
+ * Flags controlling which files get visited when done with a directory.
+ */
+enum bftw_gc_flags {
+ /** Don't visit anything. */
+ BFTW_VISIT_NONE = 0,
+ /** Report directory errors. */
+ BFTW_VISIT_ERROR = 1 << 0,
+ /** Visit the file itself. */
+ BFTW_VISIT_FILE = 1 << 1,
+ /** Visit the file's ancestors. */
+ BFTW_VISIT_PARENTS = 1 << 2,
+ /** Visit both the file and its ancestors. */
+ BFTW_VISIT_ALL = BFTW_VISIT_ERROR | BFTW_VISIT_FILE | BFTW_VISIT_PARENTS,
+};
+
+/** Garbage collect the current file and its parents. */
+static int bftw_gc(struct bftw_state *state, enum bftw_gc_flags flags) {
+ int ret = 0;
+
+ struct bftw_file *file = state->file;
+ if (file) {
+ if (state->dir) {
+ bftw_cache_unpin(&state->cache, file);
+ }
+ if (file->dir) {
+ bftw_delayed_unwrap(state, file);
+ }
+ }
+ state->dir = NULL;
+ state->de = NULL;
+
+ if (state->direrror != 0) {
+ if (flags & BFTW_VISIT_ERROR) {
+ if (bftw_call_back(state, NULL, BFTW_PRE) == BFTW_STOP) {
+ ret = -1;
+ flags = 0;
+ }
+ } else {
+ state->error = state->direrror;
+ }
+ }
+ state->direrror = 0;
+
+ drain_slist (struct bftw_file, dead, &state->to_close, ready) {
+ bftw_unwrapdir(state, dead);
+ }
+
+ enum bftw_gc_flags visit = BFTW_VISIT_FILE;
+ while ((file = state->file)) {
+ if (--file->refcount > 0) {
+ state->file = NULL;
+ break;
+ }
+
+ if (flags & visit) {
+ if (bftw_call_back(state, NULL, BFTW_POST) == BFTW_STOP) {
+ ret = -1;
+ flags = 0;
+ }
+ }
+ visit = BFTW_VISIT_PARENTS;
+
+ struct bftw_file *parent = file->parent;
+ if (state->previous == file) {
+ state->previous = parent;
+ }
+ state->file = parent;
+
+ if (file->fd >= 0) {
+ bftw_close(state, file);
+ }
+ bftw_file_free(&state->cache, file);
+ }
+
+ return ret;
+}
+
+/** Sort a bftw_list by filename. */
+static void bftw_list_sort(struct bftw_list *list) {
+ if (!list->head || !list->head->next) {
+ return;
+ }
+
+ struct bftw_list left, right;
+ SLIST_INIT(&left);
+ SLIST_INIT(&right);
+
+ // Split
+ for (struct bftw_file *hare = list->head; hare && (hare = hare->next); hare = hare->next) {
+ struct bftw_file *tortoise = SLIST_POP(list);
+ SLIST_APPEND(&left, tortoise);
+ }
+ SLIST_EXTEND(&right, list);
+
+ // Recurse
+ bftw_list_sort(&left);
+ bftw_list_sort(&right);
+
+ // Merge
+ while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) {
+ struct bftw_file *lf = left.head;
+ struct bftw_file *rf = right.head;
+
+ if (strcoll(lf->name, rf->name) <= 0) {
+ SLIST_POP(&left);
+ SLIST_APPEND(list, lf);
+ } else {
+ SLIST_POP(&right);
+ SLIST_APPEND(list, rf);
+ }
+ }
+ SLIST_EXTEND(list, &left);
+ SLIST_EXTEND(list, &right);
+}
+
+/** Flush all the queue buffers. */
+static void bftw_flush(struct bftw_state *state) {
+ if (state->flags & BFTW_SORT) {
+ bftw_list_sort(&state->fileq.buffer);
+ }
+ bftw_queue_flush(&state->fileq);
+ bftw_stat_files(state);
+
+ bftw_queue_flush(&state->dirq);
+ bftw_ioq_opendirs(state);
+
+ if (state->ioq) {
+ ioq_submit(state->ioq);
+ }
+}
+
+/** Close the current directory. */
+static int bftw_closedir(struct bftw_state *state) {
+ if (bftw_gc(state, BFTW_VISIT_ALL) != 0) {
+ return -1;
+ }
+
+ bftw_flush(state);
+ return 0;
+}
+
+/** Fill file identity information from an ftwbuf. */
+static void bftw_save_ftwbuf(struct bftw_file *file, const struct BFTW *ftwbuf) {
+ file->type = ftwbuf->type;
+
+ const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, ftwbuf->stat_flags);
+ if (statbuf) {
+ file->dev = statbuf->dev;
+ file->ino = statbuf->ino;
+ }
+}
+
+/** Check if we should buffer a file instead of visiting it. */
+static bool bftw_buffer_file(const struct bftw_state *state, const struct bftw_file *file, const char *name) {
+ if (!name) {
+ // Already buffered
+ return false;
+ }
+
+ if (state->flags & BFTW_BUFFER) {
+ return true;
+ }
+
+ // If we need to call stat(), and can do it async, buffer this file
+ if (!state->ioq) {
+ return false;
+ }
+
+ if (!bftw_queue_balanced(&state->fileq)) {
+ // stat() would run synchronously anyway
+ return false;
+ }
+
+ size_t depth = file ? file->depth + 1 : 1;
+ enum bfs_type type = state->de ? state->de->type : BFS_UNKNOWN;
+ return bftw_must_stat(state, depth, type, name);
+}
+
+/** Visit and/or enqueue the current file. */
+static int bftw_visit(struct bftw_state *state, const char *name) {
+ struct bftw_cache *cache = &state->cache;
+ struct bftw_file *file = state->file;
+
+ if (bftw_buffer_file(state, file, name)) {
+ file = bftw_file_new(cache, file, name);
+ if (!file) {
+ state->error = errno;
+ return -1;
+ }
+
+ if (state->de) {
+ file->type = state->de->type;
+ }
+
+ bftw_push_file(state, file);
+ return 0;
+ }
+
+ switch (bftw_call_back(state, name, BFTW_PRE)) {
+ case BFTW_CONTINUE:
+ if (name) {
+ file = bftw_file_new(cache, state->file, name);
+ } else {
+ state->file = NULL;
+ }
+ if (!file) {
+ state->error = errno;
+ return -1;
+ }
+
+ bftw_save_ftwbuf(file, &state->ftwbuf);
+ bftw_stat_recycle(cache, file);
+ bftw_push_dir(state, file);
+ return 0;
+
+ case BFTW_PRUNE:
+ if (file && !name) {
+ return bftw_gc(state, BFTW_VISIT_PARENTS);
+ } else {
+ return 0;
+ }
+
+ default:
+ if (file && !name) {
+ bftw_gc(state, BFTW_VISIT_NONE);
+ }
+ return -1;
+ }
+}
+
+/** Drain a bftw_queue. */
+static void bftw_drain(struct bftw_state *state, struct bftw_queue *queue) {
+ bftw_queue_flush(queue);
+
+ while (bftw_pop(state, queue)) {
+ bftw_gc(state, BFTW_VISIT_NONE);
+ }
+}
+
+/**
+ * Dispose of the bftw() state.
+ *
+ * @return
+ * The bftw() return value.
+ */
+static int bftw_state_destroy(struct bftw_state *state) {
+ dstrfree(state->path);
+
+ struct ioq *ioq = state->ioq;
+ if (ioq) {
+ ioq_cancel(ioq);
+ while (bftw_ioq_pop(state, true) >= 0);
+ state->ioq = NULL;
+ }
+
+ bftw_gc(state, BFTW_VISIT_NONE);
+ bftw_drain(state, &state->dirq);
+ bftw_drain(state, &state->fileq);
+
+ ioq_destroy(ioq);
+
+ bftw_cache_destroy(&state->cache);
+
+ errno = state->error;
+ return state->error ? -1 : 0;
+}
+
+/**
+ * Shared implementation for all search strategies.
+ */
+static int bftw_impl(struct bftw_state *state) {
+ for (size_t i = 0; i < state->npaths; ++i) {
+ if (bftw_visit(state, state->paths[i]) != 0) {
+ return -1;
+ }
+ }
+ bftw_flush(state);
+
+ while (true) {
+ while (bftw_pop_dir(state)) {
+ if (bftw_opendir(state) != 0) {
+ return -1;
+ }
+ while (bftw_readdir(state) > 0) {
+ if (bftw_visit(state, state->de->name) != 0) {
+ return -1;
+ }
+ }
+ if (bftw_closedir(state) != 0) {
+ return -1;
+ }
+ }
+
+ if (!bftw_pop_file(state)) {
+ break;
+ }
+ if (bftw_visit(state, NULL) != 0) {
+ return -1;
+ }
+ bftw_flush(state);
+ }
+
+ return 0;
+}
+
+/**
+ * bftw() implementation for simple breadth-/depth-first search.
+ */
+static int bftw_walk(const struct bftw_args *args) {
+ struct bftw_state state;
+ if (bftw_state_init(&state, args) != 0) {
+ return -1;
+ }
+
+ bftw_impl(&state);
+ return bftw_state_destroy(&state);
+}
+
+/**
+ * Iterative deepening search state.
+ */
+struct bftw_ids_state {
+ /** Nested walk state. */
+ struct bftw_state nested;
+ /** The wrapped callback. */
+ bftw_callback *delegate;
+ /** The wrapped callback arguments. */
+ void *ptr;
+ /** Which visit this search corresponds to. */
+ enum bftw_visit visit;
+ /** Whether to override the bftw_visit. */
+ bool force_visit;
+ /** The current minimum depth (inclusive). */
+ size_t min_depth;
+ /** The current maximum depth (exclusive). */
+ size_t max_depth;
+ /** The set of pruned paths. */
+ struct trie pruned;
+ /** Whether the bottom has been found. */
+ bool bottom;
+};
+
+/** Iterative deepening callback function. */
+static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr) {
+ struct bftw_ids_state *state = ptr;
+
+ if (state->force_visit) {
+ struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
+ mutbuf->visit = state->visit;
+ }
+
+ if (ftwbuf->type == BFS_ERROR) {
+ if (ftwbuf->depth + 1 >= state->min_depth) {
+ return state->delegate(ftwbuf, state->ptr);
+ } else {
+ return BFTW_PRUNE;
+ }
+ }
+
+ if (ftwbuf->depth < state->min_depth) {
+ if (trie_find_str(&state->pruned, ftwbuf->path)) {
+ return BFTW_PRUNE;
+ } else {
+ return BFTW_CONTINUE;
+ }
+ } else if (state->visit == BFTW_POST) {
+ if (trie_find_str(&state->pruned, ftwbuf->path)) {
+ return BFTW_PRUNE;
+ }
+ }
+
+ enum bftw_action ret = BFTW_CONTINUE;
+ if (ftwbuf->visit == state->visit) {
+ ret = state->delegate(ftwbuf, state->ptr);
+ }
+
+ switch (ret) {
+ case BFTW_CONTINUE:
+ if (ftwbuf->type == BFS_DIR && ftwbuf->depth + 1 >= state->max_depth) {
+ state->bottom = false;
+ ret = BFTW_PRUNE;
+ }
+ break;
+
+ case BFTW_PRUNE:
+ if (ftwbuf->type == BFS_DIR) {
+ if (!trie_insert_str(&state->pruned, ftwbuf->path)) {
+ state->nested.error = errno;
+ ret = BFTW_STOP;
+ }
+ }
+ break;
+
+ case BFTW_STOP:
+ break;
+ }
+
+ return ret;
+}
+
+/** Initialize iterative deepening state. */
+static int bftw_ids_init(struct bftw_ids_state *state, const struct bftw_args *args) {
+ state->delegate = args->callback;
+ state->ptr = args->ptr;
+ state->visit = BFTW_PRE;
+ state->force_visit = false;
+ state->min_depth = 0;
+ state->max_depth = 1;
+ trie_init(&state->pruned);
+ state->bottom = false;
+
+ struct bftw_args ids_args = *args;
+ ids_args.callback = bftw_ids_callback;
+ ids_args.ptr = state;
+ ids_args.flags &= ~BFTW_POST_ORDER;
+ return bftw_state_init(&state->nested, &ids_args);
+}
+
+/** Finish an iterative deepening search. */
+static int bftw_ids_destroy(struct bftw_ids_state *state) {
+ trie_destroy(&state->pruned);
+ return bftw_state_destroy(&state->nested);
+}
+
+/**
+ * Iterative deepening bftw() wrapper.
+ */
+static int bftw_ids(const struct bftw_args *args) {
+ struct bftw_ids_state state;
+ if (bftw_ids_init(&state, args) != 0) {
+ return -1;
+ }
+
+ while (!state.bottom) {
+ state.bottom = true;
+
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
+ }
+
+ ++state.min_depth;
+ ++state.max_depth;
+ }
+
+ if (args->flags & BFTW_POST_ORDER) {
+ state.visit = BFTW_POST;
+ state.force_visit = true;
+
+ while (state.min_depth > 0) {
+ --state.max_depth;
+ --state.min_depth;
+
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
+ }
+ }
+ }
+
+done:
+ return bftw_ids_destroy(&state);
+}
+
+/**
+ * Exponential deepening bftw() wrapper.
+ */
+static int bftw_eds(const struct bftw_args *args) {
+ struct bftw_ids_state state;
+ if (bftw_ids_init(&state, args) != 0) {
+ return -1;
+ }
+
+ while (!state.bottom) {
+ state.bottom = true;
+
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
+ }
+
+ state.min_depth = state.max_depth;
+ state.max_depth *= 2;
+ }
+
+ if (args->flags & BFTW_POST_ORDER) {
+ state.visit = BFTW_POST;
+ state.min_depth = 0;
+ state.nested.flags |= BFTW_POST_ORDER;
+
+ bftw_impl(&state.nested);
+ }
+
+done:
+ return bftw_ids_destroy(&state);
+}
+
+int bftw(const struct bftw_args *args) {
+ switch (args->strategy) {
+ case BFTW_BFS:
+ case BFTW_DFS:
+ return bftw_walk(args);
+ case BFTW_IDS:
+ return bftw_ids(args);
+ case BFTW_EDS:
+ return bftw_eds(args);
+ }
+
+ errno = EINVAL;
+ return -1;
+}
diff --git a/bftw.h b/src/bftw.h
index 619d2a3..8b3ed7f 100644
--- a/bftw.h
+++ b/src/bftw.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A file-walking API based on nftw().
@@ -23,6 +10,7 @@
#include "dir.h"
#include "stat.h"
+
#include <stddef.h>
/**
@@ -39,12 +27,14 @@ enum bftw_visit {
* Cached bfs_stat() info for a file.
*/
struct bftw_stat {
- /** A pointer to the bfs_stat() buffer, if available. */
- const struct bfs_stat *buf;
- /** Storage for the bfs_stat() buffer, if needed. */
- struct bfs_stat storage;
- /** The cached error code, if any. */
- int error;
+ /** The bfs_stat(BFS_STAT_FOLLOW) buffer. */
+ const struct bfs_stat *stat_buf;
+ /** The bfs_stat(BFS_STAT_NOFOLLOW) buffer. */
+ const struct bfs_stat *lstat_buf;
+ /** The cached bfs_stat(BFS_STAT_FOLLOW) error. */
+ int stat_err;
+ /** The cached bfs_stat(BFS_STAT_NOFOLLOW) error. */
+ int lstat_err;
};
/**
@@ -65,8 +55,10 @@ struct BFTW {
/** The file type. */
enum bfs_type type;
- /** The errno that occurred, if type == BFTW_ERROR. */
+ /** The errno that occurred, if type == BFS_ERROR. */
int error;
+ /** For filesystem loops, the length of the loop prefix. */
+ size_t loopoff;
/** A parent file descriptor for the *at() family of calls. */
int at_fd;
@@ -75,19 +67,17 @@ struct BFTW {
/** Flags for bfs_stat(). */
enum bfs_stat_flags stat_flags;
- /** Cached bfs_stat() info for BFS_STAT_NOFOLLOW. */
- struct bftw_stat lstat_cache;
- /** Cached bfs_stat() info for BFS_STAT_FOLLOW. */
- struct bftw_stat stat_cache;
+ /** Cached bfs_stat() info. */
+ struct bftw_stat stat_bufs;
};
/**
* Get bfs_stat() info for a file encountered during bftw(), caching the result
* whenever possible.
*
- * @param ftwbuf
+ * @ftwbuf
* bftw() data for the file to stat.
- * @param flags
+ * @flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
* A pointer to a bfs_stat() buffer, or NULL if the call failed.
@@ -98,9 +88,9 @@ const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags
* Get bfs_stat() info for a file encountered during bftw(), if it has already
* been cached.
*
- * @param ftwbuf
+ * @ftwbuf
* bftw() data for the file to stat.
- * @param flags
+ * @flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
* A pointer to a bfs_stat() buffer, or NULL if no stat info is cached.
@@ -112,12 +102,12 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat
* whether to follow links. This function will avoid calling bfs_stat() if
* possible.
*
- * @param ftwbuf
+ * @ftwbuf
* bftw() data for the file to check.
- * @param flags
+ * @flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
- * The type of the file, or BFTW_ERROR if an error occurred.
+ * The type of the file, or BFS_ERROR if an error occurred.
*/
enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags);
@@ -136,9 +126,9 @@ enum bftw_action {
/**
* Callback function type for bftw().
*
- * @param ftwbuf
+ * @ftwbuf
* Data about the current file.
- * @param ptr
+ * @ptr
* The pointer passed to bftw().
* @return
* An action value.
@@ -167,6 +157,10 @@ enum bftw_flags {
BFTW_PRUNE_MOUNTS = 1 << 7,
/** Sort directory entries before processing them. */
BFTW_SORT = 1 << 8,
+ /** Read each directory into memory before processing its children. */
+ BFTW_BUFFER = 1 << 9,
+ /** Include whiteouts in the search results. */
+ BFTW_WHITEOUTS = 1 << 10,
};
/**
@@ -191,16 +185,22 @@ struct bftw_args {
const char **paths;
/** The number of starting paths. */
size_t npaths;
+
/** The callback to invoke. */
bftw_callback *callback;
/** A pointer which is passed to the callback. */
void *ptr;
+
/** The maximum number of file descriptors to keep open. */
int nopenfd;
+ /** The maximum number of threads to use. */
+ int nthreads;
+
/** Flags that control bftw() behaviour. */
enum bftw_flags flags;
/** The search strategy to use. */
enum bftw_strategy strategy;
+
/** The parsed mount table, if available. */
const struct bfs_mtab *mtab;
};
@@ -211,7 +211,7 @@ struct bftw_args {
* Like ftw(3) and nftw(3), this function walks a directory tree recursively,
* and invokes a callback for each path it encounters.
*
- * @param args
+ * @args
* The arguments that control the walk.
* @return
* 0 on success, or -1 on failure.
diff --git a/src/bit.h b/src/bit.h
new file mode 100644
index 0000000..5d6fb9d
--- /dev/null
+++ b/src/bit.h
@@ -0,0 +1,473 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Bits & bytes.
+ */
+
+#ifndef BFS_BIT_H
+#define BFS_BIT_H
+
+#include "bfs.h"
+
+#include <limits.h>
+#include <stdint.h>
+
+#if __has_include(<stdbit.h>)
+# include <stdbit.h>
+#endif
+
+// C23 polyfill: _WIDTH macros
+
+// The U*_MAX macros are of the form 2**n - 1, and we want to extract the n.
+// One way would be *_WIDTH = popcount(*_MAX). Alternatively, we can use
+// Hallvard B. Furuseth's technique from [1], which is shorter.
+//
+// [1]: https://groups.google.com/g/comp.lang.c/c/NfedEFBFJ0k
+
+// Let mask be of the form 2**m - 1, e.g. 0b111, and let n range over
+// [0b0, 0b1, 0b11, 0b111, 0b1111, ...]. Then we have
+//
+// n % 0b111
+// == [0b0, 0b1, 0b11, 0b0, 0b1, 0b11, ...]
+// n / (n % 0b111 + 1)
+// == [0b0 (x3), 0b111 (x3), 0b111111 (x3), ...]
+// n / (n % 0b111 + 1) / 0b111
+// == [0b0 (x3), 0b1 (x3), 0b1001 (x3), 0b1001001 (x3), ...]
+// n / (n % 0b111 + 1) / 0b111 % 0b111
+// == [0 (x3), 1 (x3), 2 (x3), ...]
+// == UMAX_CHUNK(n, 0b111)
+#define UMAX_CHUNK(n, mask) (n / (n % mask + 1) / mask % mask)
+
+// 8 * UMAX_CHUNK(n, 255) gives [0 (x8), 8 (x8), 16 (x8), ...]. To that we add
+// [0, 1, 2, ..., 6, 7, 0, 1, ...], which we get from a linear interpolation on
+// n % 255:
+//
+// n % 255
+// == [0, 1, 3, 7, 15, 31, 63, 127, 0, ...]
+// 86 / (n % 255 + 12)
+// == [7, 6, 5, 4, 3, 2, 1, 0, 7, ...]
+#define UMAX_INTERP(n) (7 - 86 / (n % 255 + 12))
+
+#define UMAX_WIDTH(n) (8 * UMAX_CHUNK(n, 255) + UMAX_INTERP(n))
+
+#ifndef CHAR_WIDTH
+# define CHAR_WIDTH CHAR_BIT
+#endif
+
+// See https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
+
+#ifndef USHRT_WIDTH
+# ifdef __SHRT_WIDTH__
+# define USHRT_WIDTH __SHRT_WIDTH__
+# else
+# define USHRT_WIDTH UMAX_WIDTH(USHRT_MAX)
+# endif
+#endif
+
+#ifndef UINT_WIDTH
+# ifdef __INT_WIDTH__
+# define UINT_WIDTH __INT_WIDTH__
+# else
+# define UINT_WIDTH UMAX_WIDTH(UINT_MAX)
+# endif
+#endif
+
+#ifndef ULONG_WIDTH
+# ifdef __LONG_WIDTH__
+# define ULONG_WIDTH __LONG_WIDTH__
+# else
+# define ULONG_WIDTH UMAX_WIDTH(ULONG_MAX)
+# endif
+#endif
+
+#ifndef ULLONG_WIDTH
+# ifdef __LONG_LONG_WIDTH__
+# define ULLONG_WIDTH __LONG_LONG_WIDTH__
+# elif defined(__LLONG_WIDTH__) // Clang
+# define ULLONG_WIDTH __LLONG_WIDTH__
+# else
+# define ULLONG_WIDTH UMAX_WIDTH(ULLONG_MAX)
+# endif
+#endif
+
+#ifndef SIZE_WIDTH
+# ifdef __SIZE_WIDTH__
+# define SIZE_WIDTH __SIZE_WIDTH__
+# else
+# define SIZE_WIDTH UMAX_WIDTH(SIZE_MAX)
+# endif
+#endif
+
+#ifndef PTRDIFF_WIDTH
+# ifdef __PTRDIFF_WIDTH__
+# define PTRDIFF_WIDTH __PTRDIFF_WIDTH__
+# else
+# define PTRDIFF_WIDTH UMAX_WIDTH(PTRDIFF_MAX)
+# endif
+#endif
+
+#ifndef UINTPTR_WIDTH
+# ifdef __INTPTR_WIDTH__
+# define UINTPTR_WIDTH __INTPTR_WIDTH__
+# else
+# define UINTPTR_WIDTH UMAX_WIDTH(UINTPTR_MAX)
+# endif
+#endif
+
+#ifndef UINTMAX_WIDTH
+# ifdef __INTMAX_WIDTH__
+# define UINTMAX_WIDTH __INTMAX_WIDTH__
+# else
+# define UINTMAX_WIDTH UMAX_WIDTH(UINTMAX_MAX)
+# endif
+#endif
+
+#ifndef UCHAR_WIDTH
+# define UCHAR_WIDTH CHAR_WIDTH
+#endif
+#ifndef SCHAR_WIDTH
+# define SCHAR_WIDTH CHAR_WIDTH
+#endif
+#ifndef SHRT_WIDTH
+# define SHRT_WIDTH USHRT_WIDTH
+#endif
+#ifndef INT_WIDTH
+# define INT_WIDTH UINT_WIDTH
+#endif
+#ifndef LONG_WIDTH
+# define LONG_WIDTH ULONG_WIDTH
+#endif
+#ifndef LLONG_WIDTH
+# define LLONG_WIDTH ULLONG_WIDTH
+#endif
+#ifndef INTPTR_WIDTH
+# define INTPTR_WIDTH UINTPTR_WIDTH
+#endif
+#ifndef INTMAX_WIDTH
+# define INTMAX_WIDTH UINTMAX_WIDTH
+#endif
+
+// N3022 polyfill: byte order
+
+#ifdef __STDC_ENDIAN_LITTLE__
+# define ENDIAN_LITTLE __STDC_ENDIAN_LITTLE__
+#elif defined(__ORDER_LITTLE_ENDIAN__)
+# define ENDIAN_LITTLE __ORDER_LITTLE_ENDIAN__
+#else
+# define ENDIAN_LITTLE 1234
+#endif
+
+#ifdef __STDC_ENDIAN_BIG__
+# define ENDIAN_BIG __STDC_ENDIAN_BIG__
+#elif defined(__ORDER_BIG_ENDIAN__)
+# define ENDIAN_BIG __ORDER_BIG_ENDIAN__
+#else
+# define ENDIAN_BIG 4321
+#endif
+
+#ifdef __STDC_ENDIAN_NATIVE__
+# define ENDIAN_NATIVE __STDC_ENDIAN_NATIVE__
+#elif defined(__BYTE_ORDER__)
+# define ENDIAN_NATIVE __BYTE_ORDER__
+#else
+# define ENDIAN_NATIVE 0
+#endif
+
+#if __GNUC__
+# define bswap_u16 __builtin_bswap16
+# define bswap_u32 __builtin_bswap32
+# define bswap_u64 __builtin_bswap64
+#else
+
+static inline uint16_t bswap_u16(uint16_t n) {
+ return (n << 8) | (n >> 8);
+}
+
+static inline uint32_t bswap_u32(uint32_t n) {
+ return ((uint32_t)bswap_u16(n) << 16) | bswap_u16(n >> 16);
+}
+
+static inline uint64_t bswap_u64(uint64_t n) {
+ return ((uint64_t)bswap_u32(n) << 32) | bswap_u32(n >> 32);
+}
+
+#endif
+
+static inline uint8_t bswap_u8(uint8_t n) {
+ return n;
+}
+
+#if UCHAR_WIDTH == 8
+# define bswap_uc bswap_u8
+#endif
+
+#if USHRT_WIDTH == 16
+# define bswap_us bswap_u16
+#elif USHRT_WIDTH == 32
+# define bswap_us bswap_u32
+#elif USHRT_WIDTH == 64
+# define bswap_us bswap_u64
+#endif
+
+#if UINT_WIDTH == 16
+# define bswap_ui bswap_u16
+#elif UINT_WIDTH == 32
+# define bswap_ui bswap_u32
+#elif UINT_WIDTH == 64
+# define bswap_ui bswap_u64
+#endif
+
+#if ULONG_WIDTH == 32
+# define bswap_ul bswap_u32
+#elif ULONG_WIDTH == 64
+# define bswap_ul bswap_u64
+#endif
+
+#if ULLONG_WIDTH == 64
+# define bswap_ull bswap_u64
+#endif
+
+// Define an overload for each unsigned type
+#define UINT_OVERLOADS(macro) \
+ macro(unsigned char, _uc, UCHAR_WIDTH) \
+ macro(unsigned short, _us, USHRT_WIDTH) \
+ macro(unsigned int, _ui, UINT_WIDTH) \
+ macro(unsigned long, _ul, ULONG_WIDTH) \
+ macro(unsigned long long, _ull, ULLONG_WIDTH)
+
+// Select an overload based on an unsigned integer type
+#define UINT_SELECT(n, name) \
+ _Generic((n), \
+ unsigned char: name##_uc, \
+ unsigned short: name##_us, \
+ unsigned int: name##_ui, \
+ unsigned long: name##_ul, \
+ unsigned long long: name##_ull)
+
+/**
+ * Reverse the byte order of an integer.
+ */
+#define bswap(n) UINT_SELECT(n, bswap)(n)
+
+#define LOAD8_LEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (i))
+#define LOAD8_BEU8(ptr, i, n) ((uint##n##_t)((const unsigned char *)ptr)[(i) / 8] << (n - (i) - 8))
+
+/** Load a little-endian 8-bit word. */
+static inline uint8_t load8_leu8(const void *ptr) {
+ return LOAD8_LEU8(ptr, 0, 8);
+}
+
+/** Load a big-endian 8-bit word. */
+static inline uint8_t load8_beu8(const void *ptr) {
+ return LOAD8_BEU8(ptr, 0, 8);
+}
+
+#define LOAD8_LEU16(ptr, i, n) (LOAD8_LEU8(ptr, i, n) | LOAD8_LEU8(ptr, i + 8, n))
+#define LOAD8_BEU16(ptr, i, n) (LOAD8_BEU8(ptr, i, n) | LOAD8_BEU8(ptr, i + 8, n))
+
+/** Load a little-endian 16-bit word. */
+static inline uint16_t load8_leu16(const void *ptr) {
+ return LOAD8_LEU16(ptr, 0, 16);
+}
+
+/** Load a big-endian 16-bit word. */
+static inline uint16_t load8_beu16(const void *ptr) {
+ return LOAD8_BEU16(ptr, 0, 16);
+}
+
+#define LOAD8_LEU32(ptr, i, n) (LOAD8_LEU16(ptr, i, n) | LOAD8_LEU16(ptr, i + 16, n))
+#define LOAD8_BEU32(ptr, i, n) (LOAD8_BEU16(ptr, i, n) | LOAD8_BEU16(ptr, i + 16, n))
+
+/** Load a little-endian 32-bit word. */
+static inline uint32_t load8_leu32(const void *ptr) {
+ return LOAD8_LEU32(ptr, 0, 32);
+}
+
+/** Load a big-endian 32-bit word. */
+static inline uint32_t load8_beu32(const void *ptr) {
+ return LOAD8_BEU32(ptr, 0, 32);
+}
+
+#define LOAD8_LEU64(ptr, i, n) (LOAD8_LEU32(ptr, i, n) | LOAD8_LEU32(ptr, i + 32, n))
+#define LOAD8_BEU64(ptr, i, n) (LOAD8_BEU32(ptr, i, n) | LOAD8_BEU32(ptr, i + 32, n))
+
+/** Load a little-endian 64-bit word. */
+static inline uint64_t load8_leu64(const void *ptr) {
+ return LOAD8_LEU64(ptr, 0, 64);
+}
+
+/** Load a big-endian 64-bit word. */
+static inline uint64_t load8_beu64(const void *ptr) {
+ return LOAD8_BEU64(ptr, 0, 64);
+}
+
+// C23 polyfill: bit utilities
+
+#if __STDC_VERSION_STDBIT_H__ >= C23
+# define count_ones stdc_count_ones
+# define count_zeros stdc_count_zeros
+# define leading_zeros stdc_leading_zeros
+# define leading_ones stdc_leading_ones
+# define trailing_zeros stdc_trailing_zeros
+# define trailing_ones stdc_trailing_ones
+# define first_leading_zero stdc_first_leading_zero
+# define first_leading_one stdc_first_leading_one
+# define first_trailing_zero stdc_first_trailing_zero
+# define first_trailing_one stdc_first_trailing_one
+# define has_single_bit stdc_has_single_bit
+# define bit_width stdc_bit_width
+# define bit_ceil stdc_bit_ceil
+# define bit_floor stdc_bit_floor
+#else
+
+#if __GNUC__
+
+// GCC provides builtins for unsigned {int,long,long long}, so promote char/short
+#define UINT_BUILTIN_uc(name) __builtin_##name
+#define UINT_BUILTIN_us(name) __builtin_##name
+#define UINT_BUILTIN_ui(name) __builtin_##name
+#define UINT_BUILTIN_ul(name) __builtin_##name##l
+#define UINT_BUILTIN_ull(name) __builtin_##name##ll
+#define UINT_BUILTIN(name, suffix) UINT_BUILTIN##suffix(name)
+
+#define BUILTIN_WIDTH_uc UINT_WIDTH
+#define BUILTIN_WIDTH_us UINT_WIDTH
+#define BUILTIN_WIDTH_ui UINT_WIDTH
+#define BUILTIN_WIDTH_ul ULONG_WIDTH
+#define BUILTIN_WIDTH_ull ULLONG_WIDTH
+#define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix
+
+#define COUNT_ONES(type, suffix, width) \
+ static inline unsigned int count_ones##suffix(type n) { \
+ return UINT_BUILTIN(popcount, suffix)(n); \
+ }
+
+#define LEADING_ZEROS(type, suffix, width) \
+ static inline unsigned int leading_zeros##suffix(type n) { \
+ return n \
+ ? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \
+ : width; \
+ }
+
+#define TRAILING_ZEROS(type, suffix, width) \
+ static inline unsigned int trailing_zeros##suffix(type n) { \
+ return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \
+ }
+
+#define FIRST_TRAILING_ONE(type, suffix, width) \
+ static inline unsigned int first_trailing_one##suffix(type n) { \
+ return UINT_BUILTIN(ffs, suffix)(n); \
+ }
+
+#else // !__GNUC__
+
+#define COUNT_ONES(type, suffix, width) \
+ static inline unsigned int count_ones##suffix(type n) { \
+ int ret; \
+ for (ret = 0; n; ++ret) { \
+ n &= n - 1; \
+ } \
+ return ret; \
+ }
+
+#define LEADING_ZEROS(type, suffix, width) \
+ static inline unsigned int leading_zeros##suffix(type n) { \
+ type bit = (type)1 << (width - 1); \
+ int ret; \
+ for (ret = 0; bit && !(n & bit); ++ret, bit >>= 1); \
+ return ret; \
+ }
+
+#define TRAILING_ZEROS(type, suffix, width) \
+ static inline unsigned int trailing_zeros##suffix(type n) { \
+ type bit = 1; \
+ int ret; \
+ for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \
+ return ret; \
+ }
+
+#define FIRST_TRAILING_ONE(type, suffix, width) \
+ static inline unsigned int first_trailing_one##suffix(type n) { \
+ return n ? trailing_zeros##suffix(n) + 1 : 0; \
+ }
+
+#endif // !__GNUC__
+
+UINT_OVERLOADS(COUNT_ONES)
+UINT_OVERLOADS(LEADING_ZEROS)
+UINT_OVERLOADS(TRAILING_ZEROS)
+UINT_OVERLOADS(FIRST_TRAILING_ONE)
+
+#define FIRST_LEADING_ONE(type, suffix, width) \
+ static inline unsigned int first_leading_one##suffix(type n) { \
+ return n ? leading_zeros##suffix(n) + 1 : 0; \
+ }
+
+#define HAS_SINGLE_BIT(type, suffix, width) \
+ static inline bool has_single_bit##suffix(type n) { \
+ /** Branchless n && !(n & (n - 1)) */ \
+ return n - 1 < (n ^ (n - 1)); \
+ }
+
+#define BIT_WIDTH(type, suffix, width) \
+ static inline unsigned int bit_width##suffix(type n) { \
+ return width - leading_zeros##suffix(n); \
+ }
+
+#define BIT_FLOOR(type, suffix, width) \
+ static inline type bit_floor##suffix(type n) { \
+ return n ? (type)1 << (bit_width##suffix(n) - 1) : 0; \
+ }
+
+#define BIT_CEIL(type, suffix, width) \
+ static inline type bit_ceil##suffix(type n) { \
+ return (type)1 << bit_width##suffix(n - !!n); \
+ }
+
+UINT_OVERLOADS(FIRST_LEADING_ONE)
+UINT_OVERLOADS(HAS_SINGLE_BIT)
+UINT_OVERLOADS(BIT_WIDTH)
+UINT_OVERLOADS(BIT_FLOOR)
+UINT_OVERLOADS(BIT_CEIL)
+
+#define count_ones(n) UINT_SELECT(n, count_ones)(n)
+#define count_zeros(n) UINT_SELECT(n, count_ones)(~(n))
+
+#define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n)
+#define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n))
+
+#define trailing_zeros(n) UINT_SELECT(n, trailing_zeros)(n)
+#define trailing_ones(n) UINT_SELECT(n, trailing_zeros)(~(n))
+
+#define first_leading_one(n) UINT_SELECT(n, first_leading_one)(n)
+#define first_leading_zero(n) UINT_SELECT(n, first_leading_one)(~(n))
+
+#define first_trailing_one(n) UINT_SELECT(n, first_trailing_one)(n)
+#define first_trailing_zero(n) UINT_SELECT(n, first_trailing_one)(~(n))
+
+#define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n)
+
+#define bit_width(n) UINT_SELECT(n, bit_width)(n)
+#define bit_floor(n) UINT_SELECT(n, bit_floor)(n)
+#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n)
+
+#endif // __STDC_VERSION_STDBIT_H__ < C23
+
+#define ROTATE_LEFT(type, suffix, width) \
+ static inline type rotate_left##suffix(type n, int c) { \
+ return (n << c) | (n >> ((width - c) % width)); \
+ }
+
+#define ROTATE_RIGHT(type, suffix, width) \
+ static inline type rotate_right##suffix(type n, int c) { \
+ return (n >> c) | (n << ((width - c) % width)); \
+ }
+
+UINT_OVERLOADS(ROTATE_LEFT)
+UINT_OVERLOADS(ROTATE_RIGHT)
+
+#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c)
+#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c)
+
+#endif // BFS_BIT_H
diff --git a/src/color.c b/src/color.c
new file mode 100644
index 0000000..a026831
--- /dev/null
+++ b/src/color.c
@@ -0,0 +1,1548 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "color.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "bftw.h"
+#include "diag.h"
+#include "dir.h"
+#include "dstring.h"
+#include "expr.h"
+#include "fsade.h"
+#include "stat.h"
+#include "trie.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+/**
+ * An escape sequence, which may contain embedded NUL bytes.
+ */
+struct esc_seq {
+ /** The length of the escape sequence. */
+ size_t len;
+ /** The escape sequence itself, without a terminating NUL. */
+ char seq[] _counted_by(len);
+};
+
+/**
+ * A colored file extension, like `*.tar=01;31`.
+ */
+struct ext_color {
+ /** Priority, to disambiguate case-sensitive and insensitive matches. */
+ size_t priority;
+ /** The escape sequence associated with this extension. */
+ struct esc_seq *esc;
+ /** The length of the extension to match. */
+ size_t len;
+ /** Whether the comparison should be case-sensitive. */
+ bool case_sensitive;
+ /** The extension to match (NUL-terminated). */
+ char ext[]; // _counted_by(len + 1);
+};
+
+struct colors {
+ /** esc_seq allocator. */
+ struct varena esc_arena;
+ /** ext_color allocator. */
+ struct varena ext_arena;
+
+ // Known dircolors keys
+
+ struct esc_seq *reset;
+ struct esc_seq *leftcode;
+ struct esc_seq *rightcode;
+ struct esc_seq *endcode;
+ struct esc_seq *clear_to_eol;
+
+ struct esc_seq *bold;
+ struct esc_seq *gray;
+ struct esc_seq *red;
+ struct esc_seq *green;
+ struct esc_seq *yellow;
+ struct esc_seq *blue;
+ struct esc_seq *magenta;
+ struct esc_seq *cyan;
+ struct esc_seq *white;
+
+ struct esc_seq *warning;
+ struct esc_seq *error;
+
+ struct esc_seq *normal;
+
+ struct esc_seq *file;
+ struct esc_seq *multi_hard;
+ struct esc_seq *executable;
+ struct esc_seq *capable;
+ struct esc_seq *setgid;
+ struct esc_seq *setuid;
+
+ struct esc_seq *directory;
+ struct esc_seq *sticky;
+ struct esc_seq *other_writable;
+ struct esc_seq *sticky_other_writable;
+
+ struct esc_seq *link;
+ struct esc_seq *orphan;
+ struct esc_seq *missing;
+ bool link_as_target;
+
+ struct esc_seq *blockdev;
+ struct esc_seq *chardev;
+ struct esc_seq *door;
+ struct esc_seq *pipe;
+ struct esc_seq *socket;
+
+ /** A mapping from color names (fi, di, ln, etc.) to struct fields. */
+ struct trie names;
+
+ /** Number of extensions. */
+ size_t ext_count;
+ /** Longest extension. */
+ size_t ext_len;
+ /** Case-sensitive extension trie. */
+ struct trie ext_trie;
+ /** Case-insensitive extension trie. */
+ struct trie iext_trie;
+};
+
+/** Allocate an escape sequence. */
+static struct esc_seq *new_esc(struct colors *colors, const char *seq, size_t len) {
+ struct esc_seq *esc = varena_alloc(&colors->esc_arena, len);
+ if (esc) {
+ esc->len = len;
+ memcpy(esc->seq, seq, len);
+ }
+ return esc;
+}
+
+/** Free an escape sequence. */
+static void free_esc(struct colors *colors, struct esc_seq *seq) {
+ varena_free(&colors->esc_arena, seq, seq->len);
+}
+
+/** Initialize a color in the table. */
+static int init_esc(struct colors *colors, const char *name, const char *value, struct esc_seq **field) {
+ struct esc_seq *esc = NULL;
+ if (value) {
+ esc = new_esc(colors, value, strlen(value));
+ if (!esc) {
+ return -1;
+ }
+ }
+
+ *field = esc;
+
+ return trie_set_str(&colors->names, name, field);
+}
+
+/** Check if an escape sequence is equal to a string. */
+static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) {
+ return esc->len == len && memcmp(esc->seq, str, len) == 0;
+}
+
+/** Get an escape sequence from the table. */
+static struct esc_seq **get_esc(const struct colors *colors, const char *name) {
+ return trie_get_str(&colors->names, name);
+}
+
+/** Append an escape sequence to a string. */
+static int cat_esc(dchar **dstr, const struct esc_seq *seq) {
+ return dstrxcat(dstr, seq->seq, seq->len);
+}
+
+/** Set a named escape sequence. */
+static int set_esc(struct colors *colors, const char *name, dchar *value) {
+ struct esc_seq **field = get_esc(colors, name);
+ if (!field) {
+ return 0;
+ }
+
+ if (*field) {
+ free_esc(colors, *field);
+ *field = NULL;
+ }
+
+ if (value) {
+ *field = new_esc(colors, value, dstrlen(value));
+ if (!*field) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/** Reverse a string, to turn suffix matches into prefix matches. */
+static void ext_reverse(char *ext, size_t len) {
+ for (size_t i = 0, j = len - 1; len && i < j; ++i, --j) {
+ char c = ext[i];
+ ext[i] = ext[j];
+ ext[j] = c;
+ }
+}
+
+/** Convert a string to lowercase for case-insensitive matching. */
+static void ext_tolower(char *ext, size_t len) {
+ for (size_t i = 0; i < len; ++i) {
+ char c = ext[i];
+
+ // What's internationalization? Doesn't matter, this is what
+ // GNU ls does. Luckily, since there's no standard C way to
+ // casefold. Not using tolower() here since it respects the
+ // current locale, which GNU ls doesn't do.
+ if (c >= 'A' && c <= 'Z') {
+ c += 'a' - 'A';
+ }
+
+ ext[i] = c;
+ }
+}
+
+/** Insert an extension into a trie. */
+static int insert_ext(struct trie *trie, struct ext_color *ext) {
+ // A later *.x should override any earlier *.x, *.y.x, etc.
+ struct trie_leaf *leaf;
+ while ((leaf = trie_find_postfix(trie, ext->ext))) {
+ trie_remove(trie, leaf);
+ }
+
+ size_t len = ext->len + 1;
+ return trie_set_mem(trie, ext->ext, len, ext);
+}
+
+/** Set the color for an extension. */
+static int set_ext(struct colors *colors, dchar *key, dchar *value) {
+ size_t len = dstrlen(key);
+
+ // Embedded NUL bytes in extensions can lead to a non-prefix-free
+ // set of strings, e.g. {".gz", "\0.gz"} would be transformed to
+ // {"zg.\0", "zg.\0\0"} (showing the implicit terminating NUL).
+ // Our trie implementation only supports prefix-free key sets, but
+ // luckily '\0' cannot appear in filenames so we can ignore them.
+ if (memchr(key, '\0', len)) {
+ return 0;
+ }
+
+ struct ext_color *ext = varena_alloc(&colors->ext_arena, len + 1);
+ if (!ext) {
+ return -1;
+ }
+
+ ext->priority = colors->ext_count++;
+ ext->len = len;
+ ext->case_sensitive = false;
+ ext->esc = new_esc(colors, value, dstrlen(value));
+ if (!ext->esc) {
+ goto fail;
+ }
+
+ memcpy(ext->ext, key, len + 1);
+
+ // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix()
+ ext_reverse(ext->ext, len);
+
+ // Insert the extension into the case-sensitive trie
+ if (insert_ext(&colors->ext_trie, ext) != 0) {
+ goto fail;
+ }
+
+ if (colors->ext_len < len) {
+ colors->ext_len = len;
+ }
+
+ return 0;
+
+fail:
+ if (ext->esc) {
+ free_esc(colors, ext->esc);
+ }
+ varena_free(&colors->ext_arena, ext, len + 1);
+ return -1;
+}
+
+/**
+ * The "smart case" algorithm.
+ *
+ * @ext
+ * The current extension being added.
+ * @iext
+ * The previous case-insensitive match, if any, for the same extension.
+ * @return
+ * Whether this extension should become case-sensitive.
+ */
+static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *iext) {
+ // This is the first case-insensitive occurrence of this extension, e.g.
+ //
+ // *.gz=01;31:*.tar.gz=01;33
+ if (!iext) {
+ return false;
+ }
+
+ // If the last version of this extension is already case-sensitive,
+ // this one should be too, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33
+ if (iext->case_sensitive) {
+ return true;
+ }
+
+ // Different case, but same value, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;31
+ if (esc_eq(iext->esc, ext->esc->seq, ext->esc->len)) {
+ return false;
+ }
+
+ // Different case, different value, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;33
+ return true;
+}
+
+/** Build the case-insensitive trie, after all extensions have been parsed. */
+static int build_iext_trie(struct colors *colors) {
+ // Find which extensions should be case-sensitive
+ for_trie (leaf, &colors->ext_trie) {
+ struct ext_color *ext = leaf->value;
+
+ // "Smart case": if the same extension is given with two different
+ // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive
+ ext_tolower(ext->ext, ext->len);
+
+ size_t len = ext->len + 1;
+ struct trie_leaf *ileaf = trie_insert_mem(&colors->iext_trie, ext->ext, len);
+ if (!ileaf) {
+ return -1;
+ }
+
+ struct ext_color *iext = ileaf->value;
+ if (ext_case_sensitive(ext, iext)) {
+ ext->case_sensitive = true;
+ iext->case_sensitive = true;
+ }
+
+ ileaf->value = ext;
+ }
+
+ // Rebuild the trie with only the case-insensitive ones
+ trie_clear(&colors->iext_trie);
+
+ for_trie (leaf, &colors->ext_trie) {
+ struct ext_color *ext = leaf->value;
+ if (ext->case_sensitive) {
+ continue;
+ }
+
+ // We already lowercased the extension above
+ if (insert_ext(&colors->iext_trie, ext) != 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/**
+ * Find a color by an extension.
+ */
+static const struct esc_seq *get_ext(const struct colors *colors, const char *filename, size_t name_len) {
+ size_t ext_len = colors->ext_len;
+ if (name_len < ext_len) {
+ ext_len = name_len;
+ }
+ const char *suffix = filename + name_len - ext_len;
+
+ char buf[256];
+ char *copy;
+ if (ext_len < sizeof(buf)) {
+ copy = memcpy(buf, suffix, ext_len);
+ copy[ext_len] = '\0';
+ } else {
+ copy = strndup(suffix, ext_len);
+ if (!copy) {
+ return NULL;
+ }
+ }
+
+ ext_reverse(copy, ext_len);
+ const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_trie, copy);
+ const struct ext_color *ext = leaf ? leaf->value : NULL;
+
+ ext_tolower(copy, ext_len);
+ const struct trie_leaf *ileaf = trie_find_prefix(&colors->iext_trie, copy);
+ const struct ext_color *iext = ileaf ? ileaf->value : NULL;
+
+ if (iext && (!ext || ext->priority < iext->priority)) {
+ ext = iext;
+ }
+
+ if (copy != buf) {
+ free(copy);
+ }
+
+ return ext ? ext->esc : NULL;
+}
+
+/**
+ * Parse a chunk of $LS_COLORS that may have escape sequences. The supported
+ * escapes are:
+ *
+ * \a, \b, \f, \n, \r, \t, \v:
+ * As in C
+ * \e:
+ * ESC (\033)
+ * \?:
+ * DEL (\177)
+ * \_:
+ * ' ' (space)
+ * \NNN:
+ * Octal
+ * \xNN:
+ * Hex
+ * ^C:
+ * Control character.
+ *
+ * See man dir_colors.
+ *
+ * @str
+ * A dstring to fill with the unescaped chunk.
+ * @value
+ * The value to parse.
+ * @end
+ * The character that marks the end of the chunk.
+ * @next[out]
+ * Will be set to the next chunk.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+static int unescape(char **str, const char *value, char end, const char **next) {
+ *next = NULL;
+
+ if (!value) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ if (dstresize(str, 0) != 0) {
+ return -1;
+ }
+
+ const char *i;
+ for (i = value; *i && *i != end; ++i) {
+ unsigned char c = 0;
+
+ switch (*i) {
+ case '\\':
+ switch (*++i) {
+ case 'a':
+ c = '\a';
+ break;
+ case 'b':
+ c = '\b';
+ break;
+ case 'e':
+ c = '\033';
+ break;
+ case 'f':
+ c = '\f';
+ break;
+ case 'n':
+ c = '\n';
+ break;
+ case 'r':
+ c = '\r';
+ break;
+ case 't':
+ c = '\t';
+ break;
+ case 'v':
+ c = '\v';
+ break;
+ case '?':
+ c = '\177';
+ break;
+ case '_':
+ c = ' ';
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ while (i[1] >= '0' && i[1] <= '7') {
+ c <<= 3;
+ c |= *i++ - '0';
+ }
+ c <<= 3;
+ c |= *i - '0';
+ break;
+
+ case 'X':
+ case 'x':
+ while (true) {
+ if (i[1] >= '0' && i[1] <= '9') {
+ c <<= 4;
+ c |= i[1] - '0';
+ } else if (i[1] >= 'A' && i[1] <= 'F') {
+ c <<= 4;
+ c |= i[1] - 'A' + 0xA;
+ } else if (i[1] >= 'a' && i[1] <= 'f') {
+ c <<= 4;
+ c |= i[1] - 'a' + 0xA;
+ } else {
+ break;
+ }
+ ++i;
+ }
+ break;
+
+ case '\0':
+ errno = EINVAL;
+ return -1;
+
+ default:
+ c = *i;
+ break;
+ }
+ break;
+
+ case '^':
+ switch (*++i) {
+ case '?':
+ c = '\177';
+ break;
+ case '\0':
+ errno = EINVAL;
+ return -1;
+ default:
+ // CTRL masks bits 6 and 7
+ c = *i & 0x1F;
+ break;
+ }
+ break;
+
+ default:
+ c = *i;
+ break;
+ }
+
+ if (dstrapp(str, c) != 0) {
+ return -1;
+ }
+ }
+
+ if (*i) {
+ *next = i + 1;
+ }
+
+ return 0;
+}
+
+/** Parse the GNU $LS_COLORS format. */
+static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) {
+ int ret = -1;
+ dchar *key = NULL;
+ dchar *value = NULL;
+
+ for (const char *chunk = ls_colors, *next; chunk; chunk = next) {
+ if (chunk[0] == '*') {
+ if (unescape(&key, chunk + 1, '=', &next) != 0) {
+ goto fail;
+ }
+ if (unescape(&value, next, ':', &next) != 0) {
+ goto fail;
+ }
+ if (set_ext(colors, key, value) != 0) {
+ goto fail;
+ }
+ } else {
+ const char *equals = strchr(chunk, '=');
+ if (!equals) {
+ break;
+ }
+
+ if (dstrxcpy(&key, chunk, equals - chunk) != 0) {
+ goto fail;
+ }
+ if (unescape(&value, equals + 1, ':', &next) != 0) {
+ goto fail;
+ }
+
+ // All-zero values should be treated like NULL, to fall
+ // back on any other relevant coloring for that file
+ dchar *esc = value;
+ if (strspn(value, "0") == dstrlen(value)
+ && strcmp(key, "rs") != 0
+ && strcmp(key, "lc") != 0
+ && strcmp(key, "rc") != 0
+ && strcmp(key, "ec") != 0) {
+ esc = NULL;
+ }
+
+ if (set_esc(colors, key, esc) != 0) {
+ goto fail;
+ }
+ }
+ }
+
+ ret = 0;
+fail:
+ dstrfree(value);
+ dstrfree(key);
+ return ret;
+}
+
+struct colors *parse_colors(void) {
+ struct colors *colors = ALLOC(struct colors);
+ if (!colors) {
+ return NULL;
+ }
+
+ VARENA_INIT(&colors->esc_arena, struct esc_seq, seq);
+ VARENA_INIT(&colors->ext_arena, struct ext_color, ext);
+ trie_init(&colors->names);
+ colors->ext_count = 0;
+ colors->ext_len = 0;
+ trie_init(&colors->ext_trie);
+ trie_init(&colors->iext_trie);
+
+ bool fail = false;
+
+ // From man console_codes
+
+ fail = fail || init_esc(colors, "rs", "0", &colors->reset);
+ fail = fail || init_esc(colors, "lc", "\033[", &colors->leftcode);
+ fail = fail || init_esc(colors, "rc", "m", &colors->rightcode);
+ fail = fail || init_esc(colors, "ec", NULL, &colors->endcode);
+ fail = fail || init_esc(colors, "cl", "\033[K", &colors->clear_to_eol);
+
+ fail = fail || init_esc(colors, "bld", "01;39", &colors->bold);
+ fail = fail || init_esc(colors, "gry", "01;30", &colors->gray);
+ fail = fail || init_esc(colors, "red", "01;31", &colors->red);
+ fail = fail || init_esc(colors, "grn", "01;32", &colors->green);
+ fail = fail || init_esc(colors, "ylw", "01;33", &colors->yellow);
+ fail = fail || init_esc(colors, "blu", "01;34", &colors->blue);
+ fail = fail || init_esc(colors, "mag", "01;35", &colors->magenta);
+ fail = fail || init_esc(colors, "cyn", "01;36", &colors->cyan);
+ fail = fail || init_esc(colors, "wht", "01;37", &colors->white);
+
+ fail = fail || init_esc(colors, "wrn", "01;33", &colors->warning);
+ fail = fail || init_esc(colors, "err", "01;31", &colors->error);
+
+ // Defaults from man dir_colors
+ // "" means fall back to ->normal
+
+ fail = fail || init_esc(colors, "no", NULL, &colors->normal);
+
+ fail = fail || init_esc(colors, "fi", "", &colors->file);
+ fail = fail || init_esc(colors, "mh", NULL, &colors->multi_hard);
+ fail = fail || init_esc(colors, "ex", "01;32", &colors->executable);
+ fail = fail || init_esc(colors, "ca", NULL, &colors->capable);
+ fail = fail || init_esc(colors, "sg", "30;43", &colors->setgid);
+ fail = fail || init_esc(colors, "su", "37;41", &colors->setuid);
+
+ fail = fail || init_esc(colors, "di", "01;34", &colors->directory);
+ fail = fail || init_esc(colors, "st", "37;44", &colors->sticky);
+ fail = fail || init_esc(colors, "ow", "34;42", &colors->other_writable);
+ fail = fail || init_esc(colors, "tw", "30;42", &colors->sticky_other_writable);
+
+ fail = fail || init_esc(colors, "ln", "01;36", &colors->link);
+ fail = fail || init_esc(colors, "or", NULL, &colors->orphan);
+ fail = fail || init_esc(colors, "mi", NULL, &colors->missing);
+ colors->link_as_target = false;
+
+ fail = fail || init_esc(colors, "bd", "01;33", &colors->blockdev);
+ fail = fail || init_esc(colors, "cd", "01;33", &colors->chardev);
+ fail = fail || init_esc(colors, "do", "01;35", &colors->door);
+ fail = fail || init_esc(colors, "pi", "33", &colors->pipe);
+ fail = fail || init_esc(colors, "so", "01;35", &colors->socket);
+
+ if (fail) {
+ goto fail;
+ }
+
+ if (parse_gnu_ls_colors(colors, getenv("LS_COLORS")) != 0) {
+ goto fail;
+ }
+ if (parse_gnu_ls_colors(colors, getenv("BFS_COLORS")) != 0) {
+ goto fail;
+ }
+ if (build_iext_trie(colors) != 0) {
+ goto fail;
+ }
+
+ if (colors->link && esc_eq(colors->link, "target", strlen("target"))) {
+ colors->link_as_target = true;
+ colors->link->len = 0;
+ }
+
+ // Pre-compute the reset escape sequence
+ if (!colors->endcode) {
+ dchar *ec = dstralloc(0);
+ if (!ec
+ || cat_esc(&ec, colors->leftcode) != 0
+ || cat_esc(&ec, colors->reset) != 0
+ || cat_esc(&ec, colors->rightcode) != 0
+ || set_esc(colors, "ec", ec) != 0) {
+ dstrfree(ec);
+ goto fail;
+ }
+ dstrfree(ec);
+ }
+
+ return colors;
+
+fail:
+ free_colors(colors);
+ return NULL;
+}
+
+void free_colors(struct colors *colors) {
+ if (!colors) {
+ return;
+ }
+
+ trie_destroy(&colors->iext_trie);
+ trie_destroy(&colors->ext_trie);
+ trie_destroy(&colors->names);
+ varena_destroy(&colors->ext_arena);
+ varena_destroy(&colors->esc_arena);
+
+ free(colors);
+}
+
+CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) {
+ CFILE *cfile = ALLOC(CFILE);
+ if (!cfile) {
+ return NULL;
+ }
+
+ cfile->buffer = dstralloc(128);
+ if (!cfile->buffer) {
+ free(cfile);
+ return NULL;
+ }
+
+ cfile->file = file;
+ cfile->fd = fileno(file);
+ cfile->need_reset = false;
+ cfile->close = close;
+
+ if (isatty(cfile->fd)) {
+ cfile->colors = colors;
+ } else {
+ cfile->colors = NULL;
+ }
+
+ return cfile;
+}
+
+int cfclose(CFILE *cfile) {
+ int ret = 0;
+
+ if (cfile) {
+ dstrfree(cfile->buffer);
+
+ if (cfile->close) {
+ ret = fclose(cfile->file);
+ }
+
+ free(cfile);
+ }
+
+ return ret;
+}
+
+bool colors_need_stat(const struct colors *colors) {
+ return colors->setuid || colors->setgid || colors->executable || colors->multi_hard
+ || colors->sticky_other_writable || colors->other_writable || colors->sticky;
+}
+
+/** A colorable file path. */
+struct cpath {
+ /** The full path to color. */
+ const char *path;
+ /** The basename offset of the last valid component. */
+ size_t nameoff;
+ /** The end offset of the last valid component. */
+ size_t valid;
+ /** The total length of the path. */
+ size_t len;
+
+ /** The bftw() buffer. */
+ const struct BFTW *ftwbuf;
+ /** bfs_stat() flags for the final component. */
+ enum bfs_stat_flags flags;
+ /** A bfs_stat() buffer, filled in when 0 < valid < len. */
+ struct bfs_stat statbuf;
+};
+
+/** Move the valid range of a path backwards. */
+static void cpath_retreat(struct cpath *cpath) {
+ const char *path = cpath->path;
+ size_t nameoff = cpath->nameoff;
+ size_t valid = cpath->valid;
+
+ if (valid > 0 && path[valid - 1] == '/') {
+ // Try without trailing slashes, to distinguish "notdir/" from "notdir"
+ do {
+ --valid;
+ } while (valid > 0 && path[valid - 1] == '/');
+
+ nameoff = valid;
+ while (nameoff > 0 && path[nameoff - 1] != '/') {
+ --nameoff;
+ }
+ } else {
+ // Remove the last component and try again
+ valid = nameoff;
+ }
+
+ cpath->nameoff = nameoff;
+ cpath->valid = valid;
+}
+
+/** Initialize a struct cpath. */
+static int cpath_init(struct cpath *cpath, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ // Normally there are only two components to color:
+ //
+ // nameoff valid
+ // v v
+ // path/to/filename
+ // --------+-------
+ // ${di} ${fi}
+ //
+ // Error cases also usually have two components:
+ //
+ // valid,
+ // nameoff
+ // v
+ // path/to/nowhere
+ // --------+------
+ // ${di} ${mi}
+ //
+ // But with ENOTDIR, there may be three:
+ //
+ // nameoff valid
+ // v v
+ // path/to/filename/nowhere
+ // --------+-------+-------
+ // ${di} ${fi} ${mi}
+
+ cpath->path = path;
+ cpath->len = strlen(path);
+ cpath->ftwbuf = ftwbuf;
+ cpath->flags = flags;
+
+ cpath->valid = cpath->len;
+ if (path == ftwbuf->path) {
+ cpath->nameoff = ftwbuf->nameoff;
+ } else {
+ cpath->nameoff = xbaseoff(path);
+ }
+
+ if (bftw_type(ftwbuf, flags) != BFS_ERROR) {
+ return 0;
+ }
+
+ cpath_retreat(cpath);
+
+ // Find the base path. For symlinks like
+ //
+ // path/to/symlink -> nested/file
+ //
+ // this will be something like
+ //
+ // path/to/nested/file
+ int at_fd = AT_FDCWD;
+ dchar *at_path = NULL;
+ if (path == ftwbuf->path) {
+ if (ftwbuf->depth > 0) {
+ // The parent must have existed to get here
+ return 0;
+ }
+ } else {
+ // We're in print_link_target(), so resolve relative to the link's parent directory
+ at_fd = ftwbuf->at_fd;
+ if (at_fd == (int)AT_FDCWD && path[0] != '/') {
+ at_path = dstrxdup(ftwbuf->path, ftwbuf->nameoff);
+ if (!at_path) {
+ return -1;
+ }
+ }
+ }
+
+ if (!at_path) {
+ at_path = dstralloc(cpath->valid);
+ if (!at_path) {
+ return -1;
+ }
+ }
+ if (dstrxcat(&at_path, path, cpath->valid) != 0) {
+ dstrfree(at_path);
+ return -1;
+ }
+
+ size_t at_off = dstrlen(at_path) - cpath->valid;
+
+ // Find the longest valid path prefix
+ while (cpath->valid > 0) {
+ if (bfs_stat(at_fd, at_path, BFS_STAT_FOLLOW, &cpath->statbuf) == 0) {
+ break;
+ }
+
+ cpath_retreat(cpath);
+ dstrshrink(at_path, at_off + cpath->valid);
+ }
+
+ dstrfree(at_path);
+ return 0;
+}
+
+/** Get the bfs_stat() buffer for the last valid component. */
+static const struct bfs_stat *cpath_stat(const struct cpath *cpath) {
+ if (cpath->valid == cpath->len) {
+ return bftw_stat(cpath->ftwbuf, cpath->flags);
+ } else {
+ return &cpath->statbuf;
+ }
+}
+
+/** Check if a path has non-trivial capabilities. */
+static bool cpath_has_capabilities(const struct cpath *cpath) {
+ if (cpath->valid == cpath->len) {
+ return bfs_check_capabilities(cpath->ftwbuf) > 0;
+ } else {
+ // TODO: implement capability checks for arbitrary paths
+ return false;
+ }
+}
+
+/** Check if a symlink is broken. */
+static bool cpath_is_broken(const struct cpath *cpath) {
+ if (cpath->valid < cpath->len) {
+ // A valid parent can't be a broken link
+ return false;
+ }
+
+ const struct BFTW *ftwbuf = cpath->ftwbuf;
+ if (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW) {
+ return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, F_OK) != 0;
+ } else {
+ // A link encountered with BFS_STAT_TRYFOLLOW must be broken
+ return true;
+ }
+}
+
+/** Get the color for a file. */
+static const struct esc_seq *file_color(const struct colors *colors, const struct cpath *cpath) {
+ enum bfs_type type;
+ if (cpath->valid == cpath->len) {
+ type = bftw_type(cpath->ftwbuf, cpath->flags);
+ } else {
+ type = bfs_mode_to_type(cpath->statbuf.mode);
+ }
+
+ if (type == BFS_ERROR) {
+ goto error;
+ }
+
+ const struct bfs_stat *statbuf = NULL;
+ const struct esc_seq *color = NULL;
+
+ switch (type) {
+ case BFS_REG:
+ if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) {
+ statbuf = cpath_stat(cpath);
+ if (!statbuf) {
+ goto error;
+ }
+ }
+
+ if (colors->setuid && (statbuf->mode & 04000)) {
+ color = colors->setuid;
+ } else if (colors->setgid && (statbuf->mode & 02000)) {
+ color = colors->setgid;
+ } else if (colors->capable && cpath_has_capabilities(cpath)) {
+ color = colors->capable;
+ } else if (colors->executable && (statbuf->mode & 00111)) {
+ color = colors->executable;
+ } else if (colors->multi_hard && statbuf->nlink > 1) {
+ color = colors->multi_hard;
+ }
+
+ if (!color) {
+ const char *name = cpath->path + cpath->nameoff;
+ size_t namelen = cpath->valid - cpath->nameoff;
+ color = get_ext(colors, name, namelen);
+ }
+
+ if (!color) {
+ color = colors->file;
+ }
+
+ break;
+
+ case BFS_DIR:
+ if (colors->sticky_other_writable || colors->other_writable || colors->sticky) {
+ statbuf = cpath_stat(cpath);
+ if (!statbuf) {
+ goto error;
+ }
+ }
+
+ if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) {
+ color = colors->sticky_other_writable;
+ } else if (colors->other_writable && (statbuf->mode & 00002)) {
+ color = colors->other_writable;
+ } else if (colors->sticky && (statbuf->mode & 01000)) {
+ color = colors->sticky;
+ } else {
+ color = colors->directory;
+ }
+
+ break;
+
+ case BFS_LNK:
+ if (colors->orphan && cpath_is_broken(cpath)) {
+ color = colors->orphan;
+ } else {
+ color = colors->link;
+ }
+ break;
+
+ case BFS_BLK:
+ color = colors->blockdev;
+ break;
+ case BFS_CHR:
+ color = colors->chardev;
+ break;
+ case BFS_FIFO:
+ color = colors->pipe;
+ break;
+ case BFS_SOCK:
+ color = colors->socket;
+ break;
+ case BFS_DOOR:
+ color = colors->door;
+ break;
+
+ default:
+ break;
+ }
+
+ if (color && color->len == 0) {
+ color = colors->normal;
+ }
+
+ return color;
+
+error:
+ if (colors->missing) {
+ return colors->missing;
+ } else {
+ return colors->orphan;
+ }
+}
+
+/** Print an escape sequence chunk. */
+static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) {
+ return cat_esc(&cfile->buffer, esc);
+}
+
+/** Print an ANSI escape sequence. */
+static int print_esc(CFILE *cfile, const struct esc_seq *esc) {
+ if (!esc) {
+ return 0;
+ }
+
+ const struct colors *colors = cfile->colors;
+ if (esc != colors->reset) {
+ cfile->need_reset = true;
+ }
+
+ if (print_esc_chunk(cfile, cfile->colors->leftcode) != 0) {
+ return -1;
+ }
+ if (print_esc_chunk(cfile, esc) != 0) {
+ return -1;
+ }
+ if (print_esc_chunk(cfile, cfile->colors->rightcode) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Reset after an ANSI escape sequence. */
+static int print_reset(CFILE *cfile) {
+ if (!cfile->need_reset) {
+ return 0;
+ }
+ cfile->need_reset = false;
+
+ return print_esc_chunk(cfile, cfile->colors->endcode);
+}
+
+/** Print a shell-escaped string. */
+static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flags flags) {
+ return dstrnescat(&cfile->buffer, str, n, flags);
+}
+
+/** Print a string with an optional color. */
+static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) {
+ if (len == 0) {
+ return 0;
+ }
+
+ if (print_esc(cfile, esc) != 0) {
+ return -1;
+ }
+
+ // Don't let the string itself interfere with the colors
+ if (print_wordesc(cfile, str, len, WESC_TTY) != 0) {
+ return -1;
+ }
+
+ if (print_reset(cfile) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Print a path with colors. */
+static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ struct cpath cpath;
+ if (cpath_init(&cpath, path, ftwbuf, flags) != 0) {
+ return -1;
+ }
+
+ const struct colors *colors = cfile->colors;
+ const struct esc_seq *dirs_color = colors->directory;
+ const struct esc_seq *name_color = NULL;
+ const struct esc_seq *err_color = colors->missing;
+ if (!err_color) {
+ err_color = colors->orphan;
+ }
+
+ if (cpath.nameoff < cpath.valid) {
+ name_color = file_color(colors, &cpath);
+ if (name_color == dirs_color) {
+ cpath.nameoff = cpath.valid;
+ }
+ }
+
+ if (print_colored(cfile, dirs_color, path, cpath.nameoff) != 0) {
+ return -1;
+ }
+
+ const char *name = path + cpath.nameoff;
+ size_t name_len = cpath.valid - cpath.nameoff;
+ if (print_colored(cfile, name_color, name, name_len) != 0) {
+ return -1;
+ }
+
+ const char *tail = path + cpath.valid;
+ size_t tail_len = cpath.len - cpath.valid;
+ if (print_colored(cfile, err_color, tail, tail_len) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Print a file name with colors. */
+static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ size_t len = strlen(name);
+ const struct cpath cpath = {
+ .path = name,
+ .nameoff = 0,
+ .valid = len,
+ .len = len,
+ .ftwbuf = ftwbuf,
+ .flags = flags,
+ };
+
+ const struct esc_seq *esc = file_color(cfile->colors, &cpath);
+ return print_colored(cfile, esc, name, cpath.len);
+}
+
+/** Print the name of a file with the appropriate colors. */
+static int print_name(CFILE *cfile, const struct BFTW *ftwbuf) {
+ const char *name = ftwbuf->path + ftwbuf->nameoff;
+
+ const struct colors *colors = cfile->colors;
+ if (!colors) {
+ return dstrcat(&cfile->buffer, name);
+ }
+
+ enum bfs_stat_flags flags = ftwbuf->stat_flags;
+ if (colors->link_as_target && ftwbuf->type == BFS_LNK) {
+ flags = BFS_STAT_TRYFOLLOW;
+ }
+
+ return print_name_colored(cfile, name, ftwbuf, flags);
+}
+
+/** Print the path to a file with the appropriate colors. */
+static int print_path(CFILE *cfile, const struct BFTW *ftwbuf) {
+ const struct colors *colors = cfile->colors;
+ if (!colors) {
+ return dstrcat(&cfile->buffer, ftwbuf->path);
+ }
+
+ enum bfs_stat_flags flags = ftwbuf->stat_flags;
+ if (colors->link_as_target && ftwbuf->type == BFS_LNK) {
+ flags = BFS_STAT_TRYFOLLOW;
+ }
+
+ return print_path_colored(cfile, ftwbuf->path, ftwbuf, flags);
+}
+
+/** Print a link target with the appropriate colors. */
+static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
+ size_t len = statbuf ? statbuf->size : 0;
+
+ char *target = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
+ if (!target) {
+ return -1;
+ }
+
+ int ret;
+ if (cfile->colors) {
+ ret = print_path_colored(cfile, target, ftwbuf, BFS_STAT_FOLLOW);
+ } else {
+ ret = dstrcat(&cfile->buffer, target);
+ }
+
+ free(target);
+ return ret;
+}
+
+/** Format some colored output to the buffer. */
+_printf(2, 3)
+static int cbuff(CFILE *cfile, const char *format, ...);
+
+/** Print an expression's name, for diagnostics. */
+static int print_expr_name(CFILE *cfile, const struct bfs_expr *expr) {
+ switch (expr->kind) {
+ case BFS_FLAG:
+ return cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]);
+ case BFS_OPERATOR:
+ return cbuff(cfile, "${red}%pq${rs}", expr->argv[0]);
+ default:
+ return cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]);
+ }
+}
+
+/** Print an expression's args, for diagnostics. */
+static int print_expr_args(CFILE *cfile, const struct bfs_expr *expr) {
+ if (print_expr_name(cfile, expr) != 0) {
+ return -1;
+ }
+
+ for (size_t i = 1; i < expr->argc; ++i) {
+ if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/** Dump a parsed expression tree, for debugging. */
+static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) {
+ if (depth >= 2) {
+ return dstrcat(&cfile->buffer, "(...)");
+ }
+
+ if (!expr) {
+ return dstrcat(&cfile->buffer, "(null)");
+ }
+
+ if (dstrcat(&cfile->buffer, "(") != 0) {
+ return -1;
+ }
+
+ if (print_expr_args(cfile, expr) != 0) {
+ return -1;
+ }
+
+ if (verbose) {
+ double rate = 0.0, time = 0.0;
+ if (expr->evaluations) {
+ rate = 100.0 * expr->successes / expr->evaluations;
+ time = (1.0e9 * expr->elapsed.tv_sec + expr->elapsed.tv_nsec) / expr->evaluations;
+ }
+ if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]",
+ expr->successes, expr->evaluations, rate, time)) {
+ return -1;
+ }
+ }
+
+ int count = 0;
+ for_expr (child, expr) {
+ if (dstrcat(&cfile->buffer, " ") != 0) {
+ return -1;
+ }
+ if (++count >= 3) {
+ if (dstrcat(&cfile->buffer, "...") != 0) {
+ return -1;
+ }
+ break;
+ } else {
+ if (print_expr(cfile, child, verbose, depth + 1) != 0) {
+ return -1;
+ }
+ }
+ }
+
+ if (dstrcat(&cfile->buffer, ")") != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+_printf(2, 0)
+static int cvbuff(CFILE *cfile, const char *format, va_list args) {
+ const struct colors *colors = cfile->colors;
+
+ // Color specifier (e.g. ${blu}) state
+ struct esc_seq **esc;
+ const char *end;
+ size_t len;
+ char name[4];
+
+ for (const char *i = format; *i; ++i) {
+ size_t verbatim = strcspn(i, "%$");
+ if (dstrxcat(&cfile->buffer, i, verbatim) != 0) {
+ return -1;
+ }
+ i += verbatim;
+
+ switch (*i) {
+ case '%':
+ switch (*++i) {
+ case '%':
+ if (dstrapp(&cfile->buffer, '%') != 0) {
+ return -1;
+ }
+ break;
+
+ case 'c':
+ if (dstrapp(&cfile->buffer, va_arg(args, int)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'd':
+ if (dstrcatf(&cfile->buffer, "%d", va_arg(args, int)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'g':
+ if (dstrcatf(&cfile->buffer, "%g", va_arg(args, double)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 's':
+ if (dstrcat(&cfile->buffer, va_arg(args, const char *)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'z':
+ ++i;
+ if (*i != 'u') {
+ goto invalid;
+ }
+ if (dstrcatf(&cfile->buffer, "%zu", va_arg(args, size_t)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'p':
+ switch (*++i) {
+ case 'q':
+ if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_SHELL | WESC_TTY) != 0) {
+ return -1;
+ }
+ break;
+ case 'Q':
+ if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_TTY) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'F':
+ if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'P':
+ if (print_path(cfile, va_arg(args, const struct BFTW *)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'L':
+ if (print_link_target(cfile, va_arg(args, const struct BFTW *)) != 0) {
+ return -1;
+ }
+ break;
+
+ case 'e':
+ if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false, 0) != 0) {
+ return -1;
+ }
+ break;
+ case 'E':
+ if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true, 0) != 0) {
+ return -1;
+ }
+ break;
+ case 'x':
+ if (print_expr_args(cfile, va_arg(args, const struct bfs_expr *)) != 0) {
+ return -1;
+ }
+ break;
+ case 'X':
+ if (print_expr_name(cfile, va_arg(args, const struct bfs_expr *)) != 0) {
+ return -1;
+ }
+ break;
+
+ default:
+ goto invalid;
+ }
+
+ break;
+
+ default:
+ goto invalid;
+ }
+ break;
+
+ case '$':
+ switch (*++i) {
+ case '$':
+ if (dstrapp(&cfile->buffer, '$') != 0) {
+ return -1;
+ }
+ break;
+
+ case '{':
+ ++i;
+ end = strchr(i, '}');
+ if (!end) {
+ goto invalid;
+ }
+ if (!colors) {
+ i = end;
+ break;
+ }
+
+ len = end - i;
+ if (len >= sizeof(name)) {
+ goto invalid;
+ }
+ memcpy(name, i, len);
+ name[len] = '\0';
+
+ if (strcmp(name, "rs") == 0) {
+ if (print_reset(cfile) != 0) {
+ return -1;
+ }
+ } else {
+ esc = get_esc(colors, name);
+ if (!esc) {
+ goto invalid;
+ }
+ if (print_esc(cfile, *esc) != 0) {
+ return -1;
+ }
+ }
+
+ i = end;
+ break;
+
+ default:
+ goto invalid;
+ }
+ break;
+
+ default:
+ return 0;
+ }
+ }
+
+ return 0;
+
+invalid:
+ bfs_bug("Invalid format string '%s'", format);
+ errno = EINVAL;
+ return -1;
+}
+
+static int cbuff(CFILE *cfile, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ int ret = cvbuff(cfile, format, args);
+ va_end(args);
+ return ret;
+}
+
+int cvfprintf(CFILE *cfile, const char *format, va_list args) {
+ bfs_assert(dstrlen(cfile->buffer) == 0);
+
+ int ret = -1;
+ if (cvbuff(cfile, format, args) == 0) {
+ size_t len = dstrlen(cfile->buffer);
+ if (fwrite(cfile->buffer, 1, len, cfile->file) == len) {
+ ret = 0;
+ }
+ }
+
+ dstrshrink(cfile->buffer, 0);
+ return ret;
+}
+
+int cfprintf(CFILE *cfile, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ int ret = cvfprintf(cfile, format, args);
+ va_end(args);
+ return ret;
+}
+
+int cfreset(CFILE *cfile) {
+ const struct colors *colors = cfile->colors;
+ if (!colors) {
+ return 0;
+ }
+
+ const struct esc_seq *esc = colors->endcode;
+ size_t ret = xwrite(cfile->fd, esc->seq, esc->len);
+ return ret == esc->len ? 0 : -1;
+}
diff --git a/src/color.h b/src/color.h
new file mode 100644
index 0000000..aac8b33
--- /dev/null
+++ b/src/color.h
@@ -0,0 +1,120 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Utilities for colored output on ANSI terminals.
+ */
+
+#ifndef BFS_COLOR_H
+#define BFS_COLOR_H
+
+#include "bfs.h"
+#include "dstring.h"
+
+#include <stdio.h>
+
+/**
+ * A color scheme.
+ */
+struct colors;
+
+/**
+ * Parse the color table from the environment.
+ */
+struct colors *parse_colors(void);
+
+/**
+ * Check if stat() info is required to color a file correctly.
+ */
+bool colors_need_stat(const struct colors *colors);
+
+/**
+ * Free a color table.
+ */
+void free_colors(struct colors *colors);
+
+/**
+ * A file/stream with associated colors.
+ */
+typedef struct CFILE {
+ /** The underlying file/stream. */
+ FILE *file;
+ /** The color table to use, if any. */
+ const struct colors *colors;
+ /** A buffer for colored formatting. */
+ dchar *buffer;
+ /** Cached file descriptor number. */
+ int fd;
+ /** Whether the next ${rs} is actually necessary. */
+ bool need_reset;
+ /** Whether to close the underlying stream. */
+ bool close;
+} CFILE;
+
+/**
+ * Wrap an existing file into a colored stream.
+ *
+ * @file
+ * The underlying file.
+ * @colors
+ * The color table to use if file is a TTY.
+ * @close
+ * Whether to close the underlying stream when this stream is closed.
+ * @return
+ * A colored wrapper around file.
+ */
+CFILE *cfwrap(FILE *file, const struct colors *colors, bool close);
+
+/**
+ * Close a colored file.
+ *
+ * @cfile
+ * The colored file to close.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int cfclose(CFILE *cfile);
+
+/**
+ * Colored, formatted output.
+ *
+ * @cfile
+ * The colored stream to print to.
+ * @format
+ * A printf()-style format string, supporting these format specifiers:
+ *
+ * %c: A single character
+ * %d: An integer
+ * %g: A double
+ * %s: A string
+ * %zu: A size_t
+ * %pq: A shell-escaped string, like bash's printf %q
+ * %pQ: A TTY-escaped string.
+ * %pF: A colored file name, from a const struct BFTW * argument
+ * %pP: A colored file path, from a const struct BFTW * argument
+ * %pL: A colored link target, from a const struct BFTW * argument
+ * %pe: Dump a const struct bfs_expr *, for debugging.
+ * %pE: Dump a const struct bfs_expr * in verbose form, for debugging.
+ * %px: Print a const struct bfs_expr * with syntax highlighting.
+ * %pX: Print the name of a const struct bfs_expr *, without arguments.
+ * %%: A literal '%'
+ * ${cc}: Change the color to 'cc'
+ * $$: A literal '$'
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_printf(2, 3)
+int cfprintf(CFILE *cfile, const char *format, ...);
+
+/**
+ * cfprintf() variant that takes a va_list.
+ */
+_printf(2, 0)
+int cvfprintf(CFILE *cfile, const char *format, va_list args);
+
+/**
+ * Reset the TTY state when terminating abnormally (async-signal-safe).
+ */
+int cfreset(CFILE *cfile);
+
+#endif // BFS_COLOR_H
diff --git a/src/ctx.c b/src/ctx.c
new file mode 100644
index 0000000..05baa1d
--- /dev/null
+++ b/src/ctx.c
@@ -0,0 +1,295 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "ctx.h"
+
+#include "alloc.h"
+#include "bfstd.h"
+#include "color.h"
+#include "diag.h"
+#include "expr.h"
+#include "list.h"
+#include "mtab.h"
+#include "pwcache.h"
+#include "sighook.h"
+#include "stat.h"
+#include "trie.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+struct bfs_ctx *bfs_ctx_new(void) {
+ struct bfs_ctx *ctx = ZALLOC(struct bfs_ctx);
+ if (!ctx) {
+ return NULL;
+ }
+
+ SLIST_INIT(&ctx->expr_list);
+ ARENA_INIT(&ctx->expr_arena, struct bfs_expr);
+
+ ctx->maxdepth = INT_MAX;
+ ctx->flags = BFTW_RECOVER;
+ ctx->strategy = BFTW_BFS;
+ ctx->optlevel = 3;
+
+ ctx->threads = nproc();
+ if (ctx->threads > 8) {
+ // Not much speedup after 8 threads
+ ctx->threads = 8;
+ }
+
+ trie_init(&ctx->files);
+
+ ctx->umask = umask(0);
+ umask(ctx->umask);
+
+ if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) {
+ goto fail;
+ }
+ ctx->cur_nofile = ctx->orig_nofile;
+ ctx->raise_nofile = true;
+
+ ctx->users = bfs_users_new();
+ if (!ctx->users) {
+ goto fail;
+ }
+
+ ctx->groups = bfs_groups_new();
+ if (!ctx->groups) {
+ goto fail;
+ }
+
+ if (clock_gettime(CLOCK_REALTIME, &ctx->now) != 0) {
+ goto fail;
+ }
+
+ return ctx;
+
+fail:
+ bfs_ctx_free(ctx);
+ return NULL;
+}
+
+const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx) {
+ struct bfs_ctx *mut = (struct bfs_ctx *)ctx;
+
+ if (mut->mtab_error) {
+ errno = mut->mtab_error;
+ } else if (!mut->mtab) {
+ mut->mtab = bfs_mtab_parse();
+ if (!mut->mtab) {
+ mut->mtab_error = errno;
+ }
+ }
+
+ return mut->mtab;
+}
+
+/**
+ * An open file tracked by the bfs context.
+ */
+struct bfs_ctx_file {
+ /** The file itself. */
+ CFILE *cfile;
+ /** The path to the file (for diagnostics). */
+ const char *path;
+ /** Signal hook to send a reset escape sequence. */
+ struct sighook *hook;
+ /** Remembers I/O errors, to propagate them to the exit status. */
+ int error;
+};
+
+/** Call cfreset() on a tracked file. */
+static void cfreset_hook(int sig, siginfo_t *info, void *arg) {
+ cfreset(arg);
+}
+
+CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
+ struct bfs_stat sb;
+ if (bfs_stat(cfile->fd, NULL, 0, &sb) != 0) {
+ return NULL;
+ }
+
+ bfs_file_id id;
+ bfs_stat_id(&sb, &id);
+
+ struct trie_leaf *leaf = trie_insert_mem(&ctx->files, id, sizeof(id));
+ if (!leaf) {
+ return NULL;
+ }
+
+ struct bfs_ctx_file *ctx_file = leaf->value;
+ if (ctx_file) {
+ ctx_file->path = path;
+ return ctx_file->cfile;
+ }
+
+ leaf->value = ctx_file = ALLOC(struct bfs_ctx_file);
+ if (!ctx_file) {
+ goto fail;
+ }
+
+ ctx_file->cfile = cfile;
+ ctx_file->path = path;
+ ctx_file->error = 0;
+ ctx_file->hook = NULL;
+
+ if (cfile->colors) {
+ ctx_file->hook = atsigexit(cfreset_hook, cfile);
+ if (!ctx_file->hook) {
+ goto fail;
+ }
+ }
+
+ if (cfile != ctx->cout && cfile != ctx->cerr) {
+ ++ctx->nfiles;
+ }
+
+ return cfile;
+
+fail:
+ trie_remove(&ctx->files, leaf);
+ free(ctx_file);
+ return NULL;
+}
+
+void bfs_ctx_flush(const struct bfs_ctx *ctx) {
+ // Before executing anything, flush all open streams. This ensures that
+ // - the user sees everything relevant before an -ok[dir] prompt
+ // - output from commands is interleaved consistently with bfs
+ // - executed commands can rely on I/O from other bfs actions
+ for_trie (leaf, &ctx->files) {
+ struct bfs_ctx_file *ctx_file = leaf->value;
+ CFILE *cfile = ctx_file->cfile;
+ if (fflush(cfile->file) == 0) {
+ continue;
+ }
+
+ ctx_file->error = errno;
+ clearerr(cfile->file);
+
+ const char *path = ctx_file->path;
+ if (path) {
+ bfs_error(ctx, "%pq: %s.\n", path, errstr());
+ } else if (cfile == ctx->cout) {
+ bfs_error(ctx, "(standard output): %s.\n", errstr());
+ }
+ }
+
+ // Flush the user/group caches, in case the executed command edits the
+ // user/group tables
+ bfs_users_flush(ctx->users);
+ bfs_groups_flush(ctx->groups);
+}
+
+/** Flush a file and report any errors. */
+static int bfs_ctx_fflush(CFILE *cfile) {
+ int ret = 0, error = 0;
+ if (ferror(cfile->file)) {
+ ret = -1;
+ error = EIO;
+ }
+ if (fflush(cfile->file) != 0) {
+ ret = -1;
+ error = errno;
+ }
+
+ errno = error;
+ return ret;
+}
+
+/** Close a file tracked by the bfs context. */
+static int bfs_ctx_fclose(struct bfs_ctx *ctx, struct bfs_ctx_file *ctx_file) {
+ CFILE *cfile = ctx_file->cfile;
+
+ // Writes to stderr are allowed to fail silently, unless the same file
+ // was used by -fprint, -fls, etc.
+ bool silent = cfile == ctx->cerr && !ctx_file->path;
+ int ret = 0, error = 0;
+
+ if (ctx_file->error) {
+ // An error was previously reported during bfs_ctx_flush()
+ ret = -1;
+ error = ctx_file->error;
+ }
+
+ // Flush the file just before we remove the hook, to maximize the chance
+ // we leave the TTY in a good state
+ if (bfs_ctx_fflush(cfile) != 0) {
+ ret = -1;
+ error = errno;
+ }
+
+ sigunhook(ctx_file->hook);
+
+ // Close the CFILE, except for stdio streams, which are closed later
+ if (cfile != ctx->cout && cfile != ctx->cerr) {
+ if (cfclose(cfile) != 0) {
+ ret = -1;
+ error = errno;
+ }
+ }
+
+ if (silent) {
+ ret = 0;
+ }
+
+ if (ret != 0 && ctx->cerr) {
+ if (ctx_file->path) {
+ bfs_error(ctx, "%pq: %s.\n", ctx_file->path, xstrerror(error));
+ } else if (cfile == ctx->cout) {
+ bfs_error(ctx, "(standard output): %s.\n", xstrerror(error));
+ }
+ }
+
+ free(ctx_file);
+ return ret;
+}
+
+int bfs_ctx_free(struct bfs_ctx *ctx) {
+ int ret = 0;
+
+ if (ctx) {
+ CFILE *cout = ctx->cout;
+ CFILE *cerr = ctx->cerr;
+
+ bfs_mtab_free(ctx->mtab);
+
+ bfs_groups_free(ctx->groups);
+ bfs_users_free(ctx->users);
+
+ for_trie (leaf, &ctx->files) {
+ struct bfs_ctx_file *ctx_file = leaf->value;
+ if (bfs_ctx_fclose(ctx, ctx_file) != 0) {
+ ret = -1;
+ }
+ }
+ trie_destroy(&ctx->files);
+
+ cfclose(cout);
+ cfclose(cerr);
+ free_colors(ctx->colors);
+
+ for_slist (struct bfs_expr, expr, &ctx->expr_list, freelist) {
+ bfs_expr_clear(expr);
+ }
+ arena_destroy(&ctx->expr_arena);
+
+ for (size_t i = 0; i < ctx->npaths; ++i) {
+ free((char *)ctx->paths[i]);
+ }
+ free(ctx->paths);
+
+ free(ctx->kinds);
+ free(ctx->argv);
+ free(ctx);
+ }
+
+ return ret;
+}
diff --git a/ctx.h b/src/ctx.h
index 02d296f..908338f 100644
--- a/ctx.h
+++ b/src/ctx.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* bfs execution context.
@@ -21,50 +8,43 @@
#ifndef BFS_CTX_H
#define BFS_CTX_H
+#include "alloc.h"
#include "bftw.h"
+#include "diag.h"
+#include "expr.h"
#include "trie.h"
-#include <stdbool.h>
+
+#include <stddef.h>
#include <sys/resource.h>
+#include <sys/types.h>
+#include <time.h>
-/**
- * Various debugging flags.
- */
-enum debug_flags {
- /** Print cost estimates. */
- DEBUG_COST = 1 << 0,
- /** Print executed command details. */
- DEBUG_EXEC = 1 << 1,
- /** Print optimization details. */
- DEBUG_OPT = 1 << 2,
- /** Print rate information. */
- DEBUG_RATES = 1 << 3,
- /** Trace the filesystem traversal. */
- DEBUG_SEARCH = 1 << 4,
- /** Trace all stat() calls. */
- DEBUG_STAT = 1 << 5,
- /** Print the parse tree. */
- DEBUG_TREE = 1 << 6,
- /** All debug flags. */
- DEBUG_ALL = (1 << 7) - 1,
-};
-
-/**
- * Convert a debug flag to a string.
- */
-const char *debug_flag_name(enum debug_flags flag);
+struct CFILE;
/**
* The execution context for bfs.
*/
struct bfs_ctx {
+ /** The number of command line arguments. */
+ size_t argc;
/** The unparsed command line arguments. */
char **argv;
+ /** The argument token kinds. */
+ enum bfs_kind *kinds;
+
/** The root paths. */
const char **paths;
+ /** The number of root paths. */
+ size_t npaths;
+
/** The main command line expression. */
- struct expr *expr;
+ struct bfs_expr *expr;
/** An expression for files to filter out. */
- struct expr *exclude;
+ struct bfs_expr *exclude;
+ /** A list of allocated expressions. */
+ struct bfs_exprs expr_list;
+ /** bfs_expr arena. */
+ struct arena expr_arena;
/** -mindepth option. */
int mindepth;
@@ -76,6 +56,8 @@ struct bfs_ctx {
/** bftw() search strategy. */
enum bftw_strategy strategy;
+ /** Threads (-j). */
+ int threads;
/** Optimization level (-O). */
int optlevel;
/** Debugging flags (-D). */
@@ -88,11 +70,18 @@ struct bfs_ctx {
bool status;
/** Whether to only return unique files (-unique). */
bool unique;
- /** Whether to print warnings (-warn/-nowarn). */
- bool warn;
/** Whether to only handle paths with xargs-safe characters (-X). */
bool xargs_safe;
+ /** Whether bfs was run interactively. */
+ bool interactive;
+ /** Whether to print warnings (-warn/-nowarn). */
+ bool warn;
+ /** Whether to report errors (-noerror). */
+ bool ignore_errors;
+ /** Whether any dangerous actions (-delete/-exec) are present. */
+ bool dangerous;
+
/** Color data. */
struct colors *colors;
/** The error that occurred parsing the color table, if any. */
@@ -102,10 +91,8 @@ struct bfs_ctx {
/** Colored stderr. */
struct CFILE *cerr;
- /** User table. */
+ /** User cache. */
struct bfs_users *users;
- /** The error that occurred parsing the user table, if any. */
- int users_error;
/** Group table. */
struct bfs_groups *groups;
/** The error that occurred parsing the group table, if any. */
@@ -121,10 +108,18 @@ struct bfs_ctx {
/** The number of files owned by the context. */
int nfiles;
- /** The initial RLIMIT_NOFILE soft limit. */
- rlim_t nofile_soft;
- /** The initial RLIMIT_NOFILE hard limit. */
- rlim_t nofile_hard;
+ /** The current file creation mask. */
+ mode_t umask;
+
+ /** The initial RLIMIT_NOFILE limits. */
+ struct rlimit orig_nofile;
+ /** The current RLIMIT_NOFILE limits. */
+ struct rlimit cur_nofile;
+ /** Whether the fd limit should be raised. */
+ bool raise_nofile;
+
+ /** The current time. */
+ struct timespec now;
};
/**
@@ -134,29 +129,9 @@ struct bfs_ctx {
struct bfs_ctx *bfs_ctx_new(void);
/**
- * Get the users table.
- *
- * @param ctx
- * The bfs context.
- * @return
- * The cached users table, or NULL on failure.
- */
-const struct bfs_users *bfs_ctx_users(const struct bfs_ctx *ctx);
-
-/**
- * Get the groups table.
- *
- * @param ctx
- * The bfs context.
- * @return
- * The cached groups table, or NULL on failure.
- */
-const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx);
-
-/**
* Get the mount table.
*
- * @param ctx
+ * @ctx
* The bfs context.
* @return
* The cached mount table, or NULL on failure.
@@ -166,11 +141,11 @@ const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx);
/**
* Deduplicate an opened file.
*
- * @param ctx
+ * @ctx
* The bfs context.
- * @param cfile
+ * @cfile
* The opened file.
- * @param path
+ * @path
* The path to the opened file (or NULL for standard streams).
* @return
* If the same file was opened previously, that file is returned. If cfile is a new file,
@@ -179,11 +154,19 @@ const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx);
struct CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, struct CFILE *cfile, const char *path);
/**
+ * Flush any caches for consistency with external processes.
+ *
+ * @ctx
+ * The bfs context.
+ */
+void bfs_ctx_flush(const struct bfs_ctx *ctx);
+
+/**
* Dump the parsed command line.
*
- * @param ctx
+ * @ctx
* The bfs context.
- * @param flag
+ * @flag
* The -D flag that triggered the dump.
*/
void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag);
@@ -191,7 +174,7 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag);
/**
* Free a bfs context.
*
- * @param ctx
+ * @ctx
* The context to free.
* @return
* 0 on success, -1 if any errors occurred.
diff --git a/src/diag.c b/src/diag.c
new file mode 100644
index 0000000..a86b060
--- /dev/null
+++ b/src/diag.c
@@ -0,0 +1,291 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "diag.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "color.h"
+#include "ctx.h"
+#include "dstring.h"
+#include "expr.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/**
+ * Print an error using dprintf() if possible, because it's more likely to be
+ * async-signal-safe in practice.
+ */
+#if BFS_HAS_DPRINTF
+# define veprintf(...) vdprintf(STDERR_FILENO, __VA_ARGS__)
+#else
+# define veprintf(...) vfprintf(stderr, __VA_ARGS__)
+#endif
+
+void bfs_diagf(const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ veprintf(format, args);
+ va_end(args);
+}
+
+_noreturn
+void bfs_abortf(const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ veprintf(format, args);
+ va_end(args);
+
+ abort();
+}
+
+const char *debug_flag_name(enum debug_flags flag) {
+ switch (flag) {
+ case DEBUG_COST:
+ return "cost";
+ case DEBUG_EXEC:
+ return "exec";
+ case DEBUG_OPT:
+ return "opt";
+ case DEBUG_RATES:
+ return "rates";
+ case DEBUG_SEARCH:
+ return "search";
+ case DEBUG_STAT:
+ return "stat";
+ case DEBUG_TREE:
+ return "tree";
+
+ case DEBUG_ALL:
+ break;
+ }
+
+ bfs_bug("Unrecognized debug flag");
+ return "???";
+}
+
+void bfs_perror(const struct bfs_ctx *ctx, const char *str) {
+ bfs_error(ctx, "%s: %s.\n", str, errstr());
+}
+
+void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_verror(ctx, format, args);
+ va_end(args);
+}
+
+bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bool ret = bfs_vwarning(ctx, format, args);
+ va_end(args);
+ return ret;
+}
+
+bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bool ret = bfs_vdebug(ctx, flag, format, args);
+ va_end(args);
+ return ret;
+}
+
+void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args) {
+ bfs_error_prefix(ctx);
+ cvfprintf(ctx->cerr, format, args);
+}
+
+bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args) {
+ if (bfs_warning_prefix(ctx)) {
+ cvfprintf(ctx->cerr, format, args);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args) {
+ if (bfs_debug_prefix(ctx, flag)) {
+ cvfprintf(ctx->cerr, format, args);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/** Get the command name without any leading directories. */
+static const char *bfs_cmd(const struct bfs_ctx *ctx) {
+ return ctx->argv[0] + xbaseoff(ctx->argv[0]);
+}
+
+void bfs_error_prefix(const struct bfs_ctx *ctx) {
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", bfs_cmd(ctx));
+}
+
+bool bfs_warning_prefix(const struct bfs_ctx *ctx) {
+ if (ctx->warn) {
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", bfs_cmd(ctx));
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) {
+ if (ctx->debug & flag) {
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", bfs_cmd(ctx), debug_flag_name(flag));
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/** Recursive part of highlight_expr(). */
+static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) {
+ if (!expr) {
+ return false;
+ }
+
+ bool ret = false;
+
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (&ctx->argv[i] == expr->argv) {
+ for (size_t j = 0; j < expr->argc; ++j) {
+ bfs_assert(i + j < ctx->argc);
+ args[i + j] = true;
+ ret = true;
+ }
+ break;
+ }
+ }
+
+ for_expr (child, expr) {
+ ret |= highlight_expr_recursive(ctx, child, args);
+ }
+
+ return ret;
+}
+
+/** Highlight an expression in the command line. */
+static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) {
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ args[i] = false;
+ }
+
+ return highlight_expr_recursive(ctx, expr, args);
+}
+
+/** Print a highlighted portion of the command line. */
+static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool args[], bool warning) {
+ if (warning) {
+ bfs_warning_prefix(ctx);
+ } else {
+ bfs_error_prefix(ctx);
+ }
+
+ dchar **argv = ZALLOC_ARRAY(dchar *, ctx->argc);
+ if (!argv) {
+ return;
+ }
+
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (dstrescat(&argv[i], ctx->argv[i], WESC_SHELL | WESC_TTY) != 0) {
+ goto done;
+ }
+ }
+
+ size_t max_argc = 0;
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (i > 0) {
+ cfprintf(ctx->cerr, " ");
+ }
+
+ if (args[i]) {
+ max_argc = i + 1;
+ cfprintf(ctx->cerr, "${bld}%s${rs}", argv[i]);
+ } else {
+ cfprintf(ctx->cerr, "%s", argv[i]);
+ }
+ }
+
+ cfprintf(ctx->cerr, "\n");
+
+ if (warning) {
+ bfs_warning_prefix(ctx);
+ } else {
+ bfs_error_prefix(ctx);
+ }
+
+ for (size_t i = 0; i < max_argc; ++i) {
+ if (i > 0) {
+ if (args[i - 1] && args[i]) {
+ cfprintf(ctx->cerr, "~");
+ } else {
+ cfprintf(ctx->cerr, " ");
+ }
+ }
+
+ if (args[i] && (i == 0 || !args[i - 1])) {
+ if (warning) {
+ cfprintf(ctx->cerr, "${wrn}");
+ } else {
+ cfprintf(ctx->cerr, "${err}");
+ }
+ }
+
+ size_t len = xstrwidth(argv[i]);
+ for (size_t j = 0; j < len; ++j) {
+ if (args[i]) {
+ cfprintf(ctx->cerr, "~");
+ } else {
+ cfprintf(ctx->cerr, " ");
+ }
+ }
+
+ if (args[i] && (i + 1 >= max_argc || !args[i + 1])) {
+ cfprintf(ctx->cerr, "${rs}");
+ }
+ }
+
+ cfprintf(ctx->cerr, "\n");
+
+done:
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ dstrfree(argv[i]);
+ }
+ free(argv);
+}
+
+void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]) {
+ bfs_argv_diag(ctx, args, false);
+}
+
+void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr) {
+ bool args[ctx->argc];
+ if (highlight_expr(ctx, expr, args)) {
+ bfs_argv_error(ctx, args);
+ }
+}
+
+bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]) {
+ if (!ctx->warn) {
+ return false;
+ }
+
+ bfs_argv_diag(ctx, args, true);
+ return true;
+}
+
+bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr) {
+ bool args[ctx->argc];
+ if (highlight_expr(ctx, expr, args)) {
+ return bfs_argv_warning(ctx, args);
+ }
+
+ return false;
+}
diff --git a/src/diag.h b/src/diag.h
new file mode 100644
index 0000000..645dbb1
--- /dev/null
+++ b/src/diag.h
@@ -0,0 +1,265 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Diagnostic messages.
+ */
+
+#ifndef BFS_DIAG_H
+#define BFS_DIAG_H
+
+#include "bfs.h"
+#include "bfstd.h"
+
+#include <stdarg.h>
+
+/**
+ * Wrap a diagnostic format string so it looks like
+ *
+ * bfs: func@src/file.c:0: Message
+ */
+#define BFS_DIAG_FORMAT_(format) \
+ ((format) ? "%s: %s@%s:%d: " format "%s" : "")
+
+/**
+ * Add arguments to match a BFS_DIAG_FORMAT string.
+ */
+#define BFS_DIAG_ARGS_(...) \
+ xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ "\n"
+
+/**
+ * Print a low-level diagnostic message to standard error.
+ */
+_printf(1, 2)
+void bfs_diagf(const char *format, ...);
+
+/**
+ * Unconditional diagnostic message.
+ */
+#define bfs_diag(...) \
+ bfs_diag_(__VA_ARGS__, )
+
+#define bfs_diag_(format, ...) \
+ bfs_diagf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__))
+
+/**
+ * Print a diagnostic message including the last error.
+ */
+#define bfs_ediag(...) \
+ bfs_ediag_(__VA_ARGS__, )
+
+#define bfs_ediag_(format, ...) \
+ bfs_diag_(format "%s%s", __VA_ARGS__ (sizeof("" format) > 1 ? ": " : ""), errstr(), )
+
+/**
+ * Print a message to standard error and abort.
+ */
+_cold
+_printf(1, 2)
+_noreturn
+void bfs_abortf(const char *format, ...);
+
+/**
+ * Unconditional abort with a message.
+ */
+#define bfs_abort(...) \
+ bfs_abort_(__VA_ARGS__, )
+
+#define bfs_abort_(format, ...) \
+ bfs_abortf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__))
+
+/**
+ * Abort with a message including the last error.
+ */
+#define bfs_eabort(...) \
+ bfs_eabort_(__VA_ARGS__, )
+
+#define bfs_eabort_(format, ...) \
+ ((format) ? bfs_abort_(format ": %s", __VA_ARGS__ errstr(), ) : (void)0)
+
+/**
+ * Abort in debug builds; no-op in release builds.
+ */
+#ifdef NDEBUG
+# define bfs_bug(...) ((void)0)
+# define bfs_ebug(...) ((void)0)
+#else
+# define bfs_bug bfs_abort
+# define bfs_ebug bfs_eabort
+#endif
+
+/**
+ * Get the default assertion message, if no format string was specified.
+ */
+#define BFS_DIAG_MSG_(format, str) \
+ (sizeof(format) > 1 ? "" : str)
+
+/**
+ * Unconditional assert.
+ */
+#define bfs_verify(...) \
+ bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", )
+
+#define bfs_verify_(str, cond, format, ...) \
+ ((cond) ? (void)0 : bfs_verify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__))
+
+#define bfs_verify__(format, ...) \
+ bfs_abortf( \
+ sizeof(format) > 1 \
+ ? BFS_DIAG_FORMAT_("%s" format "%s") \
+ : BFS_DIAG_FORMAT_("Assertion failed: `%s`"), \
+ BFS_DIAG_ARGS_(__VA_ARGS__))
+
+/**
+ * Unconditional assert, including the last error.
+ */
+#define bfs_everify(...) \
+ bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", )
+
+
+#define bfs_everify_(str, cond, format, ...) \
+ ((cond) ? (void)0 : bfs_everify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__))
+
+#define bfs_everify__(format, ...) \
+ bfs_abortf( \
+ sizeof(format) > 1 \
+ ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \
+ : BFS_DIAG_FORMAT_("Assertion failed: `%s`: %s"), \
+ BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), ))
+
+/**
+ * Assert in debug builds; no-op in release builds.
+ */
+#ifdef NDEBUG
+# define bfs_assert(...) ((void)0)
+# define bfs_eassert(...) ((void)0)
+#else
+# define bfs_assert bfs_verify
+# define bfs_eassert bfs_everify
+#endif
+
+struct bfs_ctx;
+struct bfs_expr;
+
+/**
+ * Various debugging flags.
+ */
+enum debug_flags {
+ /** Print cost estimates. */
+ DEBUG_COST = 1 << 0,
+ /** Print executed command details. */
+ DEBUG_EXEC = 1 << 1,
+ /** Print optimization details. */
+ DEBUG_OPT = 1 << 2,
+ /** Print rate information. */
+ DEBUG_RATES = 1 << 3,
+ /** Trace the filesystem traversal. */
+ DEBUG_SEARCH = 1 << 4,
+ /** Trace all stat() calls. */
+ DEBUG_STAT = 1 << 5,
+ /** Print the parse tree. */
+ DEBUG_TREE = 1 << 6,
+ /** All debug flags. */
+ DEBUG_ALL = (1 << 7) - 1,
+};
+
+/**
+ * Convert a debug flag to a string.
+ */
+const char *debug_flag_name(enum debug_flags flag);
+
+/**
+ * Like perror(), but decorated like bfs_error().
+ */
+_cold
+void bfs_perror(const struct bfs_ctx *ctx, const char *str);
+
+/**
+ * Shorthand for printing error messages.
+ */
+_cold
+_printf(2, 3)
+void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
+
+/**
+ * Shorthand for printing warning messages.
+ *
+ * @return Whether a warning was printed.
+ */
+_cold
+_printf(2, 3)
+bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
+
+/**
+ * Shorthand for printing debug messages.
+ *
+ * @return Whether a debug message was printed.
+ */
+_cold
+_printf(3, 4)
+bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...);
+
+/**
+ * bfs_error() variant that takes a va_list.
+ */
+_cold
+_printf(2, 0)
+void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args);
+
+/**
+ * bfs_warning() variant that takes a va_list.
+ */
+_cold
+_printf(2, 0)
+bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args);
+
+/**
+ * bfs_debug() variant that takes a va_list.
+ */
+_cold
+_printf(3, 0)
+bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args);
+
+/**
+ * Print the error message prefix.
+ */
+_cold
+void bfs_error_prefix(const struct bfs_ctx *ctx);
+
+/**
+ * Print the warning message prefix.
+ */
+_cold
+bool bfs_warning_prefix(const struct bfs_ctx *ctx);
+
+/**
+ * Print the debug message prefix.
+ */
+_cold
+bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag);
+
+/**
+ * Highlight parts of the command line in an error message.
+ */
+_cold
+void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]);
+
+/**
+ * Highlight parts of an expression in an error message.
+ */
+_cold
+void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
+
+/**
+ * Highlight parts of the command line in a warning message.
+ */
+_cold
+bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]);
+
+/**
+ * Highlight parts of an expression in a warning message.
+ */
+_cold
+bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
+
+#endif // BFS_DIAG_H
diff --git a/src/dir.c b/src/dir.c
new file mode 100644
index 0000000..4bf72a1
--- /dev/null
+++ b/src/dir.c
@@ -0,0 +1,373 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "dir.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+#include "trie.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#if BFS_USE_GETDENTS
+# if BFS_HAS_GETDENTS64_SYSCALL
+# include <sys/syscall.h>
+# endif
+
+/** getdents() syscall wrapper. */
+static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
+ sanitize_uninit(buf, size);
+
+#if BFS_HAS_POSIX_GETDENTS
+ int flags = 0;
+# ifdef DT_FORCE_TYPE
+ flags |= DT_FORCE_TYPE;
+# endif
+ ssize_t ret = posix_getdents(fd, buf, size, flags);
+#elif BFS_HAS_GETDENTS
+ ssize_t ret = getdents(fd, buf, size);
+#elif BFS_HAS_GETDENTS64
+ ssize_t ret = getdents64(fd, buf, size);
+#elif BFS_HAS_GETDENTS64_SYSCALL
+ ssize_t ret = syscall(SYS_getdents64, fd, buf, size);
+#else
+# error "No getdents() implementation"
+#endif
+
+ if (ret > 0) {
+ sanitize_init(buf, ret);
+ }
+
+ return ret;
+}
+
+#endif // BFS_USE_GETDENTS
+
+/** Directory entry type for bfs_getdents() */
+#if !BFS_USE_GETDENTS || BFS_HAS_GETDENTS
+typedef struct dirent sys_dirent;
+#elif BFS_HAS_POSIX_GETDENTS
+typedef struct posix_dent sys_dirent;
+#else
+typedef struct dirent64 sys_dirent;
+#endif
+
+enum bfs_type bfs_mode_to_type(mode_t mode) {
+ switch (mode & S_IFMT) {
+#ifdef S_IFBLK
+ case S_IFBLK:
+ return BFS_BLK;
+#endif
+#ifdef S_IFCHR
+ case S_IFCHR:
+ return BFS_CHR;
+#endif
+#ifdef S_IFDIR
+ case S_IFDIR:
+ return BFS_DIR;
+#endif
+#ifdef S_IFDOOR
+ case S_IFDOOR:
+ return BFS_DOOR;
+#endif
+#ifdef S_IFIFO
+ case S_IFIFO:
+ return BFS_FIFO;
+#endif
+#ifdef S_IFLNK
+ case S_IFLNK:
+ return BFS_LNK;
+#endif
+#ifdef S_IFPORT
+ case S_IFPORT:
+ return BFS_PORT;
+#endif
+#ifdef S_IFREG
+ case S_IFREG:
+ return BFS_REG;
+#endif
+#ifdef S_IFSOCK
+ case S_IFSOCK:
+ return BFS_SOCK;
+#endif
+#ifdef S_IFWHT
+ case S_IFWHT:
+ return BFS_WHT;
+#endif
+
+ default:
+ return BFS_UNKNOWN;
+ }
+}
+
+/**
+ * Private directory flags.
+ */
+enum {
+ /** We've reached the end of the directory. */
+ BFS_DIR_EOF = BFS_DIR_PRIVATE << 0,
+ /** This directory is a union mount we need to dedup manually. */
+ BFS_DIR_UNION = BFS_DIR_PRIVATE << 1,
+};
+
+struct bfs_dir {
+ unsigned int flags;
+
+#if BFS_USE_GETDENTS
+ int fd;
+ unsigned short pos;
+ unsigned short size;
+# if __FreeBSD__
+ struct trie trie;
+# endif
+ alignas(sys_dirent) char buf[];
+#else
+ DIR *dir;
+ struct dirent *de;
+#endif
+};
+
+#if BFS_USE_GETDENTS
+# define DIR_SIZE (64 << 10)
+# define BUF_SIZE (DIR_SIZE - sizeof(struct bfs_dir))
+#else
+# define DIR_SIZE sizeof(struct bfs_dir)
+#endif
+
+struct bfs_dir *bfs_allocdir(void) {
+ return malloc(DIR_SIZE);
+}
+
+void bfs_dir_arena(struct arena *arena) {
+ arena_init(arena, alignof(struct bfs_dir), DIR_SIZE);
+}
+
+int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags) {
+ int fd;
+ if (at_path) {
+ fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
+ if (fd < 0) {
+ return -1;
+ }
+ } else if (at_fd >= 0) {
+ fd = at_fd;
+ } else {
+ errno = EBADF;
+ return -1;
+ }
+
+ dir->flags = flags;
+
+#if BFS_USE_GETDENTS
+ dir->fd = fd;
+ dir->pos = 0;
+ dir->size = 0;
+
+# if __FreeBSD__ && defined(F_ISUNIONSTACK)
+ if (fcntl(fd, F_ISUNIONSTACK) > 0) {
+ dir->flags |= BFS_DIR_UNION;
+ trie_init(&dir->trie);
+ }
+# endif
+#else // !BFS_USE_GETDENTS
+ dir->dir = fdopendir(fd);
+ if (!dir->dir) {
+ if (at_path) {
+ close_quietly(fd);
+ }
+ return -1;
+ }
+ dir->de = NULL;
+#endif
+
+ return 0;
+}
+
+int bfs_dirfd(const struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
+ return dir->fd;
+#else
+ return dirfd(dir->dir);
+#endif
+}
+
+int bfs_polldir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
+ if (dir->pos < dir->size) {
+ return 1;
+ } else if (dir->flags & BFS_DIR_EOF) {
+ return 0;
+ }
+
+ char *buf = (char *)(dir + 1);
+ ssize_t size = bfs_getdents(dir->fd, buf, BUF_SIZE);
+ if (size == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ return 0;
+ } else if (size < 0) {
+ return -1;
+ }
+
+ dir->pos = 0;
+ dir->size = size;
+
+ // Like read(), getdents() doesn't indicate EOF until another call returns zero.
+ // Check that eagerly here to hopefully avoid a syscall in the last bfs_readdir().
+ size_t rest = BUF_SIZE - size;
+ if (rest >= sizeof(sys_dirent)) {
+ size = bfs_getdents(dir->fd, buf + size, rest);
+ if (size > 0) {
+ dir->size += size;
+ } else if (size == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ }
+ }
+
+ return 1;
+#else // !BFS_USE_GETDENTS
+ if (dir->de) {
+ return 1;
+ } else if (dir->flags & BFS_DIR_EOF) {
+ return 0;
+ }
+
+ errno = 0;
+ dir->de = readdir(dir->dir);
+ if (dir->de) {
+ return 1;
+ } else if (errno == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ return 0;
+ } else {
+ return -1;
+ }
+#endif
+}
+
+/** Read a single directory entry. */
+static int bfs_getdent(struct bfs_dir *dir, const sys_dirent **de) {
+ int ret = bfs_polldir(dir);
+ if (ret > 0) {
+#if BFS_USE_GETDENTS
+ char *buf = (char *)(dir + 1);
+ *de = (const sys_dirent *)(buf + dir->pos);
+ dir->pos += (*de)->d_reclen;
+#else
+ *de = dir->de;
+ dir->de = NULL;
+#endif
+ }
+ return ret;
+}
+
+/** Skip ".", "..", and deleted/empty dirents. */
+static int bfs_skipdent(struct bfs_dir *dir, const sys_dirent *de) {
+#if BFS_USE_GETDENTS
+# if __FreeBSD__
+ // Union mounts on FreeBSD have to be de-duplicated in userspace
+ if (dir->flags & BFS_DIR_UNION) {
+ struct trie_leaf *leaf = trie_insert_str(&dir->trie, de->d_name);
+ if (!leaf) {
+ return -1;
+ } else if (leaf->value) {
+ return 1;
+ } else {
+ leaf->value = leaf;
+ }
+ }
+
+ // NFS mounts on FreeBSD can return empty dirents with inode number 0
+ if (de->d_ino == 0) {
+ return 1;
+ }
+# endif
+
+# ifdef DT_WHT
+ if (de->d_type == DT_WHT && !(dir->flags & BFS_DIR_WHITEOUTS)) {
+ return 1;
+ }
+# endif
+#endif // BFS_USE_GETDENTS
+
+ const char *name = de->d_name;
+ return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
+}
+
+/** Convert de->d_type to a bfs_type, if it exists. */
+static enum bfs_type bfs_d_type(const sys_dirent *de) {
+#ifdef DTTOIF
+ return bfs_mode_to_type(DTTOIF(de->d_type));
+#else
+ return BFS_UNKNOWN;
+#endif
+}
+
+int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) {
+ while (true) {
+ const sys_dirent *sysde;
+ int ret = bfs_getdent(dir, &sysde);
+ if (ret <= 0) {
+ return ret;
+ }
+
+ int skip = bfs_skipdent(dir, sysde);
+ if (skip < 0) {
+ return skip;
+ } else if (skip) {
+ continue;
+ }
+
+ if (de) {
+ de->type = bfs_d_type(sysde);
+ de->name = sysde->d_name;
+ }
+
+ return 1;
+ }
+}
+
+static void bfs_destroydir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS && __FreeBSD__
+ if (dir->flags & BFS_DIR_UNION) {
+ trie_destroy(&dir->trie);
+ }
+#endif
+
+ sanitize_uninit(dir, DIR_SIZE);
+}
+
+int bfs_closedir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
+ int ret = xclose(dir->fd);
+#else
+ int ret = closedir(dir->dir);
+ if (ret != 0) {
+ bfs_verify(errno != EBADF);
+ }
+#endif
+
+ bfs_destroydir(dir);
+ return ret;
+}
+
+#if BFS_USE_UNWRAPDIR
+int bfs_unwrapdir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
+ int ret = dir->fd;
+#elif BFS_HAS_FDCLOSEDIR
+ int ret = fdclosedir(dir->dir);
+#endif
+
+ bfs_destroydir(dir);
+ return ret;
+}
+#endif
diff --git a/src/dir.h b/src/dir.h
new file mode 100644
index 0000000..885dac3
--- /dev/null
+++ b/src/dir.h
@@ -0,0 +1,178 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Directories and their contents.
+ */
+
+#ifndef BFS_DIR_H
+#define BFS_DIR_H
+
+#include "bfs.h"
+
+#include <sys/types.h>
+
+/**
+ * Whether the implementation uses the getdents() syscall directly, rather than
+ * libc's readdir().
+ */
+#ifndef BFS_USE_GETDENTS
+# if BFS_HAS_POSIX_GETDENTS
+# define BFS_USE_GETDENTS true
+# elif __linux__ || __FreeBSD__
+# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL)
+# else
+# define BFS_USE_GETDENTS false
+# endif
+#endif
+
+/**
+ * A directory.
+ */
+struct bfs_dir;
+
+/**
+ * File types.
+ */
+enum bfs_type {
+ /** An error occurred for this file. */
+ BFS_ERROR = -1,
+ /** Unknown type. */
+ BFS_UNKNOWN,
+ /** Block device. */
+ BFS_BLK,
+ /** Character device. */
+ BFS_CHR,
+ /** Directory. */
+ BFS_DIR,
+ /** Solaris door. */
+ BFS_DOOR,
+ /** Pipe. */
+ BFS_FIFO,
+ /** Symbolic link. */
+ BFS_LNK,
+ /** Solaris event port. */
+ BFS_PORT,
+ /** Regular file. */
+ BFS_REG,
+ /** Socket. */
+ BFS_SOCK,
+ /** BSD whiteout. */
+ BFS_WHT,
+};
+
+/**
+ * Convert a bfs_stat() mode to a bfs_type.
+ */
+enum bfs_type bfs_mode_to_type(mode_t mode);
+
+/**
+ * A directory entry.
+ */
+struct bfs_dirent {
+ /** The type of this file (possibly unknown). */
+ enum bfs_type type;
+ /** The name of this file. */
+ const char *name;
+};
+
+/**
+ * Allocate space for a directory.
+ *
+ * @return
+ * An allocated, unopen directory, or NULL on failure.
+ */
+struct bfs_dir *bfs_allocdir(void);
+
+struct arena;
+
+/**
+ * Initialize an arena for directories.
+ *
+ * @arena
+ * The arena to initialize.
+ */
+void bfs_dir_arena(struct arena *arena);
+
+/**
+ * bfs_opendir() flags.
+ */
+enum bfs_dir_flags {
+ /** Include whiteouts in the results. */
+ BFS_DIR_WHITEOUTS = 1 << 0,
+ /** @internal Start of private flags. */
+ BFS_DIR_PRIVATE = 1 << 1,
+};
+
+/**
+ * Open a directory.
+ *
+ * @dir
+ * The allocated directory.
+ * @at_fd
+ * The base directory for path resolution.
+ * @at_path
+ * The path of the directory to open, relative to at_fd. Pass NULL to
+ * open at_fd itself.
+ * @flags
+ * Flags that control which directory entries are listed.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags);
+
+/**
+ * Get the file descriptor for a directory.
+ */
+int bfs_dirfd(const struct bfs_dir *dir);
+
+/**
+ * Performs any I/O necessary for the next bfs_readdir() call.
+ *
+ * @dir
+ * The directory to poll.
+ * @return
+ * 1 on success, 0 on EOF, or -1 on failure.
+ */
+int bfs_polldir(struct bfs_dir *dir);
+
+/**
+ * Read a directory entry.
+ *
+ * @dir
+ * The directory to read.
+ * @dirent[out]
+ * The directory entry to populate.
+ * @return
+ * 1 on success, 0 on EOF, or -1 on failure.
+ */
+int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de);
+
+/**
+ * Close a directory.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_closedir(struct bfs_dir *dir);
+
+/**
+ * Whether the bfs_unwrapdir() function is supported.
+ */
+#ifndef BFS_USE_UNWRAPDIR
+# define BFS_USE_UNWRAPDIR (BFS_USE_GETDENTS || BFS_HAS_FDCLOSEDIR)
+#endif
+
+#if BFS_USE_UNWRAPDIR
+/**
+ * Detach the file descriptor from an open directory.
+ *
+ * @dir
+ * The directory to detach.
+ * @return
+ * The file descriptor of the directory.
+ */
+int bfs_unwrapdir(struct bfs_dir *dir);
+#endif
+
+#endif // BFS_DIR_H
diff --git a/src/dstring.c b/src/dstring.c
new file mode 100644
index 0000000..678d685
--- /dev/null
+++ b/src/dstring.c
@@ -0,0 +1,308 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "dstring.h"
+
+#include "alloc.h"
+#include "bit.h"
+#include "diag.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * The memory representation of a dynamic string. Users get a pointer to str.
+ */
+struct dstring {
+ /** Capacity of the string, *including* the terminating NUL. */
+ size_t cap;
+ /** Length of the string, *excluding* the terminating NUL. */
+ size_t len;
+ /** The string itself. */
+ alignas(dchar) char str[] _counted_by(cap);
+};
+
+#define DSTR_OFFSET offsetof(struct dstring, str)
+
+/** Back up to the header from a pointer to dstring::str. */
+static struct dstring *dstrheader(const dchar *dstr) {
+ return (struct dstring *)(dstr - DSTR_OFFSET);
+}
+
+/**
+ * In some provenance models, the expression `header->str` has its provenance
+ * restricted to just the `str` field itself, making a future dstrheader()
+ * illegal. This alternative is guaranteed to preserve provenance for the entire
+ * allocation.
+ *
+ * - https://stackoverflow.com/q/25296019
+ * - https://mastodon.social/@void_friend@tech.lgbt/111144859908104311
+ */
+static dchar *dstrdata(struct dstring *header) {
+ return (char *)header + DSTR_OFFSET;
+}
+
+/** Set the length of a dynamic string. */
+static void dstrsetlen(struct dstring *header, size_t len) {
+ bfs_assert(len < header->cap);
+ header->len = len;
+ header->str[len] = '\0';
+}
+
+/** Allocate a dstring with the given contents. */
+static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) {
+ // Avoid reallocations for small strings
+ if (cap < DSTR_OFFSET) {
+ cap = DSTR_OFFSET;
+ }
+
+ struct dstring *header = ALLOC_FLEX(struct dstring, str, cap);
+ if (!header) {
+ return NULL;
+ }
+
+ header->cap = cap;
+ dstrsetlen(header, len);
+
+ dchar *ret = dstrdata(header);
+ memcpy(ret, str, len);
+ return ret;
+}
+
+dchar *dstralloc(size_t cap) {
+ return dstralloc_impl(cap + 1, 0, "");
+}
+
+dchar *dstrdup(const char *str) {
+ return dstrxdup(str, strlen(str));
+}
+
+dchar *dstrndup(const char *str, size_t n) {
+ return dstrxdup(str, strnlen(str, n));
+}
+
+dchar *dstrddup(const dchar *dstr) {
+ return dstrxdup(dstr, dstrlen(dstr));
+}
+
+dchar *dstrxdup(const char *str, size_t len) {
+ return dstralloc_impl(len + 1, len, str);
+}
+
+size_t dstrlen(const dchar *dstr) {
+ return dstrheader(dstr)->len;
+}
+
+int dstreserve(dchar **dstr, size_t cap) {
+ if (!*dstr) {
+ *dstr = dstralloc(cap);
+ return *dstr ? 0 : -1;
+ }
+
+ struct dstring *header = dstrheader(*dstr);
+ size_t old_cap = header->cap;
+ size_t new_cap = cap + 1; // Terminating NUL
+ if (old_cap >= new_cap) {
+ return 0;
+ }
+
+ new_cap = bit_ceil(new_cap);
+ header = REALLOC_FLEX(struct dstring, str, header, old_cap, new_cap);
+ if (!header) {
+ return -1;
+ }
+
+ header->cap = new_cap;
+ *dstr = dstrdata(header);
+ return 0;
+}
+
+int dstresize(dchar **dstr, size_t len) {
+ if (dstreserve(dstr, len) != 0) {
+ return -1;
+ }
+
+ struct dstring *header = dstrheader(*dstr);
+ dstrsetlen(header, len);
+ return 0;
+}
+
+void dstrshrink(dchar *dstr, size_t len) {
+ struct dstring *header = dstrheader(dstr);
+ bfs_assert(len <= header->len);
+ dstrsetlen(header, len);
+}
+
+int dstrcat(dchar **dest, const char *src) {
+ return dstrxcat(dest, src, strlen(src));
+}
+
+int dstrncat(dchar **dest, const char *src, size_t n) {
+ return dstrxcat(dest, src, strnlen(src, n));
+}
+
+int dstrdcat(dchar **dest, const dchar *src) {
+ return dstrxcat(dest, src, dstrlen(src));
+}
+
+int dstrxcat(dchar **dest, const char *src, size_t len) {
+ size_t oldlen = dstrlen(*dest);
+ size_t newlen = oldlen + len;
+
+ if (dstresize(dest, newlen) != 0) {
+ return -1;
+ }
+
+ memcpy(*dest + oldlen, src, len);
+ return 0;
+}
+
+int dstrapp(dchar **str, char c) {
+ return dstrxcat(str, &c, 1);
+}
+
+int dstrcpy(dchar **dest, const char *src) {
+ return dstrxcpy(dest, src, strlen(src));
+}
+
+int dstrncpy(dchar **dest, const char *src, size_t n) {
+ return dstrxcpy(dest, src, strnlen(src, n));
+}
+
+int dstrdcpy(dchar **dest, const dchar *src) {
+ return dstrxcpy(dest, src, dstrlen(src));
+}
+
+int dstrxcpy(dchar **dest, const char *src, size_t len) {
+ if (dstresize(dest, len) != 0) {
+ return -1;
+ }
+
+ memcpy(*dest, src, len);
+ return 0;
+}
+
+dchar *dstrprintf(const char *format, ...) {
+ va_list args;
+
+ va_start(args, format);
+ dchar *str = dstrvprintf(format, args);
+ va_end(args);
+
+ return str;
+}
+
+dchar *dstrvprintf(const char *format, va_list args) {
+ // Guess a capacity to try to avoid reallocating
+ dchar *str = dstralloc(2 * strlen(format));
+ if (!str) {
+ return NULL;
+ }
+
+ if (dstrvcatf(&str, format, args) != 0) {
+ dstrfree(str);
+ return NULL;
+ }
+
+ return str;
+}
+
+int dstrcatf(dchar **str, const char *format, ...) {
+ va_list args;
+
+ va_start(args, format);
+ int ret = dstrvcatf(str, format, args);
+ va_end(args);
+
+ return ret;
+}
+
+int dstrvcatf(dchar **str, const char *format, va_list args) {
+ // Guess a capacity to try to avoid calling vsnprintf() twice
+ size_t len = dstrlen(*str);
+ dstreserve(str, len + 2 * strlen(format));
+ size_t cap = dstrheader(*str)->cap;
+
+ va_list copy;
+ va_copy(copy, args);
+
+ char *tail = *str + len;
+ size_t tail_cap = cap - len;
+ int ret = vsnprintf(tail, tail_cap, format, args);
+ if (ret < 0) {
+ goto fail;
+ }
+
+ size_t tail_len = ret;
+ if (tail_len >= tail_cap) {
+ if (dstreserve(str, len + tail_len) != 0) {
+ goto fail;
+ }
+
+ tail = *str + len;
+ ret = vsnprintf(tail, tail_len + 1, format, copy);
+ if (ret < 0 || (size_t)ret != tail_len) {
+ bfs_bug("Length of formatted string changed");
+ goto fail;
+ }
+ }
+
+ va_end(copy);
+
+ dstrheader(*str)->len += tail_len;
+ return 0;
+
+fail:
+ va_end(copy);
+ *tail = '\0';
+ return -1;
+}
+
+int dstrescat(dchar **dest, const char *str, enum wesc_flags flags) {
+ return dstrnescat(dest, str, SIZE_MAX, flags);
+}
+
+int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags) {
+ size_t len = *dest ? dstrlen(*dest) : 0;
+
+ // Worst case growth is `ccc...` => $'\xCC\xCC\xCC...'
+ n = strnlen(str, n);
+ size_t cap = len + 4 * n + 3;
+ if (dstreserve(dest, cap) != 0) {
+ return -1;
+ }
+
+ char *cur = *dest + len;
+ char *end = *dest + cap + 1;
+ cur = wordnesc(cur, end, str, n, flags);
+ bfs_assert(cur != end, "wordesc() result truncated");
+
+ return dstresize(dest, cur - *dest);
+}
+
+void dstrfree(dchar *dstr) {
+ if (dstr) {
+ free(dstrheader(dstr));
+ }
+}
+
+dchar *dstrepeat(const char *str, size_t n) {
+ size_t len = strlen(str);
+ dchar *ret = dstralloc(n * len);
+ if (!ret) {
+ return NULL;
+ }
+
+ for (size_t i = 0; i < n; ++i) {
+ if (dstrxcat(&ret, str, len) < 0) {
+ dstrfree(ret);
+ return NULL;
+ }
+ }
+
+ return ret;
+}
diff --git a/src/dstring.h b/src/dstring.h
new file mode 100644
index 0000000..ce7ef86
--- /dev/null
+++ b/src/dstring.h
@@ -0,0 +1,355 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A dynamic string library.
+ */
+
+#ifndef BFS_DSTRING_H
+#define BFS_DSTRING_H
+
+#include "bfs.h"
+#include "bfstd.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+
+/** Marker type for dynamic strings. */
+#if BFS_LINT && __clang__
+// Abuse __attribute__(aligned) to make a type that allows
+//
+// dchar * -> char *
+//
+// conversions, but warns (with Clang's -Walign-mismatch) on
+//
+// char * -> dchar *
+typedef __attribute__((aligned(alignof(size_t)))) char dchar;
+#else
+typedef char dchar;
+#endif
+
+/**
+ * Free a dynamic string.
+ *
+ * @dstr
+ * The string to free.
+ */
+void dstrfree(dchar *dstr);
+
+/**
+ * Allocate a dynamic string.
+ *
+ * @cap
+ * The initial capacity of the string.
+ */
+_malloc(dstrfree, 1)
+dchar *dstralloc(size_t cap);
+
+/**
+ * Create a dynamic copy of a string.
+ *
+ * @str
+ * The NUL-terminated string to copy.
+ */
+_malloc(dstrfree, 1)
+dchar *dstrdup(const char *str);
+
+/**
+ * Create a length-limited dynamic copy of a string.
+ *
+ * @str
+ * The string to copy.
+ * @n
+ * The maximum number of characters to copy from str.
+ */
+_malloc(dstrfree, 1)
+dchar *dstrndup(const char *str, size_t n);
+
+/**
+ * Create a dynamic copy of a dynamic string.
+ *
+ * @dstr
+ * The dynamic string to copy.
+ */
+_malloc(dstrfree, 1)
+dchar *dstrddup(const dchar *dstr);
+
+/**
+ * Create an exact-sized dynamic copy of a string.
+ *
+ * @str
+ * The string to copy.
+ * @len
+ * The length of the string, which may include internal NUL bytes.
+ */
+_malloc(dstrfree, 1)
+dchar *dstrxdup(const char *str, size_t len);
+
+/**
+ * Get a dynamic string's length.
+ *
+ * @dstr
+ * The string to measure.
+ * @return
+ * The length of dstr.
+ */
+size_t dstrlen(const dchar *dstr);
+
+/**
+ * Reserve some capacity in a dynamic string.
+ *
+ * @dstr
+ * The dynamic string to preallocate.
+ * @cap
+ * The new capacity for the string.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int dstreserve(dchar **dstr, size_t cap);
+
+/**
+ * Resize a dynamic string.
+ *
+ * @dstr
+ * The dynamic string to resize.
+ * @len
+ * The new length for the dynamic string.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstresize(dchar **dstr, size_t len);
+
+/**
+ * Shrink a dynamic string.
+ *
+ * @dstr
+ * The dynamic string to shrink.
+ * @len
+ * The new length. Must not be greater than the current length.
+ */
+void dstrshrink(dchar *dstr, size_t len);
+
+/**
+ * Append to a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The string to append.
+ * @return 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrcat(dchar **dest, const char *src);
+
+/**
+ * Append to a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The string to append.
+ * @n
+ * The maximum number of characters to take from src.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrncat(dchar **dest, const char *src, size_t n);
+
+/**
+ * Append a dynamic string to another dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The dynamic string to append.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrdcat(dchar **dest, const dchar *src);
+
+/**
+ * Append to a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The string to append.
+ * @len
+ * The exact number of characters to take from src.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrxcat(dchar **dest, const char *src, size_t len);
+
+/**
+ * Append a single character to a dynamic string.
+ *
+ * @str
+ * The string to append to.
+ * @c
+ * The character to append.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrapp(dchar **str, char c);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The string to copy.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrcpy(dchar **dest, const char *str);
+
+/**
+ * Copy a dynamic string into another one.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The dynamic string to copy.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrdcpy(dchar **dest, const dchar *str);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The dynamic string to copy.
+ * @n
+ * The maximum number of characters to take from src.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrncpy(dchar **dest, const char *str, size_t n);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @src
+ * The dynamic string to copy.
+ * @len
+ * The exact number of characters to take from src.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrxcpy(dchar **dest, const char *str, size_t len);
+
+/**
+ * Create a dynamic string from a format string.
+ *
+ * @format
+ * The format string to fill in.
+ * @...
+ * Any arguments for the format string.
+ * @return
+ * The created string, or NULL on failure.
+ */
+_nodiscard
+_printf(1, 2)
+dchar *dstrprintf(const char *format, ...);
+
+/**
+ * Create a dynamic string from a format string and a va_list.
+ *
+ * @format
+ * The format string to fill in.
+ * @args
+ * The arguments for the format string.
+ * @return
+ * The created string, or NULL on failure.
+ */
+_nodiscard
+_printf(1, 0)
+dchar *dstrvprintf(const char *format, va_list args);
+
+/**
+ * Format some text onto the end of a dynamic string.
+ *
+ * @str
+ * The destination dynamic string.
+ * @format
+ * The format string to fill in.
+ * @...
+ * Any arguments for the format string.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+_printf(2, 3)
+int dstrcatf(dchar **str, const char *format, ...);
+
+/**
+ * Format some text from a va_list onto the end of a dynamic string.
+ *
+ * @str
+ * The destination dynamic string.
+ * @format
+ * The format string to fill in.
+ * @args
+ * The arguments for the format string.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+_printf(2, 0)
+int dstrvcatf(dchar **str, const char *format, va_list args);
+
+/**
+ * Concatenate while shell-escaping.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @str
+ * The string to escape.
+ * @flags
+ * Flags for wordesc().
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrescat(dchar **dest, const char *str, enum wesc_flags flags);
+
+/**
+ * Concatenate while shell-escaping.
+ *
+ * @dest
+ * The destination dynamic string.
+ * @str
+ * The string to escape.
+ * @n
+ * The maximum length of the string.
+ * @flags
+ * Flags for wordesc().
+ * @return
+ * 0 on success, -1 on failure.
+ */
+_nodiscard
+int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags);
+
+/**
+ * Repeat a string n times.
+ */
+_nodiscard
+dchar *dstrepeat(const char *str, size_t n);
+
+#endif // BFS_DSTRING_H
diff --git a/eval.c b/src/eval.c
index fbde00e..0d1bf68 100644
--- a/eval.c
+++ b/src/eval.c
@@ -1,29 +1,19 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Implementation of all the literal expressions.
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Implementation of all the primary expressions.
*/
#include "eval.h"
+
+#include "atomic.h"
#include "bar.h"
+#include "bfs.h"
+#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
-#include "darray.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
@@ -33,29 +23,33 @@
#include "mtab.h"
#include "printf.h"
#include "pwcache.h"
+#include "sanity.h"
+#include "sighook.h"
#include "stat.h"
-#include "time.h"
#include "trie.h"
-#include "util.h"
-#include <assert.h>
+#include "xregex.h"
+#include "xtime.h"
+
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <grp.h>
#include <pwd.h>
-#include <regex.h>
+#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <strings.h>
#include <sys/resource.h>
-#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
-struct eval_state {
+struct bfs_eval {
/** Data about the current file. */
const struct BFTW *ftwbuf;
/** The bfs context. */
@@ -64,6 +58,8 @@ struct eval_state {
enum bftw_action action;
/** The bfs_eval() return value. */
int *ret;
+ /** The number of errors that have occurred. */
+ size_t *nerrors;
/** Whether to quit immediately. */
bool quit;
};
@@ -71,20 +67,24 @@ struct eval_state {
/**
* Print an error message.
*/
-BFS_FORMATTER(2, 3)
-static void eval_error(struct eval_state *state, const char *format, ...) {
+_printf(2, 3)
+static void eval_error(struct bfs_eval *state, const char *format, ...) {
+ const struct bfs_ctx *ctx = state->ctx;
+
+ ++*state->nerrors;
+ if (ctx->ignore_errors) {
+ return;
+ }
+
// By POSIX, any errors should be accompanied by a non-zero exit status
*state->ret = EXIT_FAILURE;
- int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
CFILE *cerr = ctx->cerr;
bfs_error(ctx, "%pP: ", state->ftwbuf);
va_list args;
va_start(args, format);
- errno = error;
cvfprintf(cerr, format, args);
va_end(args);
}
@@ -92,25 +92,39 @@ static void eval_error(struct eval_state *state, const char *format, ...) {
/**
* Check if an error should be ignored.
*/
-static bool eval_should_ignore(const struct eval_state *state, int error) {
+static bool eval_should_ignore(const struct bfs_eval *state, int error) {
return state->ctx->ignore_races
- && is_nonexistence_error(error)
+ && error_is_like(error, ENOENT)
&& state->ftwbuf->depth > 0;
}
/**
* Report an error that occurs during evaluation.
*/
-static void eval_report_error(struct eval_state *state) {
+static void eval_report_error(struct bfs_eval *state) {
if (!eval_should_ignore(state, errno)) {
- eval_error(state, "%m.\n");
+ eval_error(state, "%s.\n", errstr());
+ }
+}
+
+/**
+ * Report an I/O error that occurs during evaluation.
+ */
+static void eval_io_error(const struct bfs_expr *expr, struct bfs_eval *state) {
+ if (expr->path) {
+ eval_error(state, "'%s': %s.\n", expr->path, errstr());
+ } else {
+ eval_error(state, "(standard output): %s.\n", errstr());
}
+
+ // Don't report the error again in bfs_ctx_free()
+ clearerr(expr->cfile->file);
}
/**
* Perform a bfs_stat() call if necessary.
*/
-static const struct bfs_stat *eval_stat(struct eval_state *state) {
+static const struct bfs_stat *eval_stat(struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
const struct bfs_stat *ret = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!ret) {
@@ -123,52 +137,65 @@ static const struct bfs_stat *eval_stat(struct eval_state *state) {
* Get the difference (in seconds) between two struct timespecs.
*/
static time_t timespec_diff(const struct timespec *lhs, const struct timespec *rhs) {
- time_t ret = lhs->tv_sec - rhs->tv_sec;
- if (lhs->tv_nsec < rhs->tv_nsec) {
- --ret;
- }
- return ret;
+ struct timespec diff = *lhs;
+ timespec_sub(&diff, rhs);
+ return diff.tv_sec;
}
-bool expr_cmp(const struct expr *expr, long long n) {
- switch (expr->cmp_flag) {
- case CMP_EXACT:
- return n == expr->idata;
- case CMP_LESS:
- return n < expr->idata;
- case CMP_GREATER:
- return n > expr->idata;
+bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) {
+ switch (expr->int_cmp) {
+ case BFS_INT_EQUAL:
+ return n == expr->num;
+ case BFS_INT_LESS:
+ return n < expr->num;
+ case BFS_INT_GREATER:
+ return n > expr->num;
}
+ bfs_bug("Invalid comparison mode");
return false;
}
+/** Common code for fnmatch() tests. */
+static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
+ if (expr->literal) {
+#ifdef FNM_CASEFOLD
+ if (expr->fnm_flags & FNM_CASEFOLD) {
+ return strcasecmp(expr->pattern, str) == 0;
+ }
+#endif
+ return strcmp(expr->pattern, str) == 0;
+ } else {
+ return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
+ }
+}
+
/**
* -true test.
*/
-bool eval_true(const struct expr *expr, struct eval_state *state) {
+bool eval_true(const struct bfs_expr *expr, struct bfs_eval *state) {
return true;
}
/**
* -false test.
*/
-bool eval_false(const struct expr *expr, struct eval_state *state) {
+bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state) {
return false;
}
/**
* -executable, -readable, -writable tests.
*/
-bool eval_access(const struct expr *expr, struct eval_state *state) {
+bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
- return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, expr->idata) == 0;
+ return xfaccessat(ftwbuf->at_fd, ftwbuf->at_path, expr->num) == 0;
}
/**
* -acl test.
*/
-bool eval_acl(const struct expr *expr, struct eval_state *state) {
+bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state) {
int ret = bfs_check_acl(state->ftwbuf);
if (ret >= 0) {
return ret;
@@ -181,7 +208,7 @@ bool eval_acl(const struct expr *expr, struct eval_state *state) {
/**
* -capable test.
*/
-bool eval_capable(const struct expr *expr, struct eval_state *state) {
+bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) {
int ret = bfs_check_capabilities(state->ftwbuf);
if (ret >= 0) {
return ret;
@@ -192,12 +219,27 @@ bool eval_capable(const struct expr *expr, struct eval_state *state) {
}
/**
+ * -context test.
+ */
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) {
+ char *con = bfs_getfilecon(state->ftwbuf);
+ if (!con) {
+ eval_report_error(state);
+ return false;
+ }
+
+ bool ret = eval_fnmatch(expr, con);
+ bfs_freecon(con);
+ return ret;
+}
+
+/**
* Get the given timespec field out of a stat buffer.
*/
-static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct eval_state *state) {
+static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) {
const struct timespec *ret = bfs_stat_time(statbuf, field);
if (!ret) {
- eval_error(state, "Couldn't get file %s: %m.\n", bfs_stat_field_name(field));
+ eval_error(state, "Couldn't get file %s: %s.\n", bfs_stat_field_name(field), errstr());
}
return ret;
}
@@ -205,7 +247,7 @@ static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enu
/**
* -[aBcm]?newer tests.
*/
-bool eval_newer(const struct expr *expr, struct eval_state *state) {
+bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -216,14 +258,13 @@ bool eval_newer(const struct expr *expr, struct eval_state *state) {
return false;
}
- return time->tv_sec > expr->reftime.tv_sec
- || (time->tv_sec == expr->reftime.tv_sec && time->tv_nsec > expr->reftime.tv_nsec);
+ return timespec_cmp(time, &expr->reftime) > 0;
}
/**
* -[aBcm]{min,time} tests.
*/
-bool eval_time(const struct expr *expr, struct eval_state *state) {
+bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -236,23 +277,23 @@ bool eval_time(const struct expr *expr, struct eval_state *state) {
time_t diff = timespec_diff(&expr->reftime, time);
switch (expr->time_unit) {
- case DAYS:
- diff /= 60*24;
- BFS_FALLTHROUGH;
- case MINUTES:
+ case BFS_DAYS:
+ diff /= 60 * 24;
+ _fallthrough;
+ case BFS_MINUTES:
diff /= 60;
- BFS_FALLTHROUGH;
- case SECONDS:
+ _fallthrough;
+ case BFS_SECONDS:
break;
}
- return expr_cmp(expr, diff);
+ return bfs_expr_cmp(expr, diff);
}
/**
* -used test.
*/
-bool eval_used(const struct expr *expr, struct eval_state *state) {
+bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -269,75 +310,71 @@ bool eval_used(const struct expr *expr, struct eval_state *state) {
return false;
}
- long long day_seconds = 60*60*24;
+ long long day_seconds = 60 * 60 * 24;
diff = (diff + day_seconds - 1) / day_seconds;
- return expr_cmp(expr, diff);
+ return bfs_expr_cmp(expr, diff);
}
/**
* -gid test.
*/
-bool eval_gid(const struct expr *expr, struct eval_state *state) {
+bool eval_gid(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- return expr_cmp(expr, statbuf->gid);
+ return bfs_expr_cmp(expr, statbuf->gid);
}
/**
* -uid test.
*/
-bool eval_uid(const struct expr *expr, struct eval_state *state) {
+bool eval_uid(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- return expr_cmp(expr, statbuf->uid);
+ return bfs_expr_cmp(expr, statbuf->uid);
}
/**
* -nogroup test.
*/
-bool eval_nogroup(const struct expr *expr, struct eval_state *state) {
+bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
- if (!groups) {
+ const struct group *grp = bfs_getgrgid(state->ctx->groups, statbuf->gid);
+ if (errno != 0) {
eval_report_error(state);
- return false;
}
-
- return bfs_getgrgid(groups, statbuf->gid) == NULL;
+ return grp == NULL;
}
/**
* -nouser test.
*/
-bool eval_nouser(const struct expr *expr, struct eval_state *state) {
+bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- if (!users) {
+ const struct passwd *pwd = bfs_getpwuid(state->ctx->users, statbuf->uid);
+ if (errno != 0) {
eval_report_error(state);
- return false;
}
-
- return bfs_getpwuid(users, statbuf->uid) == NULL;
+ return pwd == NULL;
}
/**
* -delete action.
*/
-bool eval_delete(const struct expr *expr, struct eval_state *state) {
+bool eval_delete(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
// Don't try to delete the current directory
@@ -365,30 +402,34 @@ bool eval_delete(const struct expr *expr, struct eval_state *state) {
}
/** Finish any pending -exec ... + operations. */
-static int eval_exec_finish(const struct expr *expr, const struct bfs_ctx *ctx) {
+static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *ctx) {
int ret = 0;
- if (expr->execbuf && bfs_exec_finish(expr->execbuf) != 0) {
- if (errno != 0) {
- bfs_error(ctx, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+
+ if (expr->eval_fn == eval_exec) {
+ if (bfs_exec_finish(expr->exec) != 0) {
+ if (errno != 0) {
+ bfs_error(ctx, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr());
+ }
+ ret = -1;
}
- ret = -1;
}
- if (expr->lhs && eval_exec_finish(expr->lhs, ctx) != 0) {
- ret = -1;
- }
- if (expr->rhs && eval_exec_finish(expr->rhs, ctx) != 0) {
- ret = -1;
+
+ for_expr (child, expr) {
+ if (eval_exec_finish(child, ctx) != 0) {
+ ret = -1;
+ }
}
+
return ret;
}
/**
* -exec[dir]/-ok[dir] actions.
*/
-bool eval_exec(const struct expr *expr, struct eval_state *state) {
- bool ret = bfs_exec(expr->execbuf, state->ftwbuf) == 0;
+bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) {
+ bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0;
if (errno != 0) {
- eval_error(state, "%s %s: %m.\n", expr->argv[0], expr->argv[1]);
+ eval_error(state, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr());
}
return ret;
}
@@ -396,9 +437,9 @@ bool eval_exec(const struct expr *expr, struct eval_state *state) {
/**
* -exit action.
*/
-bool eval_exit(const struct expr *expr, struct eval_state *state) {
+bool eval_exit(const struct bfs_expr *expr, struct bfs_eval *state) {
state->action = BFTW_STOP;
- *state->ret = expr->idata;
+ *state->ret = expr->num;
state->quit = true;
return true;
}
@@ -406,47 +447,56 @@ bool eval_exit(const struct expr *expr, struct eval_state *state) {
/**
* -depth N test.
*/
-bool eval_depth(const struct expr *expr, struct eval_state *state) {
- return expr_cmp(expr, state->ftwbuf->depth);
+bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) {
+ return bfs_expr_cmp(expr, state->ftwbuf->depth);
}
/**
* -empty test.
*/
-bool eval_empty(const struct expr *expr, struct eval_state *state) {
- bool ret = false;
+bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
+ const struct bfs_stat *statbuf;
+ struct bfs_dir *dir;
+
+ switch (ftwbuf->type) {
+ case BFS_REG:
+ statbuf = eval_stat(state);
+ return statbuf && statbuf->size == 0;
- if (ftwbuf->type == BFS_DIR) {
- struct bfs_dir *dir = bfs_opendir(ftwbuf->at_fd, ftwbuf->at_path);
+ case BFS_DIR:
+ dir = bfs_allocdir();
if (!dir) {
- eval_report_error(state);
- goto done;
+ goto error;
}
- int did_read = bfs_readdir(dir, NULL);
- if (did_read < 0) {
- eval_report_error(state);
- } else {
- ret = !did_read;
+ if (bfs_opendir(dir, ftwbuf->at_fd, ftwbuf->at_path, 0) != 0) {
+ goto error;
}
+ int did_read = bfs_readdir(dir, NULL);
bfs_closedir(dir);
- } else if (ftwbuf->type == BFS_REG) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (statbuf) {
- ret = statbuf->size == 0;
+
+ if (did_read < 0) {
+ goto error;
}
- }
-done:
- return ret;
+ free(dir);
+ return did_read == 0;
+ error:
+ eval_report_error(state);
+ free(dir);
+ return false;
+
+ default:
+ return false;
+ }
}
/**
* -flags test.
*/
-bool eval_flags(const struct expr *expr, struct eval_state *state) {
+bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -461,25 +511,25 @@ bool eval_flags(const struct expr *expr, struct eval_state *state) {
unsigned long set = expr->set_flags;
unsigned long clear = expr->clear_flags;
- switch (expr->mode_cmp) {
- case MODE_EXACT:
+ switch (expr->flags_cmp) {
+ case BFS_MODE_EQUAL:
return flags == set && !(flags & clear);
- case MODE_ALL:
+ case BFS_MODE_ALL:
return (flags & set) == set && !(flags & clear);
- case MODE_ANY:
+ case BFS_MODE_ANY:
return (flags & set) || (flags & clear) != clear;
}
- assert(!"Invalid comparison mode");
+ bfs_bug("Invalid comparison mode");
return false;
}
/**
* -fstype test.
*/
-bool eval_fstype(const struct expr *expr, struct eval_state *state) {
+bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -492,13 +542,18 @@ bool eval_fstype(const struct expr *expr, struct eval_state *state) {
}
const char *type = bfs_fstype(mtab, statbuf);
- return strcmp(type, expr->sdata) == 0;
+ if (!type) {
+ eval_report_error(state);
+ return false;
+ }
+
+ return strcmp(type, expr->argv[1]) == 0;
}
/**
* -hidden test.
*/
-bool eval_hidden(const struct expr *expr, struct eval_state *state) {
+bool eval_hidden(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct BFTW *ftwbuf = state->ftwbuf;
const char *name = ftwbuf->path + ftwbuf->nameoff;
@@ -513,31 +568,31 @@ bool eval_hidden(const struct expr *expr, struct eval_state *state) {
/**
* -inum test.
*/
-bool eval_inum(const struct expr *expr, struct eval_state *state) {
+bool eval_inum(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- return expr_cmp(expr, statbuf->ino);
+ return bfs_expr_cmp(expr, statbuf->ino);
}
/**
* -links test.
*/
-bool eval_links(const struct expr *expr, struct eval_state *state) {
+bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- return expr_cmp(expr, statbuf->nlink);
+ return bfs_expr_cmp(expr, statbuf->nlink);
}
/**
* -i?lname test.
*/
-bool eval_lname(const struct expr *expr, struct eval_state *state) {
+bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state) {
bool ret = false;
char *name = NULL;
@@ -555,7 +610,7 @@ bool eval_lname(const struct expr *expr, struct eval_state *state) {
goto done;
}
- ret = fnmatch(expr->sdata, name, expr->idata) == 0;
+ ret = eval_fnmatch(expr, name);
done:
free(name);
@@ -565,7 +620,8 @@ done:
/**
* -i?name test.
*/
-bool eval_name(const struct expr *expr, struct eval_state *state) {
+bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) {
+ bool ret = false;
const struct BFTW *ftwbuf = state->ftwbuf;
const char *name = ftwbuf->path + ftwbuf->nameoff;
@@ -573,18 +629,16 @@ bool eval_name(const struct expr *expr, struct eval_state *state) {
if (ftwbuf->depth == 0) {
// Any trailing slashes are not part of the name. This can only
// happen for the root path.
- const char *slash = strchr(name, '/');
- if (slash && slash > name) {
- copy = strndup(name, slash - name);
- if (!copy) {
- eval_report_error(state);
- return false;
- }
- name = copy;
+ name = copy = xbasename(name);
+ if (!name) {
+ eval_report_error(state);
+ goto done;
}
}
- bool ret = fnmatch(expr->sdata, name, expr->idata) == 0;
+ ret = eval_fnmatch(expr, name);
+
+done:
free(copy);
return ret;
}
@@ -592,15 +646,14 @@ bool eval_name(const struct expr *expr, struct eval_state *state) {
/**
* -i?path test.
*/
-bool eval_path(const struct expr *expr, struct eval_state *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- return fnmatch(expr->sdata, ftwbuf->path, expr->idata) == 0;
+bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state) {
+ return eval_fnmatch(expr, state->ftwbuf->path);
}
/**
* -perm test.
*/
-bool eval_perm(const struct expr *expr, struct eval_state *state) {
+bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -615,37 +668,87 @@ bool eval_perm(const struct expr *expr, struct eval_state *state) {
}
switch (expr->mode_cmp) {
- case MODE_EXACT:
+ case BFS_MODE_EQUAL:
return (mode & 07777) == target;
- case MODE_ALL:
+ case BFS_MODE_ALL:
return (mode & target) == target;
- case MODE_ANY:
+ case BFS_MODE_ANY:
return !(mode & target) == !target;
}
- assert(!"Invalid comparison mode");
+ bfs_bug("Invalid comparison mode");
return false;
}
+/** Print a user/group name/id, and update the column width. */
+static int print_owner(FILE *file, const char *name, uintmax_t id, int *width) {
+ if (name) {
+ int len = xstrwidth(name);
+ if (*width < len) {
+ *width = len;
+ }
+
+ return fprintf(file, " %s%*s", name, *width - len, "");
+ } else {
+ int ret = fprintf(file, " %-*ju", *width, id);
+ if (ret >= 0 && *width < ret - 1) {
+ *width = ret - 1;
+ }
+ return ret;
+ }
+}
+
+/** Print a file's modification time. */
+static int print_time(FILE *file, time_t time, time_t now) {
+ struct tm tm;
+ if (!localtime_r(&time, &tm)) {
+ goto error;
+ }
+
+ char time_str[256];
+ size_t time_ret;
+
+ time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60;
+ time_t tomorrow = now + 24 * 60 * 60;
+ if (time <= six_months_ago || time >= tomorrow) {
+ time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm);
+ } else {
+ time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm);
+ }
+
+ if (time_ret == 0) {
+ goto error;
+ }
+
+ return fprintf(file, " %s", time_str);
+
+error:
+ return fprintf(file, " %jd", (intmax_t)time);
+}
+
/**
* -f?ls action.
*/
-bool eval_fls(const struct expr *expr, struct eval_state *state) {
+bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) {
CFILE *cfile = expr->cfile;
FILE *file = cfile->file;
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
+ const struct bfs_ctx *ctx = state->ctx;
const struct BFTW *ftwbuf = state->ftwbuf;
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
goto done;
}
+ // ls -l prints non-path text in the "normal" color, so do the same
+ if (cfprintf(cfile, "${no}") < 0) {
+ goto error;
+ }
+
uintmax_t ino = statbuf->ino;
- uintmax_t block_size = state->ctx->posixly_correct ? 512 : 1024;
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + block_size - 1)/block_size;
+ uintmax_t block_size = ctx->posixly_correct ? 512 : 1024;
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + block_size - 1) / block_size;
char mode[11];
xstrmode(statbuf->mode, mode);
char acl = bfs_check_acl(ftwbuf) > 0 ? '+' : ' ';
@@ -654,33 +757,21 @@ bool eval_fls(const struct expr *expr, struct eval_state *state) {
goto error;
}
- uintmax_t uid = statbuf->uid;
- const struct passwd *pwd = users ? bfs_getpwuid(users, uid) : NULL;
- if (pwd) {
- if (fprintf(file, " %-8s", pwd->pw_name) < 0) {
- goto error;
- }
- } else {
- if (fprintf(file, " %-8ju", uid) < 0) {
- goto error;
- }
+ const struct passwd *pwd = bfs_getpwuid(ctx->users, statbuf->uid);
+ static int uwidth = 8;
+ if (print_owner(file, pwd ? pwd->pw_name : NULL, statbuf->uid, &uwidth) < 0) {
+ goto error;
}
- uintmax_t gid = statbuf->gid;
- const struct group *grp = groups ? bfs_getgrgid(groups, gid) : NULL;
- if (grp) {
- if (fprintf(file, " %-8s", grp->gr_name) < 0) {
- goto error;
- }
- } else {
- if (fprintf(file, " %-8ju", gid) < 0) {
- goto error;
- }
+ const struct group *grp = bfs_getgrgid(ctx->groups, statbuf->gid);
+ static int gwidth = 8;
+ if (print_owner(file, grp ? grp->gr_name : NULL, statbuf->gid, &gwidth) < 0) {
+ goto error;
}
if (ftwbuf->type == BFS_BLK || ftwbuf->type == BFS_CHR) {
- int ma = bfs_major(statbuf->rdev);
- int mi = bfs_minor(statbuf->rdev);
+ int ma = xmajor(statbuf->rdev);
+ int mi = xminor(statbuf->rdev);
if (fprintf(file, " %3d, %3d", ma, mi) < 0) {
goto error;
}
@@ -692,27 +783,12 @@ bool eval_fls(const struct expr *expr, struct eval_state *state) {
}
time_t time = statbuf->mtime.tv_sec;
- time_t now = expr->reftime.tv_sec;
- time_t six_months_ago = now - 6*30*24*60*60;
- time_t tomorrow = now + 24*60*60;
- struct tm tm;
- if (xlocaltime(&time, &tm) != 0) {
- goto error;
- }
- char time_str[256];
- const char *time_format = "%b %e %H:%M";
- if (time <= six_months_ago || time >= tomorrow) {
- time_format = "%b %e %Y";
- }
- if (!strftime(time_str, sizeof(time_str), time_format, &tm)) {
- errno = EOVERFLOW;
- goto error;
- }
- if (fprintf(file, " %s", time_str) < 0) {
+ time_t now = ctx->now.tv_sec;
+ if (print_time(file, time, now) < 0) {
goto error;
}
- if (cfprintf(cfile, " %pP", ftwbuf) < 0) {
+ if (cfprintf(cfile, "${rs} %pP", ftwbuf) < 0) {
goto error;
}
@@ -730,16 +806,16 @@ done:
return true;
error:
- eval_report_error(state);
+ eval_io_error(expr, state);
return true;
}
/**
* -f?print action.
*/
-bool eval_fprint(const struct expr *expr, struct eval_state *state) {
+bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state) {
if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) {
- eval_report_error(state);
+ eval_io_error(expr, state);
}
return true;
}
@@ -747,11 +823,11 @@ bool eval_fprint(const struct expr *expr, struct eval_state *state) {
/**
* -f?print0 action.
*/
-bool eval_fprint0(const struct expr *expr, struct eval_state *state) {
+bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) {
const char *path = state->ftwbuf->path;
size_t length = strlen(path) + 1;
if (fwrite(path, 1, length, expr->cfile->file) != length) {
- eval_report_error(state);
+ eval_io_error(expr, state);
}
return true;
}
@@ -759,9 +835,9 @@ bool eval_fprint0(const struct expr *expr, struct eval_state *state) {
/**
* -f?printf action.
*/
-bool eval_fprintf(const struct expr *expr, struct eval_state *state) {
- if (bfs_printf(expr->cfile->file, expr->printf, state->ftwbuf) != 0) {
- eval_report_error(state);
+bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state) {
+ if (bfs_printf(expr->cfile, expr->printf, state->ftwbuf) != 0) {
+ eval_io_error(expr, state);
}
return true;
@@ -770,7 +846,7 @@ bool eval_fprintf(const struct expr *expr, struct eval_state *state) {
/**
* -printx action.
*/
-bool eval_fprintx(const struct expr *expr, struct eval_state *state) {
+bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) {
FILE *file = expr->cfile->file;
const char *path = state->ftwbuf->path;
@@ -793,7 +869,6 @@ bool eval_fprintx(const struct expr *expr, struct eval_state *state) {
++path;
}
-
if (fputc('\n', file) == EOF) {
goto error;
}
@@ -801,14 +876,27 @@ bool eval_fprintx(const struct expr *expr, struct eval_state *state) {
return true;
error:
- eval_report_error(state);
+ eval_io_error(expr, state);
+ return true;
+}
+
+/**
+ * -limit action.
+ */
+bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state) {
+ long long evals = expr->evaluations + 1;
+ if (evals >= expr->num) {
+ state->action = BFTW_STOP;
+ state->quit = true;
+ }
+
return true;
}
/**
* -prune action.
*/
-bool eval_prune(const struct expr *expr, struct eval_state *state) {
+bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state) {
state->action = BFTW_PRUNE;
return true;
}
@@ -816,7 +904,7 @@ bool eval_prune(const struct expr *expr, struct eval_state *state) {
/**
* -quit action.
*/
-bool eval_quit(const struct expr *expr, struct eval_state *state) {
+bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state) {
state->action = BFTW_STOP;
state->quit = true;
return true;
@@ -825,40 +913,27 @@ bool eval_quit(const struct expr *expr, struct eval_state *state) {
/**
* -i?regex test.
*/
-bool eval_regex(const struct expr *expr, struct eval_state *state) {
+bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state) {
const char *path = state->ftwbuf->path;
- size_t len = strlen(path);
- regmatch_t match = {
- .rm_so = 0,
- .rm_eo = len,
- };
- int flags = 0;
-#ifdef REG_STARTEND
- flags |= REG_STARTEND;
-#endif
- int err = regexec(expr->regex, path, 1, &match, flags);
- if (err == 0) {
- return match.rm_so == 0 && (size_t)match.rm_eo == len;
- } else if (err != REG_NOMATCH) {
- char *str = xregerror(err, expr->regex);
+ int ret = bfs_regexec(expr->regex, path, BFS_REGEX_ANCHOR);
+ if (ret < 0) {
+ char *str = bfs_regerror(expr->regex);
if (str) {
eval_error(state, "%s.\n", str);
free(str);
} else {
- eval_error(state, "xregerror(): %m.\n");
+ eval_error(state, "bfs_regerror(): %s.\n", errstr());
}
-
- *state->ret = EXIT_FAILURE;
}
- return false;
+ return ret > 0;
}
/**
* -samefile test.
*/
-bool eval_samefile(const struct expr *expr, struct eval_state *state) {
+bool eval_samefile(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -870,52 +945,52 @@ bool eval_samefile(const struct expr *expr, struct eval_state *state) {
/**
* -size test.
*/
-bool eval_size(const struct expr *expr, struct eval_state *state) {
+bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
static const off_t scales[] = {
- [SIZE_BLOCKS] = 512,
- [SIZE_BYTES] = 1,
- [SIZE_WORDS] = 2,
- [SIZE_KB] = 1LL << 10,
- [SIZE_MB] = 1LL << 20,
- [SIZE_GB] = 1LL << 30,
- [SIZE_TB] = 1LL << 40,
- [SIZE_PB] = 1LL << 50,
+ [BFS_BLOCKS] = 512,
+ [BFS_BYTES] = 1,
+ [BFS_WORDS] = 2,
+ [BFS_KB] = 1LL << 10,
+ [BFS_MB] = 1LL << 20,
+ [BFS_GB] = 1LL << 30,
+ [BFS_TB] = 1LL << 40,
+ [BFS_PB] = 1LL << 50,
};
off_t scale = scales[expr->size_unit];
- off_t size = (statbuf->size + scale - 1)/scale; // Round up
- return expr_cmp(expr, size);
+ off_t size = (statbuf->size + scale - 1) / scale; // Round up
+ return bfs_expr_cmp(expr, size);
}
/**
* -sparse test.
*/
-bool eval_sparse(const struct expr *expr, struct eval_state *state) {
+bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
}
- blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1)/BFS_STAT_BLKSIZE;
+ blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1) / BFS_STAT_BLKSIZE;
return statbuf->blocks < expected;
}
/**
* -type test.
*/
-bool eval_type(const struct expr *expr, struct eval_state *state) {
- return (1 << state->ftwbuf->type) & expr->idata;
+bool eval_type(const struct bfs_expr *expr, struct bfs_eval *state) {
+ return (1 << state->ftwbuf->type) & expr->num;
}
/**
* -xattr test.
*/
-bool eval_xattr(const struct expr *expr, struct eval_state *state) {
+bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state) {
int ret = bfs_check_xattrs(state->ftwbuf);
if (ret >= 0) {
return ret;
@@ -926,10 +1001,10 @@ bool eval_xattr(const struct expr *expr, struct eval_state *state) {
}
/**
- * -xattr test.
+ * -xattrname test.
*/
-bool eval_xattrname(const struct expr *expr, struct eval_state *state) {
- int ret = bfs_check_xattr_named(state->ftwbuf, expr->sdata);
+bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state) {
+ int ret = bfs_check_xattr_named(state->ftwbuf, expr->argv[1]);
if (ret >= 0) {
return ret;
} else {
@@ -941,58 +1016,48 @@ bool eval_xattrname(const struct expr *expr, struct eval_state *state) {
/**
* -xtype test.
*/
-bool eval_xtype(const struct expr *expr, struct eval_state *state) {
+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;
} else {
- return (1 << type) & expr->idata;
+ return (1 << type) & expr->num;
}
}
-#if _POSIX_MONOTONIC_CLOCK > 0
-# define BFS_CLOCK CLOCK_MONOTONIC
-#elif _POSIX_TIMERS > 0
-# define BFS_CLOCK CLOCK_REALTIME
-#endif
-
/**
- * Call clock_gettime(), if available.
+ * clock_gettime() wrapper.
*/
-static int eval_gettime(struct eval_state *state, struct timespec *ts) {
-#ifdef BFS_CLOCK
- int ret = clock_gettime(BFS_CLOCK, ts);
- if (ret != 0) {
- bfs_warning(state->ctx, "%pP: clock_gettime(): %m.\n", state->ftwbuf);
+static int eval_gettime(struct bfs_eval *state, struct timespec *ts) {
+ clockid_t clock = CLOCK_REALTIME;
+
+#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0
+ if (sysoption(MONOTONIC_CLOCK) > 0) {
+ clock = CLOCK_MONOTONIC;
}
- return ret;
-#else
- return -1;
#endif
-}
-/**
- * Record an elapsed time.
- */
-static void timespec_elapsed(struct timespec *elapsed, const struct timespec *start, const struct timespec *end) {
- elapsed->tv_sec += end->tv_sec - start->tv_sec;
- elapsed->tv_nsec += end->tv_nsec - start->tv_nsec;
- if (elapsed->tv_nsec < 0) {
- elapsed->tv_nsec += 1000000000L;
- --elapsed->tv_sec;
- } else if (elapsed->tv_nsec >= 1000000000L) {
- elapsed->tv_nsec -= 1000000000L;
- ++elapsed->tv_sec;
+ int ret = clock_gettime(clock, ts);
+ if (ret != 0) {
+ bfs_warning(state->ctx, "%pP: clock_gettime(): %s.\n", state->ftwbuf, errstr());
}
+ return ret;
}
/**
* Evaluate an expression.
*/
-static bool eval_expr(struct expr *expr, struct eval_state *state) {
+static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) {
struct timespec start, end;
bool time = state->ctx->debug & DEBUG_RATES;
if (time) {
@@ -1001,13 +1066,14 @@ static bool eval_expr(struct expr *expr, struct eval_state *state) {
}
}
- assert(!state->quit);
+ bfs_assert(!state->quit);
- bool ret = expr->eval(expr, state);
+ bool ret = expr->eval_fn(expr, state);
if (time) {
if (eval_gettime(state, &end) == 0) {
- timespec_elapsed(&expr->elapsed, &start, &end);
+ timespec_sub(&end, &start);
+ timespec_add(&expr->elapsed, &end);
}
}
@@ -1016,11 +1082,11 @@ static bool eval_expr(struct expr *expr, struct eval_state *state) {
++expr->successes;
}
- if (expr_never_returns(expr)) {
- assert(state->quit);
+ if (bfs_expr_never_returns(expr)) {
+ bfs_assert(state->quit);
} else if (!state->quit) {
- assert(!expr->always_true || ret);
- assert(!expr->always_false || !ret);
+ bfs_assert(!expr->always_true || ret);
+ bfs_assert(!expr->always_false || !ret);
}
return ret;
@@ -1029,68 +1095,54 @@ static bool eval_expr(struct expr *expr, struct eval_state *state) {
/**
* Evaluate a negation.
*/
-bool eval_not(const struct expr *expr, struct eval_state *state) {
- return !eval_expr(expr->rhs, state);
+bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) {
+ return !eval_expr(bfs_expr_children(expr), state);
}
/**
* Evaluate a conjunction.
*/
-bool eval_and(const struct expr *expr, struct eval_state *state) {
- if (!eval_expr(expr->lhs, state)) {
- return false;
- }
-
- if (state->quit) {
- return false;
+bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) {
+ for_expr (child, expr) {
+ if (!eval_expr(child, state) || state->quit) {
+ return false;
+ }
}
- return eval_expr(expr->rhs, state);
+ return true;
}
/**
* Evaluate a disjunction.
*/
-bool eval_or(const struct expr *expr, struct eval_state *state) {
- if (eval_expr(expr->lhs, state)) {
- return true;
- }
-
- if (state->quit) {
- return false;
+bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) {
+ for_expr (child, expr) {
+ if (eval_expr(child, state) || state->quit) {
+ return true;
+ }
}
- return eval_expr(expr->rhs, state);
+ return false;
}
/**
* Evaluate the comma operator.
*/
-bool eval_comma(const struct expr *expr, struct eval_state *state) {
- eval_expr(expr->lhs, state);
+bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) {
+ bool ret uninit(false);
- if (state->quit) {
- return false;
+ for_expr (child, expr) {
+ ret = eval_expr(child, state);
+ if (state->quit) {
+ break;
+ }
}
- return eval_expr(expr->rhs, state);
+ return ret;
}
/** Update the status bar. */
-static void eval_status(struct eval_state *state, struct bfs_bar *bar, struct timespec *last_status, size_t count) {
- struct timespec now;
- if (eval_gettime(state, &now) == 0) {
- struct timespec elapsed = {0};
- timespec_elapsed(&elapsed, last_status, &now);
-
- // Update every 0.1s
- if (elapsed.tv_sec > 0 || elapsed.tv_nsec >= 100000000L) {
- *last_status = now;
- } else {
- return;
- }
- }
-
+static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, size_t count) {
size_t width = bfs_bar_width(bar);
if (width < 3) {
return;
@@ -1098,20 +1150,21 @@ static void eval_status(struct eval_state *state, struct bfs_bar *bar, struct ti
const struct BFTW *ftwbuf = state->ftwbuf;
- char *rhs = dstrprintf(" (visited: %zu, depth: %2zu)", count, ftwbuf->depth);
+ dchar *status = NULL;
+ dchar *rhs = dstrprintf(" (visited: %'zu; depth: %2zu)", count, ftwbuf->depth);
if (!rhs) {
return;
}
- size_t rhslen = dstrlen(rhs);
+ size_t rhslen = xstrwidth(rhs);
if (3 + rhslen > width) {
- dstresize(&rhs, 0);
+ dstrshrink(rhs, 0);
rhslen = 0;
}
- char *status = dstralloc(0);
+ status = dstralloc(0);
if (!status) {
- goto out_rhs;
+ goto out;
}
const char *path = ftwbuf->path;
@@ -1120,26 +1173,25 @@ static void eval_status(struct eval_state *state, struct bfs_bar *bar, struct ti
pathlen = strlen(path);
}
+ // Escape weird filename characters
+ if (dstrnescat(&status, path, pathlen, WESC_TTY) != 0) {
+ goto out;
+ }
+ pathlen = dstrlen(status);
+
// Try to make sure even wide characters fit in the status bar
size_t pathmax = width - rhslen - 3;
size_t pathwidth = 0;
- mbstate_t mb;
- memset(&mb, 0, sizeof(mb));
- while (pathlen > 0) {
- wchar_t wc;
- size_t len = mbrtowc(&wc, path, pathlen, &mb);
+ size_t lhslen = 0;
+ mbstate_t mb = {0};
+ for (size_t i = lhslen; lhslen < pathlen; lhslen = i) {
+ wint_t wc = xmbrtowc(status, &i, pathlen, &mb);
int cwidth;
- if (len == (size_t)-1) {
+ if (wc == WEOF) {
// Invalid byte sequence, assume a single-width '?'
- len = 1;
- cwidth = 1;
- memset(&mb, 0, sizeof(mb));
- } else if (len == (size_t)-2) {
- // Incomplete byte sequence, assume a single-width '?'
- len = pathlen;
cwidth = 1;
} else {
- cwidth = wcwidth(wc);
+ cwidth = xwcwidth(wc);
if (cwidth < 0) {
cwidth = 0;
}
@@ -1148,40 +1200,34 @@ static void eval_status(struct eval_state *state, struct bfs_bar *bar, struct ti
if (pathwidth + cwidth > pathmax) {
break;
}
-
- if (dstrncat(&status, path, len) != 0) {
- goto out_rhs;
- }
-
- path += len;
- pathlen -= len;
pathwidth += cwidth;
}
+ dstrshrink(status, lhslen);
if (dstrcat(&status, "...") != 0) {
- goto out_rhs;
+ goto out;
}
while (pathwidth < pathmax) {
if (dstrapp(&status, ' ') != 0) {
- goto out_rhs;
+ goto out;
}
++pathwidth;
}
- if (dstrcat(&status, rhs) != 0) {
- goto out_rhs;
+ if (dstrdcat(&status, rhs) != 0) {
+ goto out;
}
bfs_bar_update(bar, status);
+out:
dstrfree(status);
-out_rhs:
dstrfree(rhs);
}
/** Check if we've seen a file before. */
-static bool eval_file_unique(struct eval_state *state, struct trie *seen) {
+static bool eval_file_unique(struct bfs_eval *state, struct trie *seen) {
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
return false;
@@ -1205,25 +1251,25 @@ static bool eval_file_unique(struct eval_state *state, struct trie *seen) {
}
}
-#define DEBUG_FLAG(flags, flag) \
- do { \
- if ((flags & flag) || flags == flag) { \
- fputs(#flag, stderr); \
- flags ^= flag; \
- if (flags) { \
- fputs(" | ", stderr); \
- } \
- } \
+#define DEBUG_FLAG(flags, flag) \
+ do { \
+ if ((flags & flag) || flags == flag) { \
+ fputs(#flag, stderr); \
+ flags ^= flag; \
+ if (flags) { \
+ fputs(" | ", stderr); \
+ } \
+ } \
} while (0)
/**
* Log a stat() call.
*/
-static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flags flags) {
+static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, int err) {
bfs_debug_prefix(ctx, DEBUG_STAT);
fprintf(stderr, "bfs_stat(");
- if (ftwbuf->at_fd == AT_FDCWD) {
+ if (ftwbuf->at_fd == (int)AT_FDCWD) {
fprintf(stderr, "AT_FDCWD");
} else {
size_t baselen = strlen(ftwbuf->path) - strlen(ftwbuf->at_path);
@@ -1237,11 +1283,12 @@ static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, con
DEBUG_FLAG(flags, BFS_STAT_FOLLOW);
DEBUG_FLAG(flags, BFS_STAT_NOFOLLOW);
DEBUG_FLAG(flags, BFS_STAT_TRYFOLLOW);
+ DEBUG_FLAG(flags, BFS_STAT_NOSYNC);
- fprintf(stderr, ") == %d", cache->buf ? 0 : -1);
+ fprintf(stderr, ") == %d", err == 0 ? 0 : -1);
- if (cache->error) {
- fprintf(stderr, " [%d]", cache->error);
+ if (err) {
+ fprintf(stderr, " [%d]", err);
}
fprintf(stderr, "\n");
@@ -1255,14 +1302,14 @@ static void debug_stats(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf) {
return;
}
- const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
- if (statbuf || ftwbuf->stat_cache.error) {
- debug_stat(ctx, ftwbuf, &ftwbuf->stat_cache, BFS_STAT_FOLLOW);
+ const struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+
+ if (bufs->stat_err >= 0) {
+ debug_stat(ctx, ftwbuf, BFS_STAT_FOLLOW, bufs->stat_err);
}
- const struct bfs_stat *lstatbuf = ftwbuf->lstat_cache.buf;
- if ((lstatbuf && lstatbuf != statbuf) || ftwbuf->lstat_cache.error) {
- debug_stat(ctx, ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW);
+ if (bufs->lstat_err >= 0) {
+ debug_stat(ctx, ftwbuf, BFS_STAT_NOFOLLOW, bufs->lstat_err);
}
}
@@ -1325,18 +1372,85 @@ struct callback_args {
/** The status bar. */
struct bfs_bar *bar;
- /** The time of the last status update. */
- struct timespec last_status;
+ /** The SIGALRM hook. */
+ struct sighook *alrm_hook;
+ /** The interval timer. */
+ struct timer *timer;
+ /** Flag set by SIGALRM. */
+ atomic bool alrm_flag;
+ /** Flag set by SIGINFO. */
+ atomic bool info_flag;
+
/** The number of files visited so far. */
size_t count;
/** The set of seen files. */
struct trie *seen;
+ /** The number of errors that have occurred. */
+ size_t nerrors;
/** Eventual return value from bfs_eval(). */
int ret;
};
+/** Update the status bar in response to SIGALRM. */
+static void eval_sigalrm(int sig, siginfo_t *info, void *ptr) {
+ struct callback_args *args = ptr;
+ store(&args->alrm_flag, true, relaxed);
+}
+
+/** Show/hide the bar in response to SIGINFO. */
+static void eval_siginfo(int sig, siginfo_t *info, void *ptr) {
+ struct callback_args *args = ptr;
+ store(&args->info_flag, true, relaxed);
+}
+
+/** Show the status bar. */
+static void eval_show_bar(struct callback_args *args) {
+ args->alrm_hook = sighook(SIGALRM, eval_sigalrm, args, SH_CONTINUE);
+ if (!args->alrm_hook) {
+ goto fail;
+ }
+
+ args->bar = bfs_bar_show();
+ if (!args->bar) {
+ goto fail;
+ }
+
+ // Update the bar every 0.1s
+ struct timespec ival = { .tv_nsec = 100 * 1000 * 1000 };
+ args->timer = xtimer_start(&ival);
+ if (!args->timer) {
+ goto fail;
+ }
+
+ // Update the bar immediately
+ store(&args->alrm_flag, true, relaxed);
+
+ return;
+
+fail:
+ bfs_warning(args->ctx, "Couldn't show status bar: %s.\n\n", errstr());
+
+ bfs_bar_hide(args->bar);
+ args->bar = NULL;
+
+ sigunhook(args->alrm_hook);
+ args->alrm_hook = NULL;
+}
+
+/** Hide the status bar. */
+static void eval_hide_bar(struct callback_args *args) {
+ xtimer_stop(args->timer);
+ args->timer = NULL;
+
+ sigunhook(args->alrm_hook);
+ args->alrm_hook = NULL;
+
+ bfs_bar_hide(args->bar);
+ args->bar = NULL;
+}
+
/**
* bftw() callback.
*/
@@ -1346,22 +1460,42 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) {
const struct bfs_ctx *ctx = args->ctx;
- struct eval_state state;
+ struct bfs_eval state;
state.ftwbuf = ftwbuf;
state.ctx = ctx;
state.action = BFTW_CONTINUE;
state.ret = &args->ret;
+ state.nerrors = &args->nerrors;
state.quit = false;
- if (args->bar) {
- eval_status(&state, args->bar, &args->last_status, args->count);
+ // Check whether SIGINFO was delivered and show/hide the bar
+ if (exchange(&args->info_flag, false, relaxed)) {
+ if (args->bar) {
+ eval_hide_bar(args);
+ } else {
+ eval_show_bar(args);
+ }
+ }
+
+ if (exchange(&args->alrm_flag, false, relaxed)) {
+ eval_status(&state, args->bar, args->count);
}
if (ftwbuf->type == BFS_ERROR) {
- if (!eval_should_ignore(&state, ftwbuf->error)) {
- eval_error(&state, "%s.\n", strerror(ftwbuf->error));
- }
state.action = BFTW_PRUNE;
+
+ if (ftwbuf->error == ELOOP && ftwbuf->loopoff > 0) {
+ char *loop = strndup(ftwbuf->path, ftwbuf->loopoff);
+ if (loop) {
+ eval_error(&state, "Filesystem loop back to ${di}%pq${rs}\n", loop);
+ free(loop);
+ goto done;
+ }
+ } else if (eval_should_ignore(&state, ftwbuf->error)) {
+ goto done;
+ }
+
+ eval_error(&state, "%s.\n", xstrerror(ftwbuf->error));
goto done;
}
@@ -1416,38 +1550,51 @@ done:
return state.action;
}
-/** Compare two rlimit values, accounting for RLIM_INFINITY etc. */
-static int rlim_cmp(rlim_t a, rlim_t b) {
- // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite
- bool a_inf = a == RLIM_INFINITY || a == RLIM_SAVED_CUR || a == RLIM_SAVED_MAX;
- bool b_inf = b == RLIM_INFINITY || b == RLIM_SAVED_CUR || b == RLIM_SAVED_MAX;
- if (a_inf || b_inf) {
- return a_inf - b_inf;
+/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
+static int raise_fdlimit(struct bfs_ctx *ctx) {
+ rlim_t cur = ctx->orig_nofile.rlim_cur;
+ rlim_t max = ctx->orig_nofile.rlim_max;
+ if (!ctx->raise_nofile) {
+ max = cur;
}
- return (a > b) - (a < b);
-}
-
-/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
-static int raise_fdlimit(const struct bfs_ctx *ctx) {
rlim_t target = 64 << 10;
- if (rlim_cmp(target, ctx->nofile_hard) > 0) {
- target = ctx->nofile_hard;
+ if (rlim_cmp(target, max) > 0) {
+ target = max;
}
- int ret = target;
+ if (rlim_cmp(target, cur) <= 0) {
+ return target;
+ }
- if (rlim_cmp(target, ctx->nofile_soft) > 0) {
- const struct rlimit rl = {
- .rlim_cur = target,
- .rlim_max = ctx->nofile_hard,
- };
- if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
- ret = ctx->nofile_soft;
- }
+ const struct rlimit rl = {
+ .rlim_cur = target,
+ .rlim_max = max,
+ };
+
+ if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
+ return cur;
}
- return ret;
+ ctx->cur_nofile = rl;
+ return target;
+}
+
+/** Preallocate the fd table in the kernel. */
+static void reserve_fds(int limit) {
+ // Kernels typically implement the fd table as a dynamic array.
+ // Growing the array can be expensive, especially if files are being
+ // opened in parallel. We can work around this by allocating the
+ // highest possible fd, forcing the kernel to grow the table upfront.
+
+#ifdef F_DUPFD_CLOEXEC
+ int fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, limit - 1);
+#else
+ int fd = fcntl(STDIN_FILENO, F_DUPFD, limit - 1);
+#endif
+ if (fd >= 0) {
+ xclose(fd);
+ }
}
/** Infer the number of file descriptors available to bftw(). */
@@ -1457,20 +1604,25 @@ static int infer_fdlimit(const struct bfs_ctx *ctx, int limit) {
// Check /proc/self/fd for the current number of open fds, if possible
// (we may have inherited more than just the standard ones)
- struct bfs_dir *dir = bfs_opendir(AT_FDCWD, "/proc/self/fd");
+ struct bfs_dir *dir = bfs_allocdir();
if (!dir) {
- dir = bfs_opendir(AT_FDCWD, "/dev/fd");
+ goto done;
}
- if (dir) {
- // Account for 'dir' itself
- nopen = -1;
- while (bfs_readdir(dir, NULL) > 0) {
- ++nopen;
- }
+ if (bfs_opendir(dir, AT_FDCWD, "/proc/self/fd", 0) != 0
+ && bfs_opendir(dir, AT_FDCWD, "/dev/fd", 0) != 0) {
+ goto done;
+ }
- bfs_closedir(dir);
+ // Account for 'dir' itself
+ nopen = -1;
+
+ while (bfs_readdir(dir, NULL) > 0) {
+ ++nopen;
}
+ bfs_closedir(dir);
+done:
+ free(dir);
int ret = limit - nopen;
ret -= ctx->expr->persistent_fds;
@@ -1498,8 +1650,10 @@ static void dump_bftw_flags(enum bftw_flags flags) {
DEBUG_FLAG(flags, BFTW_SKIP_MOUNTS);
DEBUG_FLAG(flags, BFTW_PRUNE_MOUNTS);
DEBUG_FLAG(flags, BFTW_SORT);
+ DEBUG_FLAG(flags, BFTW_BUFFER);
+ DEBUG_FLAG(flags, BFTW_WHITEOUTS);
- assert(!flags);
+ bfs_assert(flags == 0, "Missing bftw flag 0x%X", flags);
}
/**
@@ -1515,7 +1669,33 @@ static const char *dump_bftw_strategy(enum bftw_strategy strategy) {
return strategies[strategy];
}
-int bfs_eval(const struct bfs_ctx *ctx) {
+/** Check if we need to enable BFTW_BUFFER. */
+static bool eval_must_buffer(const struct bfs_expr *expr) {
+#if __FreeBSD__
+ // FreeBSD doesn't properly handle adding/removing directory entries
+ // during readdir() on NFS mounts. Work around it by passing BFTW_BUFFER
+ // whenever we could be mutating the directory ourselves through -delete
+ // or -exec. We don't attempt to handle concurrent modification by other
+ // processes, which are racey anyway.
+ //
+ // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=57696
+ // https://github.com/tavianator/bfs/issues/67
+
+ if (expr->eval_fn == eval_delete || expr->eval_fn == eval_exec) {
+ return true;
+ }
+
+ for_expr (child, expr) {
+ if (eval_must_buffer(child)) {
+ return true;
+ }
+ }
+#endif // __FreeBSD__
+
+ return false;
+}
+
+int bfs_eval(struct bfs_ctx *ctx) {
if (!ctx->expr) {
return EXIT_SUCCESS;
}
@@ -1526,12 +1706,16 @@ int bfs_eval(const struct bfs_ctx *ctx) {
};
if (ctx->status) {
- args.bar = bfs_bar_show();
- if (!args.bar) {
- bfs_warning(ctx, "Couldn't show status bar: %m.\n\n");
- }
+ eval_show_bar(&args);
}
+#ifdef SIGINFO
+ int siginfo = SIGINFO;
+#else
+ int siginfo = SIGUSR1;
+#endif
+ struct sighook *info_hook = sighook(siginfo, eval_siginfo, &args, SH_CONTINUE);
+
struct trie seen;
if (ctx->unique) {
trie_init(&seen);
@@ -1539,19 +1723,28 @@ int bfs_eval(const struct bfs_ctx *ctx) {
}
int fdlimit = raise_fdlimit(ctx);
+ reserve_fds(fdlimit);
fdlimit = infer_fdlimit(ctx, fdlimit);
+ // -1 for the main thread
+ int nthreads = ctx->threads - 1;
+
struct bftw_args bftw_args = {
.paths = ctx->paths,
- .npaths = darray_length(ctx->paths),
+ .npaths = ctx->npaths,
.callback = eval_callback,
.ptr = &args,
.nopenfd = fdlimit,
+ .nthreads = nthreads,
.flags = ctx->flags,
.strategy = ctx->strategy,
.mtab = bfs_ctx_mtab(ctx),
};
+ if (eval_must_buffer(ctx->expr)) {
+ bftw_args.flags |= BFTW_BUFFER;
+ }
+
if (bfs_debug(ctx, DEBUG_SEARCH, "bftw({\n")) {
fprintf(stderr, "\t.paths = {\n");
for (size_t i = 0; i < bftw_args.npaths; ++i) {
@@ -1562,6 +1755,7 @@ int bfs_eval(const struct bfs_ctx *ctx) {
fprintf(stderr, "\t.callback = eval_callback,\n");
fprintf(stderr, "\t.ptr = &args,\n");
fprintf(stderr, "\t.nopenfd = %d,\n", bftw_args.nopenfd);
+ fprintf(stderr, "\t.nthreads = %d,\n", bftw_args.nthreads);
fprintf(stderr, "\t.flags = ");
dump_bftw_flags(bftw_args.flags);
fprintf(stderr, ",\n\t.strategy = %s,\n", dump_bftw_strategy(bftw_args.strategy));
@@ -1589,7 +1783,14 @@ int bfs_eval(const struct bfs_ctx *ctx) {
trie_destroy(&seen);
}
- bfs_bar_hide(args.bar);
+ sigunhook(info_hook);
+ if (args.bar) {
+ eval_hide_bar(&args);
+ }
+
+ if (ctx->ignore_errors && args.nerrors > 0) {
+ bfs_warning(ctx, "Suppressed errors: %zu\n", args.nerrors);
+ }
return args.ret;
}
diff --git a/src/eval.h b/src/eval.h
new file mode 100644
index 0000000..b038740
--- /dev/null
+++ b/src/eval.h
@@ -0,0 +1,100 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * The evaluation functions that implement primary expressions like -name,
+ * -print, etc.
+ */
+
+#ifndef BFS_EVAL_H
+#define BFS_EVAL_H
+
+struct bfs_ctx;
+struct bfs_expr;
+
+/**
+ * Ephemeral state for evaluating an expression.
+ */
+struct bfs_eval;
+
+/**
+ * Expression evaluation function.
+ *
+ * @expr
+ * The current expression.
+ * @state
+ * The current evaluation state.
+ * @return
+ * The result of the test.
+ */
+typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state);
+
+/**
+ * Evaluate the command line.
+ *
+ * @ctx
+ * The bfs context to evaluate.
+ * @return
+ * EXIT_SUCCESS on success, otherwise on failure.
+ */
+int bfs_eval(struct bfs_ctx *ctx);
+
+// Predicate evaluation functions
+
+bool eval_true(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_newer(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_gid(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_uid(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_hidden(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_inum(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_links(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_samefile(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_type(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_regex(const struct bfs_expr *expr, struct bfs_eval *state);
+
+bool eval_delete(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_exit(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state);
+
+// Operator evaluation functions
+bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state);
+
+#endif // BFS_EVAL_H
diff --git a/exec.c b/src/exec.c
index 431fcbe..45c9f1d 100644
--- a/exec.c
+++ b/src/exec.c
@@ -1,32 +1,21 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2018 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#include "exec.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
#include "bftw.h"
-#include "ctx.h"
#include "color.h"
+#include "ctx.h"
#include "diag.h"
#include "dstring.h"
-#include "spawn.h"
-#include "util.h"
-#include <assert.h>
+#include "xspawn.h"
+
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
-#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -35,7 +24,7 @@
#include <unistd.h>
/** Print some debugging info. */
-BFS_FORMATTER(2, 3)
+_printf(2, 3)
static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) {
const struct bfs_ctx *ctx = execbuf->ctx;
@@ -59,8 +48,6 @@ static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, .
va_end(args);
}
-extern char **environ;
-
/** Determine the size of a single argument, for comparison to arg_max. */
static size_t bfs_exec_arg_size(const char *arg) {
return sizeof(arg) + strlen(arg) + 1;
@@ -71,7 +58,7 @@ static size_t bfs_exec_arg_size(const char *arg) {
/** Determine the maximum argv size. */
static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
- long arg_max = sysconf(_SC_ARG_MAX);
+ long arg_max = xsysconf(_SC_ARG_MAX);
bfs_exec_debug(execbuf, "ARG_MAX: %ld according to sysconf()\n", arg_max);
if (arg_max < 0) {
arg_max = BFS_EXEC_ARG_MAX;
@@ -79,6 +66,7 @@ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
}
// We have to share space with the environment variables
+ extern char **environ;
for (char **envp = environ; *envp; ++envp) {
arg_max -= bfs_exec_arg_size(*envp);
}
@@ -96,7 +84,7 @@ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
// Assume arguments are counted with the granularity of a single page,
// so allow a one page cushion to account for rounding up
- long page_size = sysconf(_SC_PAGESIZE);
+ long page_size = xsysconf(_SC_PAGESIZE);
if (page_size < 4096) {
page_size = 4096;
}
@@ -117,65 +105,82 @@ static size_t bfs_exec_arg_max(const struct bfs_exec *execbuf) {
return arg_max;
}
+/** Highlight part of the command line as an error. */
+static void bfs_exec_parse_error(const struct bfs_ctx *ctx, const struct bfs_exec *execbuf) {
+ char **argv = execbuf->tmpl_argv - 1;
+ size_t argc = execbuf->tmpl_argc + 1;
+ if (argv[argc]) {
+ ++argc;
+ }
+
+ bool args[ctx->argc];
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ args[i] = false;
+ }
+
+ size_t i = argv - ctx->argv;
+ for (size_t j = 0; j < argc; ++j) {
+ args[i + j] = true;
+ }
+
+ bfs_argv_error(ctx, args);
+}
+
struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags) {
- struct bfs_exec *execbuf = malloc(sizeof(*execbuf));
+ struct bfs_exec *execbuf = ZALLOC(struct bfs_exec);
if (!execbuf) {
- bfs_perror(ctx, "malloc()");
+ bfs_perror(ctx, "zalloc()");
goto fail;
}
execbuf->flags = flags;
execbuf->ctx = ctx;
- execbuf->argv = NULL;
- execbuf->argc = 0;
- execbuf->argv_cap = 0;
- execbuf->arg_size = 0;
- execbuf->arg_max = 0;
+ execbuf->tmpl_argv = argv + 1;
execbuf->wd_fd = -1;
- execbuf->wd_path = NULL;
- execbuf->wd_len = 0;
- execbuf->ret = 0;
- size_t i;
- for (i = 1; ; ++i) {
- const char *arg = argv[i];
+ while (true) {
+ const char *arg = execbuf->tmpl_argv[execbuf->tmpl_argc];
if (!arg) {
if (execbuf->flags & BFS_EXEC_CONFIRM) {
- bfs_error(ctx, "%s: Expected '... ;'.\n", argv[0]);
+ bfs_exec_parse_error(ctx, execbuf);
+ bfs_error(ctx, "Expected '... ;'.\n");
} else {
- bfs_error(ctx, "%s: Expected '... ;' or '... {} +'.\n", argv[0]);
+ bfs_exec_parse_error(ctx, execbuf);
+ bfs_error(ctx, "Expected '... ;' or '... {} +'.\n");
}
goto fail;
} else if (strcmp(arg, ";") == 0) {
break;
- } else if (strcmp(arg, "+") == 0) {
- if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(argv[i - 1], "{}") == 0) {
+ } else if (execbuf->tmpl_argc > 0 && strcmp(arg, "+") == 0) {
+ const char *prev = execbuf->tmpl_argv[execbuf->tmpl_argc - 1];
+ if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(prev, "{}") == 0) {
execbuf->flags |= BFS_EXEC_MULTI;
break;
}
}
- }
- execbuf->tmpl_argv = argv + 1;
- execbuf->tmpl_argc = i - 1;
+ ++execbuf->tmpl_argc;
+ }
if (execbuf->tmpl_argc == 0) {
- bfs_error(ctx, "%s: Missing command.\n", argv[0]);
+ bfs_exec_parse_error(ctx, execbuf);
+ bfs_error(ctx, "Missing command.\n");
goto fail;
}
execbuf->argv_cap = execbuf->tmpl_argc + 1;
- execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv));
+ execbuf->argv = ALLOC_ARRAY(char *, execbuf->argv_cap);
if (!execbuf->argv) {
- bfs_perror(ctx, "malloc()");
+ bfs_perror(ctx, "alloc()");
goto fail;
}
if (execbuf->flags & BFS_EXEC_MULTI) {
- for (i = 0; i < execbuf->tmpl_argc - 1; ++i) {
+ for (size_t i = 0; i < execbuf->tmpl_argc - 1; ++i) {
char *arg = execbuf->tmpl_argv[i];
if (strstr(arg, "{}")) {
- bfs_error(ctx, "%s ... +: Only one '{}' is supported.\n", argv[0]);
+ bfs_exec_parse_error(ctx, execbuf);
+ bfs_error(ctx, "Only one '{}' is supported.\n");
goto fail;
}
execbuf->argv[i] = arg;
@@ -183,6 +188,7 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs
execbuf->argc = execbuf->tmpl_argc - 1;
execbuf->arg_max = bfs_exec_arg_max(execbuf);
+ execbuf->arg_min = execbuf->arg_max;
}
return execbuf;
@@ -211,9 +217,8 @@ static char *bfs_exec_format_path(const struct bfs_exec *execbuf, const struct B
return NULL;
}
- strcpy(path, "./");
- strcpy(path + 2, name);
-
+ char *cur = stpcpy(path, "./");
+ cur = stpcpy(cur, name);
return path;
}
@@ -224,14 +229,14 @@ static char *bfs_exec_format_arg(char *arg, const char *path) {
return arg;
}
- char *ret = dstralloc(0);
+ dchar *ret = dstralloc(0);
if (!ret) {
return NULL;
}
char *last = arg;
do {
- if (dstrncat(&ret, last, match - last) != 0) {
+ if (dstrxcat(&ret, last, match - last) != 0) {
goto err;
}
if (dstrcat(&ret, path) != 0) {
@@ -256,18 +261,18 @@ err:
/** Free a formatted argument. */
static void bfs_exec_free_arg(char *arg, const char *tmpl) {
if (arg != tmpl) {
- dstrfree(arg);
+ dstrfree((dchar *)arg);
}
}
/** Open a file to use as the working directory. */
static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
- assert(execbuf->wd_fd < 0);
- assert(!execbuf->wd_path);
+ bfs_assert(execbuf->wd_fd < 0);
+ bfs_assert(!execbuf->wd_path);
- if (ftwbuf->at_fd != AT_FDCWD) {
+ if (ftwbuf->at_fd != (int)AT_FDCWD) {
// Rely on at_fd being the immediate parent
- assert(ftwbuf->at_path == xbasename(ftwbuf->at_path));
+ bfs_assert(xbaseoff(ftwbuf->at_path) == 0);
execbuf->wd_fd = ftwbuf->at_fd;
if (!(execbuf->flags & BFS_EXEC_MULTI)) {
@@ -307,12 +312,10 @@ static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf)
}
/** Close the working directory. */
-static int bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
- int ret = 0;
-
+static void bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
if (execbuf->wd_fd >= 0) {
if (!ftwbuf || execbuf->wd_fd != ftwbuf->at_fd) {
- ret = close(execbuf->wd_fd);
+ xclose(execbuf->wd_fd);
}
execbuf->wd_fd = -1;
}
@@ -322,17 +325,24 @@ static int bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf)
execbuf->wd_path = NULL;
execbuf->wd_len = 0;
}
-
- return ret;
}
/** Actually spawn the process. */
static int bfs_exec_spawn(const struct bfs_exec *execbuf) {
+ const struct bfs_ctx *ctx = execbuf->ctx;
+
+ // Flush the context state for consistency with the external process
+ bfs_ctx_flush(ctx);
+
if (execbuf->flags & BFS_EXEC_CONFIRM) {
for (size_t i = 0; i < execbuf->argc; ++i) {
- fprintf(stderr, "%s ", execbuf->argv[i]);
+ if (fprintf(stderr, "%s ", execbuf->argv[i]) < 0) {
+ return -1;
+ }
+ }
+ if (fprintf(stderr, "? ") < 0) {
+ return -1;
}
- fprintf(stderr, "? ");
if (ynprompt() <= 0) {
errno = 0;
@@ -342,49 +352,46 @@ static int bfs_exec_spawn(const struct bfs_exec *execbuf) {
if (execbuf->flags & BFS_EXEC_MULTI) {
bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments] (size %zu)\n",
- execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size);
+ execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size);
} else {
bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments]\n", execbuf->argv[0], execbuf->argc - 1);
}
pid_t pid = -1;
- int error;
- struct bfs_spawn ctx;
- if (bfs_spawn_init(&ctx) != 0) {
+ struct bfs_spawn spawn;
+ if (bfs_spawn_init(&spawn) != 0) {
return -1;
}
- if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) {
- goto fail;
- }
+ spawn.flags |= BFS_SPAWN_USE_PATH;
- // Reset RLIMIT_NOFILE, to avoid breaking applications that use select()
- struct rlimit rl = {
- .rlim_cur = execbuf->ctx->nofile_soft,
- .rlim_max = execbuf->ctx->nofile_hard,
- };
- if (bfs_spawn_addsetrlimit(&ctx, RLIMIT_NOFILE, &rl) != 0) {
- goto fail;
+ if (execbuf->wd_fd >= 0) {
+ if (bfs_spawn_addfchdir(&spawn, execbuf->wd_fd) != 0) {
+ goto fail;
+ }
}
- if (execbuf->wd_fd >= 0) {
- if (bfs_spawn_addfchdir(&ctx, execbuf->wd_fd) != 0) {
+ // Reset RLIMIT_NOFILE if necessary, to avoid breaking applications that use select()
+ if (rlim_cmp(ctx->orig_nofile.rlim_cur, ctx->cur_nofile.rlim_cur) < 0) {
+ if (bfs_spawn_setrlimit(&spawn, RLIMIT_NOFILE, &ctx->orig_nofile) != 0) {
goto fail;
}
}
- pid = bfs_spawn(execbuf->argv[0], &ctx, execbuf->argv, NULL);
-fail:
- error = errno;
- bfs_spawn_destroy(&ctx);
+ pid = bfs_spawn(execbuf->argv[0], &spawn, execbuf->argv, NULL);
+
+fail:;
+ int error = errno;
+
+ bfs_spawn_destroy(&spawn);
if (pid < 0) {
errno = error;
return -1;
}
int wstatus;
- if (waitpid(pid, &wstatus, 0) < 0) {
+ if (xwaitpid(pid, &wstatus, 0) < 0) {
return -1;
}
@@ -403,9 +410,9 @@ fail:
if (!str) {
str = "unknown";
}
- bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str);
+ bfs_warning(ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str);
} else {
- bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]);
+ bfs_warning(ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]);
}
errno = 0;
@@ -461,6 +468,58 @@ static bool bfs_exec_args_remain(const struct bfs_exec *execbuf) {
return execbuf->argc >= execbuf->tmpl_argc;
}
+/** Compute the current ARG_MAX estimate for binary search. */
+static size_t bfs_exec_estimate_max(const struct bfs_exec *execbuf) {
+ size_t min = execbuf->arg_min;
+ size_t max = execbuf->arg_max;
+ return min + (max - min) / 2;
+}
+
+/** Update the ARG_MAX lower bound from a successful execution. */
+static void bfs_exec_update_min(struct bfs_exec *execbuf) {
+ if (execbuf->arg_size > execbuf->arg_min) {
+ execbuf->arg_min = execbuf->arg_size;
+
+ // Don't let min exceed max
+ if (execbuf->arg_min > execbuf->arg_max) {
+ execbuf->arg_min = execbuf->arg_max;
+ }
+
+ size_t estimate = bfs_exec_estimate_max(execbuf);
+ bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n",
+ execbuf->arg_min, execbuf->arg_max, estimate);
+ }
+}
+
+/** Update the ARG_MAX upper bound from a failed execution. */
+static size_t bfs_exec_update_max(struct bfs_exec *execbuf) {
+ bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n");
+
+ size_t size = execbuf->arg_size;
+ if (size <= execbuf->arg_min) {
+ // Lower bound was wrong, restart binary search.
+ execbuf->arg_min = 0;
+ }
+
+ // Trim a fraction off the max size to avoid repeated failures near the
+ // top end of the working range
+ size -= size / 16;
+ if (size < execbuf->arg_max) {
+ execbuf->arg_max = size;
+
+ // Don't let min exceed max
+ if (execbuf->arg_min > execbuf->arg_max) {
+ execbuf->arg_min = execbuf->arg_max;
+ }
+ }
+
+ // Binary search for a more precise bound
+ size_t estimate = bfs_exec_estimate_max(execbuf);
+ bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n",
+ execbuf->arg_min, execbuf->arg_max, estimate);
+ return estimate;
+}
+
/** Execute the pending command from a BFS_EXEC_MULTI execbuf. */
static int bfs_exec_flush(struct bfs_exec *execbuf) {
int ret = 0, error = 0;
@@ -470,20 +529,24 @@ static int bfs_exec_flush(struct bfs_exec *execbuf) {
execbuf->argv[execbuf->argc] = NULL;
ret = bfs_exec_spawn(execbuf);
error = errno;
- if (ret == 0 || error != E2BIG) {
+ if (ret == 0) {
+ bfs_exec_update_min(execbuf);
+ break;
+ } else if (error != E2BIG) {
break;
}
// Try to recover from E2BIG by trying fewer and fewer arguments
// until they fit
- bfs_exec_debug(execbuf, "Got E2BIG, shrinking argument list...\n");
- execbuf->argv[execbuf->argc] = execbuf->argv[execbuf->argc - 1];
- execbuf->arg_size -= bfs_exec_arg_size(execbuf->argv[execbuf->argc]);
- --execbuf->argc;
+ size_t new_max = bfs_exec_update_max(execbuf);
+ while (execbuf->arg_size > new_max) {
+ execbuf->argv[execbuf->argc] = execbuf->argv[execbuf->argc - 1];
+ execbuf->arg_size -= bfs_exec_arg_size(execbuf->argv[execbuf->argc]);
+ --execbuf->argc;
+ }
}
- size_t new_argc = execbuf->argc;
- size_t new_size = execbuf->arg_size;
+ size_t new_argc = execbuf->argc;
for (size_t i = execbuf->tmpl_argc - 1; i < new_argc; ++i) {
free(execbuf->argv[i]);
}
@@ -491,9 +554,6 @@ static int bfs_exec_flush(struct bfs_exec *execbuf) {
execbuf->arg_size = 0;
if (new_argc < orig_argc) {
- execbuf->arg_max = new_size;
- bfs_exec_debug(execbuf, "ARG_MAX: %zu\n", execbuf->arg_max);
-
// If we recovered from E2BIG, there are unused arguments at the
// end of the list
for (size_t i = new_argc + 1; i <= orig_argc; ++i) {
@@ -526,10 +586,11 @@ static bool bfs_exec_changed_dirs(const struct bfs_exec *execbuf, const struct B
/** Check if we need to flush the execbuf because we're too big. */
static bool bfs_exec_would_overflow(const struct bfs_exec *execbuf, const char *arg) {
+ size_t arg_max = bfs_exec_estimate_max(execbuf);
size_t next_size = execbuf->arg_size + bfs_exec_arg_size(arg);
- if (next_size > execbuf->arg_max) {
+ if (next_size > arg_max) {
bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n",
- next_size, execbuf->arg_max);
+ next_size, arg_max);
return true;
}
@@ -541,8 +602,8 @@ static int bfs_exec_push(struct bfs_exec *execbuf, char *arg) {
execbuf->argv[execbuf->argc] = arg;
if (execbuf->argc + 1 >= execbuf->argv_cap) {
- size_t cap = 2*execbuf->argv_cap;
- char **argv = realloc(execbuf->argv, cap*sizeof(*argv));
+ size_t cap = 2 * execbuf->argv_cap;
+ char **argv = REALLOC_ARRAY(char *, execbuf->argv, execbuf->argv_cap, cap);
if (!argv) {
return -1;
}
diff --git a/exec.h b/src/exec.h
index 1ba409f..1d8e75f 100644
--- a/exec.h
+++ b/src/exec.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Implementation of -exec/-execdir/-ok/-okdir.
@@ -63,6 +50,8 @@ struct bfs_exec {
size_t arg_size;
/** Maximum arg_size before E2BIG. */
size_t arg_max;
+ /** Lower bound for arg_max. */
+ size_t arg_min;
/** A file descriptor for the working directory, for BFS_EXEC_CHDIR. */
int wd_fd;
@@ -78,11 +67,11 @@ struct bfs_exec {
/**
* Parse an exec action.
*
- * @param argv
+ * @argv
* The (bfs) command line argument to parse.
- * @param flags
+ * @flags
* Any flags for this exec action.
- * @param ctx
+ * @ctx
* The bfs context.
* @return
* The parsed exec action, or NULL on failure.
@@ -92,9 +81,9 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs
/**
* Execute the command for a file.
*
- * @param execbuf
+ * @execbuf
* The parsed exec action.
- * @param ftwbuf
+ * @ftwbuf
* The bftw() data for the current file.
* @return 0 if the command succeeded, -1 if it failed. If the command could
* be executed, -1 is returned, and errno will be non-zero. For
@@ -105,7 +94,7 @@ int bfs_exec(struct bfs_exec *execbuf, const struct BFTW *ftwbuf);
/**
* Finish executing any commands.
*
- * @param execbuf
+ * @execbuf
* The parsed exec action.
* @return 0 on success, -1 if any errors were encountered.
*/
diff --git a/src/expr.c b/src/expr.c
new file mode 100644
index 0000000..ca37ffc
--- /dev/null
+++ b/src/expr.c
@@ -0,0 +1,89 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "expr.h"
+
+#include "alloc.h"
+#include "ctx.h"
+#include "diag.h"
+#include "eval.h"
+#include "exec.h"
+#include "list.h"
+#include "printf.h"
+#include "xregex.h"
+
+#include <string.h>
+
+struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) {
+ bfs_assert(kind != BFS_PATH);
+
+ struct bfs_expr *expr = arena_alloc(&ctx->expr_arena);
+ if (!expr) {
+ return NULL;
+ }
+
+ memset(expr, 0, sizeof(*expr));
+ expr->eval_fn = eval_fn;
+ expr->argc = argc;
+ expr->argv = argv;
+ expr->kind = kind;
+ expr->probability = 0.5;
+ SLIST_PREPEND(&ctx->expr_list, expr, freelist);
+
+ if (bfs_expr_is_parent(expr)) {
+ SLIST_INIT(&expr->children);
+ }
+
+ return expr;
+}
+
+bool bfs_expr_is_parent(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_and
+ || expr->eval_fn == eval_or
+ || expr->eval_fn == eval_not
+ || expr->eval_fn == eval_comma;
+}
+
+struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr) {
+ if (bfs_expr_is_parent(expr)) {
+ return expr->children.head;
+ } else {
+ return NULL;
+ }
+}
+
+void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child) {
+ bfs_assert(bfs_expr_is_parent(expr));
+
+ SLIST_APPEND(&expr->children, child);
+
+ if (!child->pure) {
+ expr->pure = false;
+ }
+
+ expr->persistent_fds += child->persistent_fds;
+ if (expr->ephemeral_fds < child->ephemeral_fds) {
+ expr->ephemeral_fds = child->ephemeral_fds;
+ }
+}
+
+void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children) {
+ drain_slist (struct bfs_expr, child, children) {
+ bfs_expr_append(expr, child);
+ }
+}
+
+bool bfs_expr_never_returns(const struct bfs_expr *expr) {
+ // Expressions that never return are vacuously both always true and always false
+ return expr->always_true && expr->always_false;
+}
+
+void bfs_expr_clear(struct bfs_expr *expr) {
+ if (expr->eval_fn == eval_exec) {
+ bfs_exec_free(expr->exec);
+ } else if (expr->eval_fn == eval_fprintf) {
+ bfs_printf_free(expr->printf);
+ } else if (expr->eval_fn == eval_regex) {
+ bfs_regfree(expr->regex);
+ }
+}
diff --git a/src/expr.h b/src/expr.h
new file mode 100644
index 0000000..c116778
--- /dev/null
+++ b/src/expr.h
@@ -0,0 +1,279 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * The expression tree representation.
+ */
+
+#ifndef BFS_EXPR_H
+#define BFS_EXPR_H
+
+#include "color.h"
+#include "eval.h"
+#include "stat.h"
+
+#include <sys/types.h>
+#include <time.h>
+
+/**
+ * Argument/token/expression kinds.
+ */
+enum bfs_kind {
+ /** A regular argument. */
+ BFS_ARG,
+
+ /** A flag (-H, -L, etc.). */
+ BFS_FLAG,
+
+ /** A root path. */
+ BFS_PATH,
+
+ /** An option (-follow, -mindepth, etc.). */
+ BFS_OPTION,
+ /** A test (-name, -size, etc.). */
+ BFS_TEST,
+ /** An action (-print, -exec, etc.). */
+ BFS_ACTION,
+
+ /** An operator (-and, -or, etc.). */
+ BFS_OPERATOR,
+};
+
+/**
+ * Integer comparison modes.
+ */
+enum bfs_int_cmp {
+ /** Exactly N. */
+ BFS_INT_EQUAL,
+ /** Less than N (-N). */
+ BFS_INT_LESS,
+ /** Greater than N (+N). */
+ BFS_INT_GREATER,
+};
+
+/**
+ * Permission comparison modes.
+ */
+enum bfs_mode_cmp {
+ /** Mode is an exact match (MODE). */
+ BFS_MODE_EQUAL,
+ /** Mode has all these bits (-MODE). */
+ BFS_MODE_ALL,
+ /** Mode has any of these bits (/MODE). */
+ BFS_MODE_ANY,
+};
+
+/**
+ * Possible time units.
+ */
+enum bfs_time_unit {
+ /** Seconds. */
+ BFS_SECONDS,
+ /** Minutes. */
+ BFS_MINUTES,
+ /** Days. */
+ BFS_DAYS,
+};
+
+/**
+ * Possible file size units.
+ */
+enum bfs_size_unit {
+ /** 512-byte blocks. */
+ BFS_BLOCKS,
+ /** Single bytes. */
+ BFS_BYTES,
+ /** Two-byte words. */
+ BFS_WORDS,
+ /** Kibibytes. */
+ BFS_KB,
+ /** Mebibytes. */
+ BFS_MB,
+ /** Gibibytes. */
+ BFS_GB,
+ /** Tebibytes. */
+ BFS_TB,
+ /** Pebibytes. */
+ BFS_PB,
+};
+
+/**
+ * A linked list of expressions.
+ */
+struct bfs_exprs {
+ struct bfs_expr *head;
+ struct bfs_expr **tail;
+};
+
+/**
+ * A command line expression.
+ */
+struct bfs_expr {
+ /** This expression's next sibling, if any. */
+ struct bfs_expr *next;
+ /** The next allocated expression. */
+ struct { struct bfs_expr *next; } freelist;
+
+ /** The function that evaluates this expression. */
+ bfs_eval_fn *eval_fn;
+
+ /** The number of command line arguments for this expression. */
+ size_t argc;
+ /** The command line arguments comprising this expression. */
+ char **argv;
+ /** The kind of expression this is. */
+ enum bfs_kind kind;
+
+ /** The number of files this expression keeps open between evaluations. */
+ int persistent_fds;
+ /** The number of files this expression opens during evaluation. */
+ int ephemeral_fds;
+
+ /** Whether this expression has no side effects. */
+ bool pure;
+ /** Whether this expression always evaluates to true. */
+ bool always_true;
+ /** Whether this expression always evaluates to false. */
+ bool always_false;
+ /** Whether this expression uses stat(). */
+ bool calls_stat;
+
+ /** Estimated cost. */
+ float cost;
+ /** Estimated probability of success. */
+ float probability;
+ /** Number of times this predicate was evaluated. */
+ size_t evaluations;
+ /** Number of times this predicate succeeded. */
+ size_t successes;
+ /** Total time spent running this predicate. */
+ struct timespec elapsed;
+
+ /** Auxiliary data for the evaluation function. */
+ union {
+ /** Child expressions. */
+ struct bfs_exprs children;
+
+ /** Integer comparisons. */
+ struct {
+ /** Integer for this comparison. */
+ long long num;
+ /** The comparison mode. */
+ enum bfs_int_cmp int_cmp;
+
+ /** -size data. */
+ enum bfs_size_unit size_unit;
+
+ /** The stat field to look at. */
+ enum bfs_stat_field stat_field;
+ /** The time unit. */
+ enum bfs_time_unit time_unit;
+ /** The reference time. */
+ struct timespec reftime;
+ };
+
+ /** String comparisons. */
+ struct {
+ /** String pattern. */
+ const char *pattern;
+ /** fnmatch() flags. */
+ int fnm_flags;
+ /** Whether strcmp() can be used instead of fnmatch(). */
+ bool literal;
+ };
+
+ /** Printing actions. */
+ struct {
+ /** The output stream. */
+ CFILE *cfile;
+ /** Optional file path. */
+ const char *path;
+ /** Optional -printf format. */
+ struct bfs_printf *printf;
+ };
+
+ /** -exec data. */
+ struct bfs_exec *exec;
+
+ /** -flags data. */
+ struct {
+ /** The comparison mode. */
+ enum bfs_mode_cmp flags_cmp;
+ /** Flags that should be set. */
+ unsigned long long set_flags;
+ /** Flags that should be cleared. */
+ unsigned long long clear_flags;
+ };
+
+ /** -perm data. */
+ struct {
+ /** The comparison mode. */
+ enum bfs_mode_cmp mode_cmp;
+ /** Mode to use for files. */
+ mode_t file_mode;
+ /** Mode to use for directories (different due to X). */
+ mode_t dir_mode;
+ };
+
+ /** -regex data. */
+ struct bfs_regex *regex;
+
+ /** -samefile data. */
+ struct {
+ /** Device number of the target file. */
+ dev_t dev;
+ /** Inode number of the target file. */
+ ino_t ino;
+ };
+ };
+};
+
+struct bfs_ctx;
+
+/**
+ * Create a new expression.
+ */
+struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv, enum bfs_kind kind);
+
+/**
+ * @return Whether this type of expression has children.
+ */
+bool bfs_expr_is_parent(const struct bfs_expr *expr);
+
+/**
+ * @return The first child of this expression, or NULL if it has none.
+ */
+struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr);
+
+/**
+ * Add a child to an expression.
+ */
+void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child);
+
+/**
+ * Add a list of children to an expression.
+ */
+void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children);
+
+/**
+ * @return Whether expr is known to always quit.
+ */
+bool bfs_expr_never_returns(const struct bfs_expr *expr);
+
+/**
+ * @return The result of the integer comparison for this expression.
+ */
+bool bfs_expr_cmp(const struct bfs_expr *expr, long long n);
+
+/**
+ * Free any resources owned by an expression.
+ */
+void bfs_expr_clear(struct bfs_expr *expr);
+
+/**
+ * Iterate over the children of an expression.
+ */
+#define for_expr(child, expr) \
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next)
+
+#endif // BFS_EXPR_H
diff --git a/fsade.c b/src/fsade.c
index 1444cf4..dfdf125 100644
--- a/fsade.c
+++ b/src/fsade.c
@@ -1,55 +1,58 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#include "fsade.h"
+
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
#include "bftw.h"
#include "dir.h"
#include "dstring.h"
-#include "util.h"
+#include "sanity.h"
+
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <unistd.h>
#if BFS_CAN_CHECK_ACL
-# include <sys/acl.h>
+# include <sys/acl.h>
#endif
#if BFS_CAN_CHECK_CAPABILITIES
-# include <sys/capability.h>
+# include <sys/capability.h>
+#endif
+
+#if BFS_CAN_CHECK_CONTEXT
+# include <selinux/selinux.h>
#endif
-#if BFS_HAS_SYS_EXTATTR
-# include <sys/extattr.h>
-#elif BFS_HAS_SYS_XATTR
-# include <sys/xattr.h>
+#if __has_include(<sys/extattr.h>)
+# include <sys/extattr.h>
+# define BFS_USE_EXTATTR true
+#elif __has_include(<sys/xattr.h>)
+# include <sys/xattr.h>
+# define BFS_USE_XATTR true
#endif
-#if BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS
+#ifndef BFS_USE_EXTATTR
+# define BFS_USE_EXTATTR false
+#endif
+#ifndef BFS_USE_XATTR
+# define BFS_USE_XATTR false
+#endif
/**
* Many of the APIs used here don't have *at() variants, but we can try to
* emulate something similar if /proc/self/fd is available.
*/
+_maybe_unused
static const char *fake_at(const struct BFTW *ftwbuf) {
- static bool proc_works = true;
- static bool proc_checked = false;
+ static atomic int proc_works = -1;
- char *path = NULL;
- if (!proc_works || ftwbuf->at_fd == AT_FDCWD) {
+ dchar *path = NULL;
+ if (ftwbuf->at_fd == (int)AT_FDCWD || load(&proc_works, relaxed) == 0) {
goto fail;
}
@@ -58,11 +61,12 @@ static const char *fake_at(const struct BFTW *ftwbuf) {
goto fail;
}
- if (!proc_checked) {
- proc_checked = true;
+ if (load(&proc_works, relaxed) < 0) {
if (xfaccessat(AT_FDCWD, path, F_OK) != 0) {
- proc_works = false;
+ store(&proc_works, 0, relaxed);
goto fail;
+ } else {
+ store(&proc_works, 1, relaxed);
}
}
@@ -77,15 +81,17 @@ fail:
return ftwbuf->path;
}
+_maybe_unused
static void free_fake_at(const struct BFTW *ftwbuf, const char *path) {
if (path != ftwbuf->path) {
- dstrfree((char *)path);
+ dstrfree((dchar *)path);
}
}
/**
* Check if an error was caused by the absence of support or data for a feature.
*/
+_maybe_unused
static bool is_absence_error(int error) {
// If the OS doesn't support the feature, it's obviously not enabled for
// any files
@@ -124,28 +130,73 @@ static bool is_absence_error(int error) {
return false;
}
-#endif // BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS
-
#if BFS_CAN_CHECK_ACL
+#if BFS_HAS_ACL_GET_FILE
+
+/** Unified interface for incompatible acl_get_entry() implementations. */
+static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
+#if BFS_HAS_ACL_GET_ENTRY
+ int ret = acl_get_entry(acl, which, entry);
+# if __APPLE__
+ // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
+ return !ret;
+# else
+ return ret;
+# endif
+#elif __DragonFly__
+# if !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
+# define ACL_FIRST_ENTRY 0
+# define ACL_NEXT_ENTRY 1
+# endif
+
+ switch (which) {
+ case ACL_FIRST_ENTRY:
+ *entry = &acl->acl_entry[0];
+ break;
+ case ACL_NEXT_ENTRY:
+ ++*entry;
+ break;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+
+ acl_entry_t last = &acl->acl_entry[acl->acl_cnt];
+ return *entry == last;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+/** Unified interface for acl_get_tag_type(). */
+_maybe_unused
+static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) {
+#if BFS_HAS_ACL_GET_TAG_TYPE
+ return acl_get_tag_type(entry, tag);
+#elif __DragonFly__
+ *tag = entry->ae_tag;
+ return 0;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
/** Check if a POSIX.1e ACL is non-trivial. */
static int bfs_check_posix1e_acl(acl_t acl, bool ignore_required) {
int ret = 0;
acl_entry_t entry;
- for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
-#if __APPLE__
- // POSIX.1e specifies a return value of 1 for success, but macOS
- // returns 0 instead
- status == 0;
-#else
+ for (int status = bfs_acl_entry(acl, ACL_FIRST_ENTRY, &entry);
status > 0;
-#endif
- status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
+ status = bfs_acl_entry(acl, ACL_NEXT_ENTRY, &entry))
+ {
#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER)
if (ignore_required) {
acl_tag_t tag;
- if (acl_get_tag_type(entry, &tag) != 0) {
+ if (bfs_acl_tag_type(entry, &tag) != 0) {
ret = -1;
continue;
}
@@ -169,52 +220,56 @@ static int bfs_check_acl_type(acl_t acl, acl_type_t type) {
return bfs_check_posix1e_acl(acl, false);
}
-#if __FreeBSD__
+#if BFS_HAS_ACL_IS_TRIVIAL_NP
int trivial;
+ int ret = acl_is_trivial_np(acl, &trivial);
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // msan seems to be missing an interceptor for acl_is_trivial_np()
- trivial = 0;
-#endif
+ // msan seems to be missing an interceptor for acl_is_trivial_np()
+ sanitize_init(&trivial);
- if (acl_is_trivial_np(acl, &trivial) < 0) {
+ if (ret < 0) {
return -1;
} else if (trivial) {
return 0;
} else {
return 1;
}
-#else // !__FreeBSD__
+#else
return bfs_check_posix1e_acl(acl, true);
#endif
}
+#endif // BFS_HAS_ACL_GET_FILE
+
int bfs_check_acl(const struct BFTW *ftwbuf) {
+ if (ftwbuf->type == BFS_LNK) {
+ return 0;
+ }
+
+ const char *path = fake_at(ftwbuf);
+
+#if BFS_HAS_ACL_TRIVIAL
+ int ret = acl_trivial(path);
+ int error = errno;
+#elif BFS_HAS_ACL_GET_FILE
static const acl_type_t acl_types[] = {
-#if __APPLE__
+# if __APPLE__
// macOS gives EINVAL for either of the two standard ACL types,
// supporting only ACL_TYPE_EXTENDED
ACL_TYPE_EXTENDED,
-#else
+# else
// The two standard POSIX.1e ACL types
ACL_TYPE_ACCESS,
ACL_TYPE_DEFAULT,
-#endif
+# endif
-#ifdef ACL_TYPE_NFS4
+# ifdef ACL_TYPE_NFS4
ACL_TYPE_NFS4,
-#endif
+# endif
};
- static const size_t n_acl_types = sizeof(acl_types)/sizeof(acl_types[0]);
-
- if (ftwbuf->type == BFS_LNK) {
- return 0;
- }
-
- const char *path = fake_at(ftwbuf);
int ret = -1, error = 0;
- for (size_t i = 0; i < n_acl_types && ret <= 0; ++i) {
+ for (size_t i = 0; i < countof(acl_types) && ret <= 0; ++i) {
acl_type_t type = acl_types[i];
if (type == ACL_TYPE_DEFAULT && ftwbuf->type != BFS_DIR) {
@@ -236,6 +291,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) {
error = errno;
acl_free(acl);
}
+#endif
free_fake_at(ftwbuf, path);
errno = error;
@@ -299,17 +355,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) {
#if BFS_CAN_CHECK_XATTRS
+#if BFS_USE_EXTATTR
+
+/** Wrapper for extattr_list_{file,link}. */
+static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_LIST_LINK
+ return extattr_list_link(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_LIST_FILE
+ return extattr_list_file(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_FILE
+ // From man extattr(2):
+ //
+ // In earlier versions of this API, passing an empty string for the
+ // attribute name to extattr_get_file() would return the list of attributes
+ // defined for the target object. This interface has been deprecated in
+ // preference to using the explicit list API, and should not be used.
+ return extattr_get_file(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+/** Wrapper for extattr_get_{file,link}. */
+static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_GET_FILE
+ return extattr_get_file(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+#endif // BFS_USE_EXTATTR
+
int bfs_check_xattrs(const struct BFTW *ftwbuf) {
const char *path = fake_at(ftwbuf);
ssize_t len;
-#if BFS_HAS_SYS_EXTATTR
- ssize_t (*extattr_list)(const char *, int, void*, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_list_link : extattr_list_file;
-
- len = extattr_list(path, EXTATTR_NAMESPACE_SYSTEM, NULL, 0);
+#if BFS_USE_EXTATTR
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM);
if (len <= 0) {
- len = extattr_list(path, EXTATTR_NAMESPACE_USER, NULL, 0);
+ len = bfs_extattr_list(path, ftwbuf->type, EXTATTR_NAMESPACE_USER);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
@@ -342,13 +443,10 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
const char *path = fake_at(ftwbuf);
ssize_t len;
-#if BFS_HAS_SYS_EXTATTR
- ssize_t (*extattr_get)(const char *, int, const char *, void*, size_t) =
- ftwbuf->type == BFS_LNK ? extattr_get_link : extattr_get_file;
-
- len = extattr_get(path, EXTATTR_NAMESPACE_SYSTEM, name, NULL, 0);
+#if BFS_USE_EXTATTR
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_SYSTEM, name);
if (len < 0) {
- len = extattr_get(path, EXTATTR_NAMESPACE_USER, name, NULL, 0);
+ len = bfs_extattr_get(path, ftwbuf->type, EXTATTR_NAMESPACE_USER, name);
}
#elif __APPLE__
int options = ftwbuf->type == BFS_LNK ? XATTR_NOFOLLOW : 0;
@@ -390,3 +488,32 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
}
#endif
+
+char *bfs_getfilecon(const struct BFTW *ftwbuf) {
+#if BFS_CAN_CHECK_CONTEXT
+ const char *path = fake_at(ftwbuf);
+
+ char *con;
+ int ret;
+ if (ftwbuf->type == BFS_LNK) {
+ ret = lgetfilecon(path, &con);
+ } else {
+ ret = getfilecon(path, &con);
+ }
+
+ if (ret >= 0) {
+ return con;
+ } else {
+ return NULL;
+ }
+#else
+ errno = ENOTSUP;
+ return NULL;
+#endif
+}
+
+void bfs_freecon(char *con) {
+#if BFS_CAN_CHECK_CONTEXT
+ freecon(con);
+#endif
+}
diff --git a/src/fsade.h b/src/fsade.h
new file mode 100644
index 0000000..fbe02d8
--- /dev/null
+++ b/src/fsade.h
@@ -0,0 +1,85 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A facade over (file)system features that are (un)implemented differently
+ * between platforms.
+ */
+
+#ifndef BFS_FSADE_H
+#define BFS_FSADE_H
+
+#include "bfs.h"
+
+#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL)
+
+#define BFS_CAN_CHECK_CAPABILITIES BFS_WITH_LIBCAP
+
+#define BFS_CAN_CHECK_CONTEXT BFS_WITH_LIBSELINUX
+
+#if __has_include(<sys/extattr.h>) || __has_include(<sys/xattr.h>)
+# define BFS_CAN_CHECK_XATTRS true
+#else
+# define BFS_CAN_CHECK_XATTRS false
+#endif
+
+struct BFTW;
+
+/**
+ * Check if a file has a non-trivial Access Control List.
+ *
+ * @ftwbuf
+ * The file to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
+ */
+int bfs_check_acl(const struct BFTW *ftwbuf);
+
+/**
+ * Check if a file has a non-trivial capability set.
+ *
+ * @ftwbuf
+ * The file to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
+ */
+int bfs_check_capabilities(const struct BFTW *ftwbuf);
+
+/**
+ * Check if a file has any extended attributes set.
+ *
+ * @ftwbuf
+ * The file to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
+ */
+int bfs_check_xattrs(const struct BFTW *ftwbuf);
+
+/**
+ * Check if a file has an extended attribute with the given name.
+ *
+ * @ftwbuf
+ * The file to check.
+ * @name
+ * The name of the xattr to check.
+ * @return
+ * 1 if it does, 0 if it doesn't, or -1 if an error occurred.
+ */
+int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name);
+
+/**
+ * Get a file's SELinux context
+ *
+ * @ftwbuf
+ * The file to check.
+ * @return
+ * The file's SELinux context, or NULL on failure.
+ */
+char *bfs_getfilecon(const struct BFTW *ftwbuf);
+
+/**
+ * Free a bfs_getfilecon() result.
+ */
+void bfs_freecon(char *con);
+
+#endif // BFS_FSADE_H
diff --git a/src/ioq.c b/src/ioq.c
new file mode 100644
index 0000000..57eb4a5
--- /dev/null
+++ b/src/ioq.c
@@ -0,0 +1,1330 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * An asynchronous I/O queue implementation.
+ *
+ * struct ioq is composed of two separate queues:
+ *
+ * struct ioqq *pending; // Pending I/O requests
+ * struct ioqq *ready; // Ready I/O responses
+ *
+ * Worker threads pop requests from `pending`, execute them, and push them back
+ * to the `ready` queue. The main thread pushes requests to `pending` and pops
+ * them from `ready`.
+ *
+ * struct ioqq is a blocking MPMC queue (though it could be SPMC/MPSC for
+ * pending/ready respectively). It is implemented as a circular buffer:
+ *
+ * size_t mask; // (1 << N) - 1
+ * [padding]
+ * size_t head; // Writer index
+ * [padding]
+ * size_t tail; // Reader index
+ * [padding]
+ * ioq_slot slots[1 << N]; // Queue contents
+ *
+ * Pushes are implemented with an unconditional
+ *
+ * fetch_add(&ioqq->head, 1)
+ *
+ * which scales better on many architectures than compare-and-swap (see [1] for
+ * details). Pops are implemented similarly. Since the fetch-and-adds are
+ * unconditional, non-blocking readers can get ahead of writers:
+ *
+ * Reader Writer
+ * ──────────────── ──────────────────────
+ * head: 0 → 1
+ * slots[0]: empty
+ * tail: 0 → 1
+ * slots[0]: empty → full
+ * head: 1 → 2
+ * slots[1]: empty!
+ *
+ * To avoid this, non-blocking reads (ioqq_pop(ioqq, false)) must mark the slots
+ * somehow so that writers can skip them:
+ *
+ * Reader Writer
+ * ─────────────────────── ───────────────────────
+ * head: 0 → 1
+ * slots[0]: empty → skip
+ * tail: 0 → 1
+ * slots[0]: skip → empty
+ * tail: 1 → 2
+ * slots[1]: empty → full
+ * head: 1 → 2
+ * slots[1]: full → empty
+ *
+ * As well, a reader might "lap" a writer (or another reader), so slots need to
+ * count how many times they should be skipped:
+ *
+ * Reader Writer
+ * ────────────────────────── ─────────────────────────
+ * head: 0 → 1
+ * slots[0]: empty → skip(1)
+ * head: 1 → 2
+ * slots[1]: empty → skip(1)
+ * ...
+ * head: M → 0
+ * slots[M]: empty → skip(1)
+ * head: 0 → 1
+ * slots[0]: skip(1 → 2)
+ * tail: 0 → 1
+ * slots[0]: skip(2 → 1)
+ * tail: 1 → 2
+ * slots[1]: skip(1) → empty
+ * ...
+ * tail: M → 0
+ * slots[M]: skip(1) → empty
+ * tail: 0 → 1
+ * slots[0]: skip(1) → empty
+ * tail: 1 → 2
+ * slots[1]: empty → full
+ * head: 1 → 2
+ * slots[1]: full → empty
+ *
+ * As described in [1], this approach is susceptible to livelock if readers stay
+ * ahead of writers. This is okay for us because we don't retry failed non-
+ * blocking reads.
+ *
+ * The slot representation uses tag bits to hold either a pointer or skip(N):
+ *
+ * IOQ_SKIP (highest bit) IOQ_BLOCKED (lowest bit)
+ * ↓ ↓
+ * 0 0 0 ... 0 0 0
+ * └──────────┬──────────┘
+ * │
+ * value bits
+ *
+ * If IOQ_SKIP is unset, the value bits hold a pointer (or zero/NULL for empty).
+ * If IOQ_SKIP is set, the value bits hold a negative skip count. Writers can
+ * reduce the skip count by adding 1 to the value bits, and when the count hits
+ * zero, the carry will automatically clear IOQ_SKIP:
+ *
+ * IOQ_SKIP IOQ_BLOCKED
+ * ↓ ↓
+ * 1 1 1 ... 1 0 0 skip(2)
+ * 1 1 1 ... 1 1 0 skip(1)
+ * 0 0 0 ... 0 0 0 empty
+ *
+ * The IOQ_BLOCKED flag is used to track sleeping waiters, futex-style. To wait
+ * for a slot to change, waiters call ioq_slot_wait() which sets IOQ_BLOCKED and
+ * goes to sleep. Whenever a slot is updated, if the old value had IOQ_BLOCKED
+ * set, ioq_slot_wake() must be called to wake up that waiter.
+ *
+ * Blocking/waking uses a pool of monitors (mutex, condition variable pairs).
+ * Slots are assigned round-robin to a monitor from the pool.
+ *
+ * [1]: https://arxiv.org/abs/2201.02179
+ */
+
+#include "ioq.h"
+
+#include "alloc.h"
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "bit.h"
+#include "diag.h"
+#include "dir.h"
+#include "stat.h"
+#include "thread.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#if BFS_WITH_LIBURING
+# include <liburing.h>
+#endif
+
+/**
+ * A monitor for an I/O queue slot.
+ */
+struct ioq_monitor {
+ cache_align pthread_mutex_t mutex;
+ pthread_cond_t cond;
+};
+
+/** Initialize an ioq_monitor. */
+static int ioq_monitor_init(struct ioq_monitor *monitor) {
+ if (mutex_init(&monitor->mutex, NULL) != 0) {
+ return -1;
+ }
+
+ if (cond_init(&monitor->cond, NULL) != 0) {
+ mutex_destroy(&monitor->mutex);
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Destroy an ioq_monitor. */
+static void ioq_monitor_destroy(struct ioq_monitor *monitor) {
+ cond_destroy(&monitor->cond);
+ mutex_destroy(&monitor->mutex);
+}
+
+/** A single entry in a command queue. */
+typedef atomic uintptr_t ioq_slot;
+
+/** Someone might be waiting on this slot. */
+#define IOQ_BLOCKED ((uintptr_t)1)
+
+/** Bit for IOQ_SKIP. */
+#define IOQ_SKIP_BIT (UINTPTR_WIDTH - 1)
+/** The next push(es) should skip this slot. */
+#define IOQ_SKIP ((uintptr_t)1 << IOQ_SKIP_BIT)
+/** Amount to add for an additional skip. */
+#define IOQ_SKIP_ONE (~IOQ_BLOCKED)
+
+static_assert(alignof(struct ioq_ent) >= (1 << 2), "struct ioq_ent is underaligned");
+
+/**
+ * An MPMC queue of I/O commands.
+ */
+struct ioqq {
+ /** Circular buffer index mask. */
+ size_t slot_mask;
+
+ /** Monitor index mask. */
+ size_t monitor_mask;
+ /** Array of monitors used by the slots. */
+ struct ioq_monitor *monitors;
+
+ /** Index of next writer. */
+ cache_align atomic size_t head;
+ /** Index of next reader. */
+ cache_align atomic size_t tail;
+
+ /** The circular buffer itself. */
+ cache_align ioq_slot slots[]; // _counted_by(slot_mask + 1)
+};
+
+/** Destroy an I/O command queue. */
+static void ioqq_destroy(struct ioqq *ioqq) {
+ if (!ioqq) {
+ return;
+ }
+
+ for (size_t i = 0; i < ioqq->monitor_mask + 1; ++i) {
+ ioq_monitor_destroy(&ioqq->monitors[i]);
+ }
+ free(ioqq->monitors);
+ free(ioqq);
+}
+
+/** Create an I/O command queue. */
+static struct ioqq *ioqq_create(size_t size) {
+ // Circular buffer size must be a power of two
+ size = bit_ceil(size);
+
+ struct ioqq *ioqq = ALLOC_FLEX(struct ioqq, slots, size);
+ if (!ioqq) {
+ return NULL;
+ }
+
+ ioqq->slot_mask = size - 1;
+ ioqq->monitor_mask = -1;
+
+ // Use a pool of monitors
+ size_t nmonitors = size < 64 ? size : 64;
+ ioqq->monitors = ALLOC_ARRAY(struct ioq_monitor, nmonitors);
+ if (!ioqq->monitors) {
+ ioqq_destroy(ioqq);
+ return NULL;
+ }
+
+ for (size_t i = 0; i < nmonitors; ++i) {
+ if (ioq_monitor_init(&ioqq->monitors[i]) != 0) {
+ ioqq_destroy(ioqq);
+ return NULL;
+ }
+ ++ioqq->monitor_mask;
+ }
+
+ atomic_init(&ioqq->head, 0);
+ atomic_init(&ioqq->tail, 0);
+
+ for (size_t i = 0; i < size; ++i) {
+ atomic_init(&ioqq->slots[i], 0);
+ }
+
+ return ioqq;
+}
+
+/** Get the monitor associated with a slot. */
+static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) {
+ uint32_t i = slot - ioqq->slots;
+
+ // Hash the index to de-correlate waiters
+ // https://nullprogram.com/blog/2018/07/31/
+ // https://github.com/skeeto/hash-prospector/issues/19#issuecomment-1120105785
+ i ^= i >> 16;
+ i *= UINT32_C(0x21f0aaad);
+ i ^= i >> 15;
+ i *= UINT32_C(0x735a2d97);
+ i ^= i >> 15;
+
+ return &ioqq->monitors[i & ioqq->monitor_mask];
+}
+
+/** Atomically wait for a slot to change. */
+_noinline
+static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) {
+ uintptr_t ret;
+
+ // Try spinning a few times (with exponential backoff) before blocking
+ _nounroll
+ for (int i = 1; i < 1024; i *= 2) {
+ _nounroll
+ for (int j = 0; j < i; ++j) {
+ spin_loop();
+ }
+
+ // Check if the slot changed
+ ret = load(slot, relaxed);
+ if (ret != value) {
+ return ret;
+ }
+ }
+
+ // Nothing changed, start blocking
+ struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot);
+ mutex_lock(&monitor->mutex);
+
+ ret = load(slot, relaxed);
+ if (ret != value) {
+ goto done;
+ }
+
+ if (!(value & IOQ_BLOCKED)) {
+ value |= IOQ_BLOCKED;
+ if (!compare_exchange_strong(slot, &ret, value, relaxed, relaxed)) {
+ goto done;
+ }
+ }
+
+ do {
+ // To avoid missed wakeups, it is important that
+ // cond_broadcast() is not called right here
+ cond_wait(&monitor->cond, &monitor->mutex);
+ ret = load(slot, relaxed);
+ } while (ret == value);
+
+done:
+ mutex_unlock(&monitor->mutex);
+ return ret;
+}
+
+/** Wake up any threads waiting on a slot. */
+_noinline
+static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) {
+ struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot);
+
+ // The following implementation would clearly avoid the missed wakeup
+ // issue mentioned above in ioq_slot_wait():
+ //
+ // mutex_lock(&monitor->mutex);
+ // cond_broadcast(&monitor->cond);
+ // mutex_unlock(&monitor->mutex);
+ //
+ // As a minor optimization, we move the broadcast outside of the lock.
+ // This optimization is correct, even though it leads to a seemingly-
+ // useless empty critical section.
+
+ mutex_lock(&monitor->mutex);
+ mutex_unlock(&monitor->mutex);
+ cond_broadcast(&monitor->cond);
+}
+
+/** Branch-free ((slot & IOQ_SKIP) ? skip : full) & ~IOQ_BLOCKED */
+static uintptr_t ioq_slot_blend(uintptr_t slot, uintptr_t skip, uintptr_t full) {
+ uintptr_t mask = -(slot >> IOQ_SKIP_BIT);
+ uintptr_t ret = (skip & mask) | (full & ~mask);
+ return ret & ~IOQ_BLOCKED;
+}
+
+/** Push an entry into a slot. */
+static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent) {
+ uintptr_t prev = load(slot, relaxed);
+
+ while (true) {
+ uintptr_t full = ioq_slot_blend(prev, 0, prev);
+ if (full) {
+ // full(ptr) → wait
+ prev = ioq_slot_wait(ioqq, slot, prev);
+ continue;
+ }
+
+ // empty → full(ptr)
+ uintptr_t next = (uintptr_t)ent >> 1;
+ // skip(1) → empty
+ // skip(n) → skip(n - 1)
+ next = ioq_slot_blend(prev, prev - IOQ_SKIP_ONE, next);
+
+ if (compare_exchange_weak(slot, &prev, next, release, relaxed)) {
+ break;
+ }
+ }
+
+ if (prev & IOQ_BLOCKED) {
+ ioq_slot_wake(ioqq, slot);
+ }
+
+ return !(prev & IOQ_SKIP);
+}
+
+/** (Try to) pop an entry from a slot. */
+static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool block) {
+ uintptr_t prev = load(slot, relaxed);
+ while (true) {
+#if __has_builtin(__builtin_prefetch)
+ // Optimistically prefetch the pointer in this slot. If this
+ // slot is not full, this will prefetch an invalid address, but
+ // experimentally this is worth it on both Intel (Alder Lake)
+ // and AMD (Zen 2).
+ __builtin_prefetch((void *)(prev << 1), 1 /* write */);
+#endif
+
+ // empty → skip(1)
+ // skip(n) → skip(n + 1)
+ // full(ptr) → full(ptr - 1)
+ uintptr_t next = prev + IOQ_SKIP_ONE;
+ // full(ptr) → 0
+ next = ioq_slot_blend(next, next, 0);
+
+ if (block && next) {
+ prev = ioq_slot_wait(ioqq, slot, prev);
+ continue;
+ }
+
+ if (compare_exchange_weak(slot, &prev, next, acquire, relaxed)) {
+ break;
+ }
+ }
+
+ if (prev & IOQ_BLOCKED) {
+ ioq_slot_wake(ioqq, slot);
+ }
+
+ // empty → 0
+ // skip(n) → 0
+ // full(ptr) → ptr
+ prev = ioq_slot_blend(prev, 0, prev);
+ return (struct ioq_ent *)(prev << 1);
+}
+
+/** Push an entry onto the queue. */
+static void ioqq_push(struct ioqq *ioqq, struct ioq_ent *ent) {
+ while (true) {
+ size_t i = fetch_add(&ioqq->head, 1, relaxed);
+ ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask];
+ if (ioq_slot_push(ioqq, slot, ent)) {
+ break;
+ }
+ }
+}
+
+/** Push a batch of entries to the queue. */
+static void ioqq_push_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size) {
+ size_t mask = ioqq->slot_mask;
+ do {
+ size_t i = fetch_add(&ioqq->head, size, relaxed);
+ for (size_t j = i + size; i != j; ++i) {
+ ioq_slot *slot = &ioqq->slots[i & mask];
+ if (ioq_slot_push(ioqq, slot, *batch)) {
+ ++batch;
+ --size;
+ }
+ }
+ } while (size > 0);
+}
+
+/** Pop a batch of entries from the queue. */
+static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size, bool block) {
+ size_t mask = ioqq->slot_mask;
+ size_t i = fetch_add(&ioqq->tail, size, relaxed);
+ for (size_t j = i + size; i != j; ++i) {
+ ioq_slot *slot = &ioqq->slots[i & mask];
+ *batch++ = ioq_slot_pop(ioqq, slot, block);
+ block = false;
+ }
+}
+
+/** Use cache-line-sized batches. */
+#define IOQ_BATCH (FALSE_SHARING_SIZE / sizeof(ioq_slot))
+
+/**
+ * A batch of I/O queue entries.
+ */
+struct ioq_batch {
+ /** The start of the batch. */
+ size_t head;
+ /** The end of the batch. */
+ size_t tail;
+ /** The array of entries. */
+ struct ioq_ent *entries[IOQ_BATCH];
+};
+
+/** Reset a batch. */
+static void ioq_batch_reset(struct ioq_batch *batch) {
+ batch->head = batch->tail = 0;
+}
+
+/** Check if a batch is empty. */
+static bool ioq_batch_empty(const struct ioq_batch *batch) {
+ return batch->head >= batch->tail;
+}
+
+/** Send a batch to a queue. */
+static void ioq_batch_flush(struct ioqq *ioqq, struct ioq_batch *batch) {
+ if (batch->tail > 0) {
+ ioqq_push_batch(ioqq, batch->entries, batch->tail);
+ ioq_batch_reset(batch);
+ }
+}
+
+/** Push an entry to a batch, flushing if necessary. */
+static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct ioq_ent *ent) {
+ batch->entries[batch->tail++] = ent;
+
+ if (batch->tail >= IOQ_BATCH) {
+ ioq_batch_flush(ioqq, batch);
+ }
+}
+
+/** Fill a batch from a queue. */
+static bool ioq_batch_fill(struct ioqq *ioqq, struct ioq_batch *batch, bool block) {
+ ioqq_pop_batch(ioqq, batch->entries, IOQ_BATCH, block);
+
+ ioq_batch_reset(batch);
+ for (size_t i = 0; i < IOQ_BATCH; ++i) {
+ struct ioq_ent *ent = batch->entries[i];
+ if (ent) {
+ batch->entries[batch->tail++] = ent;
+ }
+ }
+
+ return batch->tail > 0;
+}
+
+/** Pop an entry from a batch, filling it first if necessary. */
+static struct ioq_ent *ioq_batch_pop(struct ioqq *ioqq, struct ioq_batch *batch, bool block) {
+ if (ioq_batch_empty(batch)) {
+ // For non-blocking pops, make sure that each ioq_batch_pop()
+ // corresponds to a single (amortized) increment of ioqq->head.
+ // Otherwise, we start skipping many slots and batching ends up
+ // degrading performance.
+ if (!block && batch->head < IOQ_BATCH) {
+ ++batch->head;
+ return NULL;
+ }
+
+ if (!ioq_batch_fill(ioqq, batch, block)) {
+ return NULL;
+ }
+ }
+
+ return batch->entries[batch->head++];
+}
+
+/** Sentinel stop command. */
+static struct ioq_ent IOQ_STOP;
+
+#if BFS_WITH_LIBURING
+/**
+ * Supported io_uring operations.
+ */
+enum ioq_ring_ops {
+ IOQ_RING_OPENAT = 1 << 0,
+ IOQ_RING_CLOSE = 1 << 1,
+ IOQ_RING_STATX = 1 << 2,
+};
+#endif
+
+/** I/O queue thread-specific data. */
+struct ioq_thread {
+ /** The thread handle. */
+ pthread_t id;
+ /** Pointer back to the I/O queue. */
+ struct ioq *parent;
+
+#if BFS_WITH_LIBURING
+ /** io_uring instance. */
+ struct io_uring ring;
+ /** Any error that occurred initializing the ring. */
+ int ring_err;
+ /** Bitmask of supported io_uring operations. */
+ enum ioq_ring_ops ring_ops;
+#endif
+};
+
+struct ioq {
+ /** The depth of the queue. */
+ size_t depth;
+ /** The current size of the queue. */
+ size_t size;
+ /** Cancellation flag. */
+ atomic bool cancel;
+
+ /** ioq_ent arena. */
+ struct arena ents;
+#if BFS_WITH_LIBURING && BFS_USE_STATX
+ /** struct statx arena. */
+ struct arena xbufs;
+#endif
+
+ /** Pending I/O request queue. */
+ struct ioqq *pending;
+ /** Ready I/O response queue. */
+ struct ioqq *ready;
+
+ /** Pending request batch. */
+ struct ioq_batch pending_batch;
+ /** Ready request batch. */
+ struct ioq_batch ready_batch;
+
+ /** The number of background threads. */
+ size_t nthreads;
+ /** The background threads themselves. */
+ struct ioq_thread threads[] _counted_by(nthreads);
+};
+
+/** Cancel a request if we need to. */
+static bool ioq_check_cancel(struct ioq *ioq, struct ioq_ent *ent) {
+ if (!load(&ioq->cancel, relaxed)) {
+ return false;
+ }
+
+ // Always close(), even if we're cancelled, just like a real EINTR
+ if (ent->op == IOQ_CLOSE || ent->op == IOQ_CLOSEDIR) {
+ return false;
+ }
+
+ ent->result = -EINTR;
+ return true;
+}
+
+/** Dispatch a single request synchronously. */
+static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) {
+ switch (ent->op) {
+ case IOQ_NOP:
+ if (ent->nop.type == IOQ_NOP_HEAVY) {
+ // A fast, no-op syscall
+ getppid();
+ }
+ ent->result = 0;
+ return;
+
+ case IOQ_CLOSE:
+ ent->result = try(xclose(ent->close.fd));
+ return;
+
+ case IOQ_OPENDIR: {
+ struct ioq_opendir *args = &ent->opendir;
+ ent->result = try(bfs_opendir(args->dir, args->dfd, args->path, args->flags));
+ if (ent->result >= 0) {
+ bfs_polldir(args->dir);
+ }
+ return;
+ }
+
+ case IOQ_CLOSEDIR:
+ ent->result = try(bfs_closedir(ent->closedir.dir));
+ return;
+
+ case IOQ_STAT: {
+ struct ioq_stat *args = &ent->stat;
+ ent->result = try(bfs_stat(args->dfd, args->path, args->flags, args->buf));
+ return;
+ }
+ }
+
+ bfs_bug("Unknown ioq_op %d", (int)ent->op);
+ ent->result = -ENOSYS;
+}
+
+#if BFS_WITH_LIBURING
+
+/** io_uring worker state. */
+struct ioq_ring_state {
+ /** The I/O queue. */
+ struct ioq *ioq;
+ /** The io_uring. */
+ struct io_uring *ring;
+ /** Supported io_uring operations. */
+ enum ioq_ring_ops ops;
+ /** Number of prepped, unsubmitted SQEs. */
+ size_t prepped;
+ /** Number of submitted, unreaped SQEs. */
+ size_t submitted;
+ /** Whether to stop the loop. */
+ bool stop;
+ /** A batch of ready entries. */
+ struct ioq_batch ready;
+};
+
+/** Reap a single CQE. */
+static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) {
+ struct ioq *ioq = state->ioq;
+
+ struct ioq_ent *ent = io_uring_cqe_get_data(cqe);
+ ent->result = cqe->res;
+
+ if (ent->result < 0) {
+ goto push;
+ }
+
+ switch (ent->op) {
+ case IOQ_OPENDIR: {
+ int fd = ent->result;
+ if (ioq_check_cancel(ioq, ent)) {
+ xclose(fd);
+ goto push;
+ }
+
+ struct ioq_opendir *args = &ent->opendir;
+ ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags));
+ if (ent->result >= 0) {
+ // TODO: io_uring_prep_getdents()
+ bfs_polldir(args->dir);
+ } else {
+ xclose(fd);
+ }
+
+ break;
+ }
+
+#if BFS_USE_STATX
+ case IOQ_STAT: {
+ struct ioq_stat *args = &ent->stat;
+ ent->result = try(bfs_statx_convert(args->buf, args->xbuf));
+ break;
+ }
+#endif
+
+ default:
+ break;
+ }
+
+push:
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+}
+
+/** Wait for submitted requests to complete. */
+static void ioq_ring_drain(struct ioq_ring_state *state, size_t wait_nr) {
+ struct ioq *ioq = state->ioq;
+ struct io_uring *ring = state->ring;
+
+ bfs_assert(wait_nr <= state->submitted);
+
+ while (state->submitted > 0) {
+ struct io_uring_cqe *cqe;
+ if (wait_nr > 0) {
+ io_uring_wait_cqes(ring, &cqe, wait_nr, NULL, NULL);
+ }
+
+ unsigned int head;
+ size_t seen = 0;
+ io_uring_for_each_cqe (ring, head, cqe) {
+ ioq_reap_cqe(state, cqe);
+ ++seen;
+ }
+
+ io_uring_cq_advance(ring, seen);
+ state->submitted -= seen;
+
+ if (seen >= wait_nr) {
+ break;
+ }
+ wait_nr -= seen;
+ }
+
+ ioq_batch_flush(ioq->ready, &state->ready);
+}
+
+/** Submit prepped SQEs, and wait for some to complete. */
+static void ioq_ring_submit(struct ioq_ring_state *state) {
+ struct io_uring *ring = state->ring;
+
+ size_t unreaped = state->prepped + state->submitted;
+ size_t wait_nr = 0;
+
+ if (state->prepped == 0 && unreaped > 0) {
+ // If we have no new SQEs, wait for at least one old one to
+ // complete, to avoid livelock
+ wait_nr = 1;
+ }
+
+ if (unreaped > ring->sq.ring_entries) {
+ // Keep the completion queue below half full
+ wait_nr = unreaped - ring->sq.ring_entries;
+ }
+
+ // Submit all prepped SQEs
+ while (state->prepped > 0) {
+ int ret = io_uring_submit_and_wait(state->ring, wait_nr);
+ if (ret <= 0) {
+ continue;
+ }
+
+ state->submitted += ret;
+ state->prepped -= ret;
+ if (state->prepped > 0) {
+ // In the unlikely event of a short submission, any SQE
+ // links will be broken. Wait for all SQEs to complete
+ // to preserve any ordering requirements.
+ ioq_ring_drain(state, state->submitted);
+ wait_nr = 0;
+ }
+ }
+
+ // Drain all the CQEs we waited for (and any others that are ready)
+ ioq_ring_drain(state, wait_nr);
+}
+
+/** Reserve space for a number of SQEs, submitting if necessary. */
+static void ioq_reserve_sqes(struct ioq_ring_state *state, unsigned int count) {
+ while (io_uring_sq_space_left(state->ring) < count) {
+ ioq_ring_submit(state);
+ }
+}
+
+/** Get an SQE, submitting if necessary. */
+static struct io_uring_sqe *ioq_get_sqe(struct ioq_ring_state *state) {
+ ioq_reserve_sqes(state, 1);
+ return io_uring_get_sqe(state->ring);
+}
+
+/** Dispatch a single request asynchronously. */
+static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, struct ioq_ent *ent) {
+ enum ioq_ring_ops ops = state->ops;
+ struct io_uring_sqe *sqe = NULL;
+
+ switch (ent->op) {
+ case IOQ_NOP:
+ if (ent->nop.type == IOQ_NOP_HEAVY) {
+ sqe = ioq_get_sqe(state);
+ io_uring_prep_nop(sqe);
+ }
+ return sqe;
+
+ case IOQ_CLOSE:
+ if (ops & IOQ_RING_CLOSE) {
+ sqe = ioq_get_sqe(state);
+ io_uring_prep_close(sqe, ent->close.fd);
+ }
+ return sqe;
+
+ case IOQ_OPENDIR:
+ if (ops & IOQ_RING_OPENAT) {
+ sqe = ioq_get_sqe(state);
+ struct ioq_opendir *args = &ent->opendir;
+ int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY;
+ io_uring_prep_openat(sqe, args->dfd, args->path, flags, 0);
+ }
+ return sqe;
+
+ case IOQ_CLOSEDIR:
+#if BFS_USE_UNWRAPDIR
+ if (ops & IOQ_RING_CLOSE) {
+ sqe = ioq_get_sqe(state);
+ io_uring_prep_close(sqe, bfs_unwrapdir(ent->closedir.dir));
+ }
+#endif
+ return sqe;
+
+ case IOQ_STAT:
+#if BFS_USE_STATX
+ if (ops & IOQ_RING_STATX) {
+ sqe = ioq_get_sqe(state);
+ struct ioq_stat *args = &ent->stat;
+ int flags = bfs_statx_flags(args->flags);
+ unsigned int mask = bfs_statx_mask();
+ io_uring_prep_statx(sqe, args->dfd, args->path, flags, mask, args->xbuf);
+ }
+#endif
+ return sqe;
+ }
+
+ bfs_bug("Unknown ioq_op %d", (int)ent->op);
+ return NULL;
+}
+
+/** Check if ioq_ring_reap() has work to do. */
+static bool ioq_ring_empty(struct ioq_ring_state *state) {
+ return !state->prepped && !state->submitted && ioq_batch_empty(&state->ready);
+}
+
+/** Prep a single SQE. */
+static void ioq_prep_sqe(struct ioq_ring_state *state, struct ioq_ent *ent) {
+ struct ioq *ioq = state->ioq;
+ if (ioq_check_cancel(ioq, ent)) {
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+ return;
+ }
+
+ struct io_uring_sqe *sqe = ioq_dispatch_async(state, ent);
+ if (sqe) {
+ io_uring_sqe_set_data(sqe, ent);
+ ++state->prepped;
+ } else {
+ ioq_dispatch_sync(ioq, ent);
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+ }
+}
+
+/** Prep a batch of SQEs. */
+static bool ioq_ring_prep(struct ioq_ring_state *state) {
+ if (state->stop) {
+ return false;
+ }
+
+ struct ioq *ioq = state->ioq;
+
+ struct ioq_batch pending;
+ ioq_batch_reset(&pending);
+
+ while (true) {
+ bool block = ioq_ring_empty(state);
+ struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, block);
+ if (ent == &IOQ_STOP) {
+ ioqq_push(ioq->pending, ent);
+ state->stop = true;
+ break;
+ } else if (ent) {
+ ioq_prep_sqe(state, ent);
+ } else {
+ break;
+ }
+ }
+
+ bfs_assert(ioq_batch_empty(&pending));
+ return !ioq_ring_empty(state);
+}
+
+/** io_uring worker loop. */
+static int ioq_ring_work(struct ioq_thread *thread) {
+ struct io_uring *ring = &thread->ring;
+
+#ifdef IORING_SETUP_R_DISABLED
+ if (ring->flags & IORING_SETUP_R_DISABLED) {
+ if (io_uring_enable_rings(ring) != 0) {
+ return -1;
+ }
+ }
+#endif
+
+ struct ioq_ring_state state = {
+ .ioq = thread->parent,
+ .ring = ring,
+ .ops = thread->ring_ops,
+ };
+
+ while (ioq_ring_prep(&state)) {
+ ioq_ring_submit(&state);
+ }
+
+ ioq_ring_drain(&state, state.submitted);
+ return 0;
+}
+
+#endif // BFS_WITH_LIBURING
+
+/** Synchronous syscall loop. */
+static void ioq_sync_work(struct ioq_thread *thread) {
+ struct ioq *ioq = thread->parent;
+
+ struct ioq_batch pending, ready;
+ ioq_batch_reset(&pending);
+ ioq_batch_reset(&ready);
+
+ while (true) {
+ if (ioq_batch_empty(&pending)) {
+ ioq_batch_flush(ioq->ready, &ready);
+ }
+
+ struct ioq_ent *ent = ioq_batch_pop(ioq->pending, &pending, true);
+ if (ent == &IOQ_STOP) {
+ ioqq_push(ioq->pending, ent);
+ break;
+ }
+
+ if (!ioq_check_cancel(ioq, ent)) {
+ ioq_dispatch_sync(ioq, ent);
+ }
+ ioq_batch_push(ioq->ready, &ready, ent);
+ }
+
+ bfs_assert(ioq_batch_empty(&pending));
+ ioq_batch_flush(ioq->ready, &ready);
+}
+
+/** Background thread entry point. */
+static void *ioq_work(void *ptr) {
+ struct ioq_thread *thread = ptr;
+
+#if BFS_WITH_LIBURING
+ if (thread->ring_err == 0) {
+ if (ioq_ring_work(thread) == 0) {
+ return NULL;
+ }
+ }
+#endif
+
+ ioq_sync_work(thread);
+ return NULL;
+}
+
+#if BFS_WITH_LIBURING
+/** Test whether some io_uring setup flags are supported. */
+static bool ioq_ring_probe_flags(struct io_uring_params *params, unsigned int flags) {
+ unsigned int saved = params->flags;
+ params->flags |= flags;
+
+ struct io_uring ring;
+ int ret = io_uring_queue_init_params(2, &ring, params);
+ if (ret == 0) {
+ io_uring_queue_exit(&ring);
+ }
+
+ if (ret == -EINVAL) {
+ params->flags = saved;
+ return false;
+ }
+
+ return true;
+}
+#endif
+
+/** Initialize io_uring thread state. */
+static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) {
+#if BFS_WITH_LIBURING
+ struct ioq_thread *prev = NULL;
+ if (thread > ioq->threads) {
+ prev = thread - 1;
+ }
+
+ if (prev && prev->ring_err) {
+ thread->ring_err = prev->ring_err;
+ return -1;
+ }
+
+ struct io_uring_params params = {0};
+
+ if (prev) {
+ // Share io-wq workers between rings
+ params.flags = prev->ring.flags | IORING_SETUP_ATTACH_WQ;
+ params.wq_fd = prev->ring.ring_fd;
+ } else {
+#ifdef IORING_SETUP_SUBMIT_ALL
+ // Don't abort submission just because an inline request fails
+ ioq_ring_probe_flags(&params, IORING_SETUP_SUBMIT_ALL);
+#endif
+
+#ifdef IORING_SETUP_R_DISABLED
+ // Don't enable the ring yet (needed for SINGLE_ISSUER)
+ if (ioq_ring_probe_flags(&params, IORING_SETUP_R_DISABLED)) {
+# ifdef IORING_SETUP_SINGLE_ISSUER
+ // Allow optimizations assuming only one task submits SQEs
+ ioq_ring_probe_flags(&params, IORING_SETUP_SINGLE_ISSUER);
+# endif
+# ifdef IORING_SETUP_DEFER_TASKRUN
+ // Don't interrupt us aggressively with completion events
+ ioq_ring_probe_flags(&params, IORING_SETUP_DEFER_TASKRUN);
+# endif
+ }
+#endif
+ }
+
+ // Use a page for each SQE ring
+ size_t entries = 4096 / sizeof(struct io_uring_sqe);
+ thread->ring_err = -io_uring_queue_init_params(entries, &thread->ring, &params);
+ if (thread->ring_err) {
+ return -1;
+ }
+
+ if (prev) {
+ // Initial setup already complete
+ thread->ring_ops = prev->ring_ops;
+ return 0;
+ }
+
+ // Check for supported operations
+ struct io_uring_probe *probe = io_uring_get_probe_ring(&thread->ring);
+ if (probe) {
+ if (io_uring_opcode_supported(probe, IORING_OP_OPENAT)) {
+ thread->ring_ops |= IOQ_RING_OPENAT;
+ }
+ if (io_uring_opcode_supported(probe, IORING_OP_CLOSE)) {
+ thread->ring_ops |= IOQ_RING_CLOSE;
+ }
+#if BFS_USE_STATX
+ if (io_uring_opcode_supported(probe, IORING_OP_STATX)) {
+ thread->ring_ops |= IOQ_RING_STATX;
+ }
+#endif
+ io_uring_free_probe(probe);
+ }
+ if (!thread->ring_ops) {
+ io_uring_queue_exit(&thread->ring);
+ thread->ring_err = ENOTSUP;
+ return -1;
+ }
+
+#if BFS_HAS_IO_URING_MAX_WORKERS
+ // Limit the number of io_uring workers
+ unsigned int values[] = {
+ ioq->nthreads, // [IO_WQ_BOUND]
+ 0, // [IO_WQ_UNBOUND]
+ };
+ io_uring_register_iowq_max_workers(&thread->ring, values);
+#endif
+
+#endif // BFS_WITH_LIBURING
+
+ return 0;
+}
+
+/** Destroy an io_uring. */
+static void ioq_ring_exit(struct ioq_thread *thread) {
+#if BFS_WITH_LIBURING
+ if (thread->ring_err == 0) {
+ io_uring_queue_exit(&thread->ring);
+ }
+#endif
+}
+
+/** Create an I/O queue thread. */
+static int ioq_thread_create(struct ioq *ioq, size_t i) {
+ struct ioq_thread *thread = &ioq->threads[i];
+ thread->parent = ioq;
+
+ ioq_ring_init(ioq, thread);
+
+ if (thread_create(&thread->id, NULL, ioq_work, thread) != 0) {
+ ioq_ring_exit(thread);
+ return -1;
+ }
+
+ char name[16];
+ if (snprintf(name, sizeof(name), "ioq-%zu", i) >= 0) {
+ thread_setname(thread->id, name);
+ }
+
+ return 0;
+}
+
+/** Join an I/O queue thread. */
+static void ioq_thread_join(struct ioq_thread *thread) {
+ thread_join(thread->id, NULL);
+ ioq_ring_exit(thread);
+}
+
+struct ioq *ioq_create(size_t depth, size_t nthreads) {
+ struct ioq *ioq = ZALLOC_FLEX(struct ioq, threads, nthreads);
+ if (!ioq) {
+ goto fail;
+ }
+
+ ioq->depth = depth;
+
+ ARENA_INIT(&ioq->ents, struct ioq_ent);
+#if BFS_WITH_LIBURING && BFS_USE_STATX
+ ARENA_INIT(&ioq->xbufs, struct statx);
+#endif
+
+ ioq->pending = ioqq_create(depth);
+ if (!ioq->pending) {
+ goto fail;
+ }
+
+ ioq->ready = ioqq_create(depth);
+ if (!ioq->ready) {
+ goto fail;
+ }
+
+ ioq->nthreads = nthreads;
+ for (size_t i = 0; i < nthreads; ++i) {
+ if (ioq_thread_create(ioq, i) != 0) {
+ ioq->nthreads = i;
+ goto fail;
+ }
+ }
+
+ return ioq;
+
+ int err;
+fail:
+ err = errno;
+ ioq_destroy(ioq);
+ errno = err;
+ return NULL;
+}
+
+size_t ioq_capacity(const struct ioq *ioq) {
+ return ioq->depth - ioq->size;
+}
+
+static struct ioq_ent *ioq_request(struct ioq *ioq, enum ioq_op op, void *ptr) {
+ if (load(&ioq->cancel, relaxed)) {
+ errno = EINTR;
+ return NULL;
+ }
+
+ if (ioq->size >= ioq->depth) {
+ errno = EAGAIN;
+ return NULL;
+ }
+
+ struct ioq_ent *ent = arena_alloc(&ioq->ents);
+ if (!ent) {
+ return NULL;
+ }
+
+ ent->op = op;
+ ent->ptr = ptr;
+ ++ioq->size;
+ return ent;
+}
+
+int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_NOP, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ ent->nop.type = type;
+
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, ent);
+ return 0;
+}
+
+int ioq_close(struct ioq *ioq, int fd, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSE, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ ent->close.fd = fd;
+
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, ent);
+ return 0;
+}
+
+int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_OPENDIR, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ struct ioq_opendir *args = &ent->opendir;
+ args->dir = dir;
+ args->dfd = dfd;
+ args->path = path;
+ args->flags = flags;
+
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, ent);
+ return 0;
+}
+
+int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSEDIR, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ ent->closedir.dir = dir;
+
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, ent);
+ return 0;
+}
+
+int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_STAT, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ struct ioq_stat *args = &ent->stat;
+ args->dfd = dfd;
+ args->path = path;
+ args->flags = flags;
+ args->buf = buf;
+
+#if BFS_WITH_LIBURING && BFS_USE_STATX
+ args->xbuf = arena_alloc(&ioq->xbufs);
+ if (!args->xbuf) {
+ ioq_free(ioq, ent);
+ return -1;
+ }
+#endif
+
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, ent);
+ return 0;
+}
+
+void ioq_submit(struct ioq *ioq) {
+ ioq_batch_flush(ioq->pending, &ioq->pending_batch);
+}
+
+struct ioq_ent *ioq_pop(struct ioq *ioq, bool block) {
+ // Don't forget to submit before popping
+ bfs_assert(ioq_batch_empty(&ioq->pending_batch));
+
+ if (ioq->size == 0) {
+ return NULL;
+ }
+
+ return ioq_batch_pop(ioq->ready, &ioq->ready_batch, block);
+}
+
+void ioq_free(struct ioq *ioq, struct ioq_ent *ent) {
+ bfs_assert(ioq->size > 0);
+ --ioq->size;
+
+#if BFS_WITH_LIBURING && BFS_USE_STATX
+ if (ent->op == IOQ_STAT && ent->stat.xbuf) {
+ arena_free(&ioq->xbufs, ent->stat.xbuf);
+ }
+#endif
+
+ arena_free(&ioq->ents, ent);
+}
+
+void ioq_cancel(struct ioq *ioq) {
+ if (!exchange(&ioq->cancel, true, relaxed)) {
+ ioq_batch_push(ioq->pending, &ioq->pending_batch, &IOQ_STOP);
+ ioq_submit(ioq);
+ }
+}
+
+void ioq_destroy(struct ioq *ioq) {
+ if (!ioq) {
+ return;
+ }
+
+ if (ioq->nthreads > 0) {
+ ioq_cancel(ioq);
+ }
+
+ for (size_t i = 0; i < ioq->nthreads; ++i) {
+ ioq_thread_join(&ioq->threads[i]);
+ }
+
+ ioqq_destroy(ioq->ready);
+ ioqq_destroy(ioq->pending);
+
+#if BFS_WITH_LIBURING && BFS_USE_STATX
+ arena_destroy(&ioq->xbufs);
+#endif
+ arena_destroy(&ioq->ents);
+
+ free(ioq);
+}
diff --git a/src/ioq.h b/src/ioq.h
new file mode 100644
index 0000000..5eaa066
--- /dev/null
+++ b/src/ioq.h
@@ -0,0 +1,227 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Asynchronous I/O queues.
+ */
+
+#ifndef BFS_IOQ_H
+#define BFS_IOQ_H
+
+#include "bfs.h"
+#include "dir.h"
+#include "stat.h"
+
+#include <stddef.h>
+
+/**
+ * An queue of asynchronous I/O operations.
+ */
+struct ioq;
+
+/**
+ * I/O queue operations.
+ */
+enum ioq_op {
+ /** ioq_nop(). */
+ IOQ_NOP,
+ /** ioq_close(). */
+ IOQ_CLOSE,
+ /** ioq_opendir(). */
+ IOQ_OPENDIR,
+ /** ioq_closedir(). */
+ IOQ_CLOSEDIR,
+ /** ioq_stat(). */
+ IOQ_STAT,
+};
+
+/**
+ * ioq_nop() types.
+ */
+enum ioq_nop_type {
+ /** A lightweight nop that avoids syscalls. */
+ IOQ_NOP_LIGHT,
+ /** A heavyweight nop that involves a syscall. */
+ IOQ_NOP_HEAVY,
+};
+
+/**
+ * An I/O queue entry.
+ */
+struct ioq_ent {
+ /** The I/O operation. */
+ cache_align enum ioq_op op;
+
+ /** The return value (on success) or negative error code (on failure). */
+ int result;
+
+ /** Arbitrary user data. */
+ void *ptr;
+
+ /** Operation-specific arguments. */
+ union {
+ /** ioq_nop() args. */
+ struct ioq_nop {
+ enum ioq_nop_type type;
+ } nop;
+ /** ioq_close() args. */
+ struct ioq_close {
+ int fd;
+ } close;
+ /** ioq_opendir() args. */
+ struct ioq_opendir {
+ struct bfs_dir *dir;
+ const char *path;
+ int dfd;
+ enum bfs_dir_flags flags;
+ } opendir;
+ /** ioq_closedir() args. */
+ struct ioq_closedir {
+ struct bfs_dir *dir;
+ } closedir;
+ /** ioq_stat() args. */
+ struct ioq_stat {
+ const char *path;
+ struct bfs_stat *buf;
+ void *xbuf;
+ int dfd;
+ enum bfs_stat_flags flags;
+ } stat;
+ };
+};
+
+/**
+ * Create an I/O queue.
+ *
+ * @depth
+ * The maximum depth of the queue.
+ * @nthreads
+ * The maximum number of background threads.
+ * @return
+ * The new I/O queue, or NULL on failure.
+ */
+struct ioq *ioq_create(size_t depth, size_t nthreads);
+
+/**
+ * Check the remaining capacity of a queue.
+ */
+size_t ioq_capacity(const struct ioq *ioq);
+
+/**
+ * A no-op, for benchmarking.
+ *
+ * @ioq
+ * The I/O queue.
+ * @type
+ * The type of operation to perform.
+ * @ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_nop(struct ioq *ioq, enum ioq_nop_type type, void *ptr);
+
+/**
+ * Asynchronous close().
+ *
+ * @ioq
+ * The I/O queue.
+ * @fd
+ * The fd to close.
+ * @ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_close(struct ioq *ioq, int fd, void *ptr);
+
+/**
+ * Asynchronous bfs_opendir().
+ *
+ * @ioq
+ * The I/O queue.
+ * @dir
+ * The allocated directory.
+ * @dfd
+ * The base file descriptor.
+ * @path
+ * The path to open, relative to dfd.
+ * @flags
+ * Flags that control which directory entries are listed.
+ * @ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr);
+
+/**
+ * Asynchronous bfs_closedir().
+ *
+ * @ioq
+ * The I/O queue.
+ * @dir
+ * The directory to close.
+ * @ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr);
+
+/**
+ * Asynchronous bfs_stat().
+ *
+ * @ioq
+ * The I/O queue.
+ * @dfd
+ * The base file descriptor.
+ * @path
+ * The path to stat, relative to dfd.
+ * @flags
+ * Flags that affect the lookup.
+ * @buf
+ * A place to store the stat buffer, if successful.
+ * @ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr);
+
+/**
+ * Submit any buffered requests.
+ */
+void ioq_submit(struct ioq *ioq);
+
+/**
+ * Pop a response from the queue.
+ *
+ * @ioq
+ * The I/O queue.
+ * @return
+ * The next response, or NULL.
+ */
+struct ioq_ent *ioq_pop(struct ioq *ioq, bool block);
+
+/**
+ * Free a queue entry.
+ *
+ * @ioq
+ * The I/O queue.
+ * @ent
+ * The entry to free.
+ */
+void ioq_free(struct ioq *ioq, struct ioq_ent *ent);
+
+/**
+ * Cancel any pending I/O operations.
+ */
+void ioq_cancel(struct ioq *ioq);
+
+/**
+ * Stop and destroy an I/O queue.
+ */
+void ioq_destroy(struct ioq *ioq);
+
+#endif // BFS_IOQ_H
diff --git a/src/list.h b/src/list.h
new file mode 100644
index 0000000..276c610
--- /dev/null
+++ b/src/list.h
@@ -0,0 +1,613 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Intrusive linked lists.
+ *
+ * Singly-linked lists are declared like this:
+ *
+ * struct item {
+ * struct item *next;
+ * };
+ *
+ * struct list {
+ * struct item *head;
+ * struct item **tail;
+ * };
+ *
+ * The SLIST_*() macros manipulate singly-linked lists.
+ *
+ * struct list list;
+ * SLIST_INIT(&list);
+ *
+ * struct item item;
+ * SLIST_ITEM_INIT(&item);
+ * SLIST_APPEND(&list, &item);
+ *
+ * Doubly linked lists are similar:
+ *
+ * struct item {
+ * struct item *next;
+ * struct item *prev;
+ * };
+ *
+ * struct list {
+ * struct item *head;
+ * struct item *tail;
+ * };
+ *
+ * struct list list;
+ * LIST_INIT(&list);
+ *
+ * struct item item;
+ * LIST_ITEM_INIT(&item);
+ * LIST_APPEND(&list, &item);
+ *
+ * Items can be on multiple lists at once:
+ *
+ * struct item {
+ * struct {
+ * struct item *next;
+ * } chain;
+ *
+ * struct {
+ * struct item *next;
+ * struct item *prev;
+ * } lru;
+ * };
+ *
+ * struct items {
+ * struct {
+ * struct item *head;
+ * struct item **tail;
+ * } queue;
+ *
+ * struct {
+ * struct item *head;
+ * struct item *tail;
+ * } cache;
+ * };
+ *
+ * struct items items;
+ * SLIST_INIT(&items.queue);
+ * LIST_INIT(&items.cache);
+ *
+ * struct item item;
+ * SLIST_ITEM_INIT(&item, chain);
+ * SLIST_APPEND(&items.queue, &item, chain);
+ * LIST_ITEM_INIT(&item, lru);
+ * LIST_APPEND(&items.cache, &item, lru);
+ */
+
+#ifndef BFS_LIST_H
+#define BFS_LIST_H
+
+#include "diag.h"
+
+#include <stddef.h>
+#include <string.h>
+
+/**
+ * Initialize a singly-linked list.
+ *
+ * @list
+ * The list to initialize.
+ *
+ * ---
+ *
+ * Like many macros in this file, this macro delegates the bulk of its work to
+ * some helper macros. We explicitly parenthesize (list) here so the helpers
+ * don't have to.
+ */
+#define SLIST_INIT(list) \
+ SLIST_INIT_((list))
+
+/**
+ * Helper for SLIST_INIT().
+ */
+#define SLIST_INIT_(list) LIST_VOID_( \
+ list->head = NULL, \
+ list->tail = &list->head)
+
+/**
+ * Cast a list of expressions to void.
+ */
+#define LIST_VOID_(...) ((void)(__VA_ARGS__))
+
+/**
+ * Initialize a singly-linked list item.
+ *
+ * @item
+ * The item to initialize.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ *
+ * ---
+ *
+ * We play some tricks with variadic macros to handle the optional parameter:
+ *
+ * SLIST_ITEM_INIT(item) => item->next = NULL
+ * SLIST_ITEM_INIT(item, node) => item->node.next = NULL
+ *
+ * The first trick is that
+ *
+ * #define SLIST_ITEM_INIT(item, ...)
+ *
+ * won't work because both commas are required (until C23; see N3033). As a
+ * workaround, we dispatch to another macro and add a trailing comma.
+ *
+ * SLIST_ITEM_INIT(item) => SLIST_ITEM_INIT_(item, )
+ * SLIST_ITEM_INIT(item, node) => SLIST_ITEM_INIT_(item, node, )
+ */
+#define SLIST_ITEM_INIT(...) \
+ SLIST_ITEM_INIT_(__VA_ARGS__, )
+
+/**
+ * Now we need a way to generate either ->next or ->node.next depending on
+ * whether the node parameter was passed. The approach is based on
+ *
+ * #define FOO(...) BAR(__VA_ARGS__, 1, 2, )
+ * #define BAR(x, y, z, ...) z
+ *
+ * FOO(a) => 2
+ * FOO(a, b) => 1
+ *
+ * The LIST_NEXT_() macro uses this technique:
+ *
+ * LIST_NEXT_() => LIST_NODE_(next, )
+ * LIST_NEXT_(node, ) => LIST_NODE_(next, node, )
+ */
+#define LIST_NEXT_(...) \
+ LIST_NODE_(next, __VA_ARGS__)
+
+/**
+ * LIST_NODE_() dispatches to yet another macro:
+ *
+ * LIST_NODE_(next, ) => LIST_NODE__(next, , . , , )
+ * LIST_NODE_(next, node, ) => LIST_NODE__(next, node, , . , , )
+ */
+#define LIST_NODE_(dir, ...) \
+ LIST_NODE__(dir, __VA_ARGS__, . , , )
+
+/**
+ * And finally, LIST_NODE__() adds the node and the dot if necessary.
+ *
+ * dir node ignored dot
+ * v v v v
+ * LIST_NODE__(next, , . , , ) => next
+ * LIST_NODE__(next, node, , . , , ) => node . next
+ * ^ ^ ^ ^
+ * dir node ignored dot
+ */
+#define LIST_NODE__(dir, node, ignored, dot, ...) \
+ node dot dir
+
+/**
+ * SLIST_ITEM_INIT_() uses LIST_NEXT_() to generate the right name for the list
+ * node, and finally delegates to the actual implementation.
+ */
+#define SLIST_ITEM_INIT_(item, ...) \
+ SLIST_ITEM_INIT__((item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_ITEM_INIT__(item, next) \
+ LIST_VOID_(item->next = NULL)
+
+/**
+ * Type-checking macro for singly-linked lists.
+ */
+#define SLIST_CHECK_(list) \
+ (void)sizeof(list->tail - &list->head)
+
+/**
+ * Get the head of a singly-linked list.
+ *
+ * @list
+ * The list in question.
+ * @return
+ * The first item in the list.
+ */
+#define SLIST_HEAD(list) \
+ SLIST_HEAD_((list))
+
+#define SLIST_HEAD_(list) \
+ (SLIST_CHECK_(list), list->head)
+
+/**
+ * Check if a singly-linked list is empty.
+ */
+#define SLIST_EMPTY(list) \
+ (!SLIST_HEAD(list))
+
+/**
+ * Like container_of(), but using the head pointer instead of offsetof() since
+ * we don't have the type around.
+ */
+#define SLIST_CONTAINER_(tail, head, next) \
+ (void *)((char *)tail - ((char *)&head->next - (char *)head))
+
+/**
+ * Get the tail of a singly-linked list.
+ *
+ * @list
+ * The list in question.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * The last item in the list.
+ */
+#define SLIST_TAIL(...) \
+ SLIST_TAIL_(__VA_ARGS__, )
+
+#define SLIST_TAIL_(list, ...) \
+ SLIST_TAIL__((list), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_TAIL__(list, next) \
+ (list->head ? SLIST_CONTAINER_(list->tail, list->head, next) : NULL)
+
+/**
+ * Check if an item is attached to a singly-linked list.
+ *
+ * @list
+ * The list to check.
+ * @item
+ * The item to check.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * Whether the item is attached to the list.
+ */
+#define SLIST_ATTACHED(list, ...) \
+ SLIST_ATTACHED_(list, __VA_ARGS__, )
+
+#define SLIST_ATTACHED_(list, item, ...) \
+ SLIST_ATTACHED__((list), (item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_ATTACHED__(list, item, next) \
+ (item->next || list->tail == &item->next)
+
+/**
+ * Insert an item into a singly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @cursor
+ * A pointer to the item to insert after, e.g. &list->head or list->tail.
+ * @item
+ * The item to insert.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * A cursor for the next item.
+ */
+#define SLIST_INSERT(list, cursor, ...) \
+ SLIST_INSERT_(list, cursor, __VA_ARGS__, )
+
+#define SLIST_INSERT_(list, cursor, item, ...) \
+ SLIST_INSERT__((list), (cursor), (item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_INSERT__(list, cursor, item, next) \
+ (bfs_assert(!SLIST_ATTACHED__(list, item, next)), \
+ item->next = *cursor, \
+ *cursor = item, \
+ list->tail = item->next ? list->tail : &item->next, \
+ &item->next)
+
+/**
+ * Add an item to the tail of a singly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @item
+ * The item to append.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define SLIST_APPEND(list, ...) \
+ SLIST_APPEND_(list, __VA_ARGS__, )
+
+#define SLIST_APPEND_(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT_(list, (list)->tail, item, __VA_ARGS__))
+
+/**
+ * Add an item to the head of a singly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @item
+ * The item to prepend.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define SLIST_PREPEND(list, ...) \
+ SLIST_PREPEND_(list, __VA_ARGS__, )
+
+#define SLIST_PREPEND_(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__))
+
+/**
+ * Splice a singly-linked list into another.
+ *
+ * @dest
+ * The destination list.
+ * @cursor
+ * A pointer to the item to splice after, e.g. &list->head or list->tail.
+ * @src
+ * The source list.
+ */
+#define SLIST_SPLICE(dest, cursor, src) \
+ LIST_VOID_(SLIST_SPLICE_((dest), (cursor), (src)))
+
+#define SLIST_SPLICE_(dest, cursor, src) \
+ *src->tail = *cursor, \
+ *cursor = src->head, \
+ dest->tail = *dest->tail ? src->tail : dest->tail, \
+ SLIST_INIT(src)
+
+/**
+ * Add an entire singly-linked list to the tail of another.
+ *
+ * @dest
+ * The destination list.
+ * @src
+ * The source list.
+ */
+#define SLIST_EXTEND(dest, src) \
+ SLIST_SPLICE(dest, (dest)->tail, src)
+
+/**
+ * Remove an item from a singly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @cursor
+ * A pointer to the item to remove, either &list->head or &prev->next.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * The removed item.
+ */
+#define SLIST_REMOVE(list, ...) \
+ SLIST_REMOVE_(list, __VA_ARGS__, )
+
+#define SLIST_REMOVE_(list, cursor, ...) \
+ SLIST_REMOVE__((list), (cursor), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_REMOVE__(list, cursor, next) \
+ (list->tail = (*cursor)->next ? list->tail : cursor, \
+ slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor)))
+
+// Helper for SLIST_REMOVE()
+static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) {
+ // ret = *cursor;
+ // *cursor = ret->next;
+ memcpy(cursor, next, size);
+ // ret->next = NULL;
+ memset(next, 0, size);
+ return ret;
+}
+
+/**
+ * Pop the head off a singly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @node (optional)
+ * If specified, use head->node.next rather than head->next.
+ * @return
+ * The popped item, or NULL if the list was empty.
+ */
+#define SLIST_POP(...) \
+ SLIST_POP_(__VA_ARGS__, )
+
+#define SLIST_POP_(list, ...) \
+ SLIST_POP__((list), __VA_ARGS__)
+
+#define SLIST_POP__(list, ...) \
+ (list->head ? SLIST_REMOVE_(list, &list->head, __VA_ARGS__) : NULL)
+
+/**
+ * Loop over the items in a singly-linked list.
+ *
+ * @type
+ * The list item type.
+ * @item
+ * The induction variable name.
+ * @list
+ * The list to iterate.
+ * @node (optional)
+ * If specified, use head->node.next rather than head->next.
+ */
+#define for_slist(type, item, ...) \
+ for_slist_(type, item, __VA_ARGS__, )
+
+#define for_slist_(type, item, list, ...) \
+ for_slist__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+
+#define for_slist__(type, item, list, next) \
+ for (type *item = list->head, *_next; \
+ item && (SLIST_CHECK_(list), _next = item->next, true); \
+ item = _next)
+
+/**
+ * Loop over a singly-linked list, popping each item.
+ *
+ * @type
+ * The list item type.
+ * @item
+ * The induction variable name.
+ * @list
+ * The list to drain.
+ * @node (optional)
+ * If specified, use head->node.next rather than head->next.
+ */
+#define drain_slist(type, item, ...) \
+ for (type *item; (item = SLIST_POP(__VA_ARGS__));)
+
+/**
+ * Initialize a doubly-linked list.
+ *
+ * @list
+ * The list to initialize.
+ */
+#define LIST_INIT(list) \
+ LIST_INIT_((list))
+
+#define LIST_INIT_(list) \
+ LIST_VOID_(list->head = list->tail = NULL)
+
+/**
+ * LIST_PREV_() => prev
+ * LIST_PREV_(node, ) => node.prev
+ */
+#define LIST_PREV_(...) \
+ LIST_NODE_(prev, __VA_ARGS__)
+
+/**
+ * Initialize a doubly-linked list item.
+ *
+ * @item
+ * The item to initialize.
+ * @node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define LIST_ITEM_INIT(...) \
+ LIST_ITEM_INIT_(__VA_ARGS__, )
+
+#define LIST_ITEM_INIT_(item, ...) \
+ LIST_ITEM_INIT__((item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_ITEM_INIT__(item, prev, next) \
+ LIST_VOID_(item->prev = item->next = NULL)
+
+/**
+ * Type-checking macro for doubly-linked lists.
+ */
+#define LIST_CHECK_(list) \
+ (void)sizeof(list->tail - list->head)
+
+/**
+ * Check if a doubly-linked list is empty.
+ */
+#define LIST_EMPTY(list) \
+ LIST_EMPTY_((list))
+
+#define LIST_EMPTY_(list) \
+ (LIST_CHECK_(list), !list->head)
+
+/**
+ * Add an item to the tail of a doubly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @item
+ * The item to append.
+ * @node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_APPEND(list, ...) \
+ LIST_INSERT(list, (list)->tail, __VA_ARGS__)
+
+/**
+ * Add an item to the head of a doubly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @item
+ * The item to prepend.
+ * @node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_PREPEND(list, ...) \
+ LIST_INSERT(list, NULL, __VA_ARGS__)
+
+/**
+ * Check if an item is attached to a doubly-linked list.
+ *
+ * @list
+ * The list to check.
+ * @item
+ * The item to check.
+ * @node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ * @return
+ * Whether the item is attached to the list.
+ */
+#define LIST_ATTACHED(list, ...) \
+ LIST_ATTACHED_(list, __VA_ARGS__, )
+
+#define LIST_ATTACHED_(list, item, ...) \
+ LIST_ATTACHED__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_ATTACHED__(list, item, prev, next) \
+ (item->prev || item->next || list->head == item || list->tail == item)
+
+/**
+ * Insert into a doubly-linked list after the given cursor.
+ *
+ * @list
+ * The list to modify.
+ * @cursor
+ * Insert after this element.
+ * @item
+ * The item to insert.
+ * @node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_INSERT(list, cursor, ...) \
+ LIST_INSERT_(list, cursor, __VA_ARGS__, )
+
+#define LIST_INSERT_(list, cursor, item, ...) \
+ LIST_INSERT__((list), (cursor), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_INSERT__(list, cursor, item, prev, next) LIST_VOID_( \
+ bfs_assert(!LIST_ATTACHED__(list, item, prev, next)), \
+ item->prev = cursor, \
+ item->next = cursor ? cursor->next : list->head, \
+ *(item->prev ? &item->prev->next : &list->head) = item, \
+ *(item->next ? &item->next->prev : &list->tail) = item)
+
+/**
+ * Remove an item from a doubly-linked list.
+ *
+ * @list
+ * The list to modify.
+ * @item
+ * The item to remove.
+ * @node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_REMOVE(list, ...) \
+ LIST_REMOVE_(list, __VA_ARGS__, )
+
+#define LIST_REMOVE_(list, item, ...) \
+ LIST_REMOVE__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_REMOVE__(list, item, prev, next) LIST_VOID_( \
+ *(item->prev ? &item->prev->next : &list->head) = item->next, \
+ *(item->next ? &item->next->prev : &list->tail) = item->prev, \
+ item->prev = item->next = NULL)
+
+/**
+ * Loop over the items in a doubly-linked list.
+ *
+ * @type
+ * The list item type.
+ * @item
+ * The induction variable name.
+ * @list
+ * The list to iterate.
+ * @node (optional)
+ * If specified, use head->node.next rather than head->next.
+ */
+#define for_list(type, item, ...) \
+ for_list_(type, item, __VA_ARGS__, )
+
+#define for_list_(type, item, list, ...) \
+ for_list__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+
+#define for_list__(type, item, list, next) \
+ for (type *item = list->head, *_next; \
+ item && (LIST_CHECK_(list), _next = item->next, true); \
+ item = _next)
+
+#endif // BFS_LIST_H
diff --git a/main.c b/src/main.c
index 7ceb0a2..da07508 100644
--- a/main.c
+++ b/src/main.c
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* - main(): the entry point for bfs(1), a breadth-first version of find(1)
@@ -33,33 +20,46 @@
* - bftw.[ch] (an extended version of nftw(3))
*
* - Utilities:
- * - bfs.h (constants about bfs itself)
+ * - prelude.h (feature test macros; automatically included)
+ * - alloc.[ch] (memory allocation)
+ * - atomic.h (atomic operations)
* - bar.[ch] (a terminal status bar)
+ * - bit.h (bit manipulation)
+ * - bfs.h (configuration and fundamental utilities)
+ * - bfstd.[ch] (standard library wrappers/polyfills)
* - color.[ch] (for pretty terminal colors)
- * - darray.[ch] (a dynamic array library)
* - diag.[ch] (formats diagnostic messages)
* - dir.[ch] (a directory API facade)
* - dstring.[ch] (a dynamic string library)
* - fsade.[ch] (a facade over non-standard filesystem features)
+ * - ioq.[ch] (an async I/O queue)
+ * - list.h (linked list macros)
* - mtab.[ch] (parses the system's mount table)
* - pwcache.[ch] (a cache for the user/group tables)
- * - spawn.[ch] (spawns processes)
+ * - sanity.h (sanitizer interfaces)
+ * - sighook.[ch] (signal hooks)
* - stat.[ch] (wraps stat(), or statx() on Linux)
- * - time.[ch] (date/time handling utilities)
+ * - thread.h (multi-threading)
* - trie.[ch] (a trie set/map implementation)
* - typo.[ch] (fuzzy matching for typos)
- * - util.[ch] (everything else)
+ * - version.c (embeds version information)
+ * - xregex.[ch] (regular expression support)
+ * - xspawn.[ch] (spawns processes)
+ * - xtime.[ch] (date/time handling utilities)
*/
+#include "bfstd.h"
#include "ctx.h"
+#include "diag.h"
#include "eval.h"
#include "parse.h"
+
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
-#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
+#include <time.h>
#include <unistd.h>
/**
@@ -79,9 +79,7 @@ static int redirect(int fd, const char *path, int flags) {
}
int ret = dup2(newfd, fd);
- int err = errno;
- close(newfd);
- errno = err;
+ close_quietly(newfd);
return ret;
}
@@ -117,25 +115,38 @@ static int open_std_streams(void) {
* bfs entry point.
*/
int main(int argc, char *argv[]) {
- int ret = EXIT_FAILURE;
-
// Make sure the standard streams are open
if (open_std_streams() != 0) {
- goto done;
+ return EXIT_FAILURE;
}
// Use the system locale instead of "C"
- setlocale(LC_ALL, "");
+ int locale_err = 0;
+ if (!setlocale(LC_ALL, "")) {
+ locale_err = errno;
+ }
+
+ // Apply the environment's timezone
+ tzset();
+ // Parse the command line
struct bfs_ctx *ctx = bfs_parse_cmdline(argc, argv);
- if (ctx) {
- ret = bfs_eval(ctx);
+ if (!ctx) {
+ return EXIT_FAILURE;
}
+ // Warn if setlocale() failed, unless there's no expression to evaluate
+ if (locale_err && ctx->warn && ctx->expr) {
+ bfs_warning(ctx, "Failed to set locale: %s\n\n", xstrerror(locale_err));
+ }
+
+ // Walk the file system tree, evaluating the expression on each file
+ int ret = bfs_eval(ctx);
+
+ // Free the parsed command line, and detect any last-minute errors
if (bfs_ctx_free(ctx) != 0 && ret == EXIT_SUCCESS) {
ret = EXIT_FAILURE;
}
-done:
return ret;
}
diff --git a/src/mtab.c b/src/mtab.c
new file mode 100644
index 0000000..40a9885
--- /dev/null
+++ b/src/mtab.c
@@ -0,0 +1,304 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "mtab.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "stat.h"
+#include "trie.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#ifndef BFS_USE_MNTENT
+# define BFS_USE_MNTENT BFS_HAS_GETMNTENT_1
+#endif
+#ifndef BFS_USE_MNTINFO
+# define BFS_USE_MNTINFO (!BFS_USE_MNTENT && BFS_HAS_GETMNTINFO)
+#endif
+#ifndef BFS_USE_MNTTAB
+# define BFS_USE_MNTTAB (!BFS_USE_MNTINFO && BFS_HAS_GETMNTENT_2)
+#endif
+
+#if BFS_USE_MNTENT
+# include <mntent.h>
+# include <paths.h>
+# include <stdio.h>
+#elif BFS_USE_MNTINFO
+# include <sys/mount.h>
+#elif BFS_USE_MNTTAB
+# include <stdio.h>
+# include <sys/mnttab.h>
+#endif
+
+/**
+ * A mount point in the table.
+ */
+struct bfs_mount {
+ /** The path to the mount point. */
+ char *path;
+ /** The filesystem type. */
+ char *type;
+ /** Buffer for the strings. */
+ char buf[];
+};
+
+struct bfs_mtab {
+ /** Mount point arena. */
+ struct varena varena;
+
+ /** The array of mount points. */
+ struct bfs_mount **mounts;
+ /** The number of mount points. */
+ size_t nmounts;
+
+ /** The basenames of every mount point. */
+ struct trie names;
+
+ /** A map from device ID to fstype (populated lazily). */
+ struct trie types;
+ /** Whether the types map has been populated. */
+ bool types_filled;
+};
+
+/**
+ * Add an entry to the mount table.
+ */
+_maybe_unused
+static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) {
+ size_t path_size = strlen(path) + 1;
+ size_t type_size = strlen(type) + 1;
+ size_t size = path_size + type_size;
+ struct bfs_mount *mount = varena_alloc(&mtab->varena, size);
+ if (!mount) {
+ return -1;
+ }
+
+ struct bfs_mount **ptr = RESERVE(struct bfs_mount *, &mtab->mounts, &mtab->nmounts);
+ if (!ptr) {
+ goto free;
+ }
+ *ptr = mount;
+
+ mount->path = mount->buf;
+ memcpy(mount->path, path, path_size);
+
+ mount->type = mount->buf + path_size;
+ memcpy(mount->type, type, type_size);
+
+ const char *name = path + xbaseoff(path);
+ if (!trie_insert_str(&mtab->names, name)) {
+ goto shrink;
+ }
+
+ return 0;
+
+shrink:
+ --mtab->nmounts;
+free:
+ varena_free(&mtab->varena, mount, size);
+ return -1;
+}
+
+struct bfs_mtab *bfs_mtab_parse(void) {
+ struct bfs_mtab *mtab = ZALLOC(struct bfs_mtab);
+ if (!mtab) {
+ return NULL;
+ }
+
+ VARENA_INIT(&mtab->varena, struct bfs_mount, buf);
+
+ trie_init(&mtab->names);
+ trie_init(&mtab->types);
+
+ int error = 0;
+
+#if BFS_USE_MNTENT
+
+ FILE *file = setmntent(_PATH_MOUNTED, "r");
+ if (!file) {
+ // In case we're in a chroot or something with /proc but no /etc/mtab
+ error = errno;
+ file = setmntent("/proc/mounts", "r");
+ }
+ if (!file) {
+ goto fail;
+ }
+
+ struct mntent *mnt;
+ while ((mnt = getmntent(file))) {
+ if (bfs_mtab_add(mtab, mnt->mnt_dir, mnt->mnt_type) != 0) {
+ error = errno;
+ endmntent(file);
+ goto fail;
+ }
+ }
+
+ endmntent(file);
+
+#elif BFS_USE_MNTINFO
+
+#if __NetBSD__
+ typedef struct statvfs bfs_statfs;
+#else
+ typedef struct statfs bfs_statfs;
+#endif
+
+ bfs_statfs *mntbuf;
+ int size = getmntinfo(&mntbuf, MNT_WAIT);
+ if (size <= 0) {
+ error = errno;
+ goto fail;
+ }
+
+ for (bfs_statfs *mnt = mntbuf; mnt < mntbuf + size; ++mnt) {
+ if (bfs_mtab_add(mtab, mnt->f_mntonname, mnt->f_fstypename) != 0) {
+ error = errno;
+ goto fail;
+ }
+ }
+
+#elif BFS_USE_MNTTAB
+
+ FILE *file = xfopen(MNTTAB, O_RDONLY | O_CLOEXEC);
+ if (!file) {
+ error = errno;
+ goto fail;
+ }
+
+ struct mnttab mnt;
+ while (getmntent(file, &mnt) == 0) {
+ if (bfs_mtab_add(mtab, mnt.mnt_mountp, mnt.mnt_fstype) != 0) {
+ error = errno;
+ fclose(file);
+ goto fail;
+ }
+ }
+
+ fclose(file);
+
+#else
+
+ error = ENOTSUP;
+ goto fail;
+
+#endif
+
+ return mtab;
+
+fail:
+ bfs_mtab_free(mtab);
+ errno = error;
+ return NULL;
+}
+
+static int bfs_mtab_fill_types(struct bfs_mtab *mtab) {
+ const enum bfs_stat_flags flags = BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC;
+ int ret = -1;
+
+ // It's possible that /path/to/mount was unmounted between bfs_mtab_parse() and bfs_mtab_fill_types().
+ // In that case, the dev_t of /path/to/mount will be the same as /path/to, which should not get its
+ // fstype from the old mount record of /path/to/mount.
+ //
+ // Detect this by comparing the st_dev of the parent (/path/to) and child (/path/to/mount). Only when
+ // they differ can the filesystem type actually change between them. As a minor optimization, we keep
+ // the parent directory open in case multiple mounts have the same parent (e.g. /mnt).
+ char *parent_dir = NULL;
+ int parent_fd = -1;
+ int parent_ret = -1;
+ struct bfs_stat parent_stat;
+
+ for (size_t i = 0; i < mtab->nmounts; ++i) {
+ struct bfs_mount *mount = mtab->mounts[i];
+ const char *path = mount->path;
+ int fd = AT_FDCWD;
+
+ char *dir = xdirname(path);
+ if (!dir) {
+ goto fail;
+ }
+
+ if (parent_dir && strcmp(parent_dir, dir) == 0) {
+ // Same parent
+ free(dir);
+ } else {
+ free(parent_dir);
+ parent_dir = dir;
+
+ if (parent_fd >= 0) {
+ xclose(parent_fd);
+ }
+ parent_fd = open(parent_dir, O_SEARCH | O_CLOEXEC | O_DIRECTORY);
+
+ parent_ret = -1;
+ if (parent_fd >= 0) {
+ parent_ret = bfs_stat(parent_fd, NULL, flags, &parent_stat);
+ }
+ }
+
+ if (parent_fd >= 0) {
+ fd = parent_fd;
+ path += xbaseoff(path);
+ }
+
+ struct bfs_stat sb;
+ if (bfs_stat(fd, path, flags, &sb) != 0) {
+ continue;
+ }
+
+ if (parent_ret == 0 && parent_stat.dev == sb.dev && parent_stat.ino != sb.ino) {
+ // Not a mount point any more (or a bind mount, but with the same fstype)
+ continue;
+ }
+
+ if (trie_set_mem(&mtab->types, &sb.mnt_id, sizeof(sb.mnt_id), mount->type) != 0) {
+ goto fail;
+ }
+ }
+
+ mtab->types_filled = true;
+ ret = 0;
+
+fail:
+ if (parent_fd >= 0) {
+ xclose(parent_fd);
+ }
+ free(parent_dir);
+ return ret;
+}
+
+const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) {
+ if (!mtab->types_filled) {
+ if (bfs_mtab_fill_types((struct bfs_mtab *)mtab) != 0) {
+ return NULL;
+ }
+ }
+
+ const char *type = trie_get_mem(&mtab->types, &statbuf->mnt_id, sizeof(statbuf->mnt_id));
+ if (type) {
+ return type;
+ } else {
+ return "unknown";
+ }
+}
+
+bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name) {
+ return trie_find_str(&mtab->names, name);
+}
+
+void bfs_mtab_free(struct bfs_mtab *mtab) {
+ if (mtab) {
+ trie_destroy(&mtab->types);
+ trie_destroy(&mtab->names);
+
+ free(mtab->mounts);
+ varena_destroy(&mtab->varena);
+
+ free(mtab);
+ }
+}
diff --git a/src/mtab.h b/src/mtab.h
new file mode 100644
index 0000000..090392b
--- /dev/null
+++ b/src/mtab.h
@@ -0,0 +1,56 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A facade over platform-specific APIs for enumerating mounted filesystems.
+ */
+
+#ifndef BFS_MTAB_H
+#define BFS_MTAB_H
+
+struct bfs_stat;
+
+/**
+ * A file system mount table.
+ */
+struct bfs_mtab;
+
+/**
+ * Parse the mount table.
+ *
+ * @return
+ * The parsed mount table, or NULL on error.
+ */
+struct bfs_mtab *bfs_mtab_parse(void);
+
+/**
+ * Determine the file system type that a file is on.
+ *
+ * @mtab
+ * The current mount table.
+ * @statbuf
+ * The bfs_stat() buffer for the file in question.
+ * @return
+ * The type of file system containing this file, "unknown" if not known,
+ * or NULL on error.
+ */
+const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf);
+
+/**
+ * Check if a file could be a mount point.
+ *
+ * @mtab
+ * The current mount table.
+ * @name
+ * The name of the file to check.
+ * @return
+ * Whether the named file could be a mount point.
+ */
+bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name);
+
+/**
+ * Free a mount table.
+ */
+void bfs_mtab_free(struct bfs_mtab *mtab);
+
+#endif // BFS_MTAB_H
diff --git a/src/opt.c b/src/opt.c
new file mode 100644
index 0000000..9094794
--- /dev/null
+++ b/src/opt.c
@@ -0,0 +1,2357 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * The expression optimizer. Different optimization levels are supported:
+ *
+ * -O1: basic logical simplifications, like folding (-true -and -foo) to -foo.
+ *
+ * -O2: dead code elimination and data flow analysis. struct df_domain is used
+ * to record data flow facts that are true at various points of evaluation.
+ * Specifically, struct df_domain records the state before an expression is
+ * evaluated (opt->before), and after an expression returns true
+ * (opt->after_true) or false (opt->after_false). Additionally, opt->impure
+ * records the possible state before any expression with side effects is
+ * evaluated.
+ *
+ * -O3: expression re-ordering to reduce expected cost. In an expression like
+ * (-foo -and -bar), if both -foo and -bar are pure (no side effects), they can
+ * be re-ordered to (-bar -and -foo). This is profitable if the expected cost
+ * is lower for the re-ordered expression, for example if -foo is very slow or
+ * -bar is likely to return false.
+ *
+ * -O4/-Ofast: aggressive optimizations that may affect correctness in corner
+ * cases. The main effect is to use opt->impure to determine if any side-
+ * effects are reachable at all, skipping the traversal if not.
+ */
+
+#include "opt.h"
+
+#include "bfs.h"
+#include "bfstd.h"
+#include "bftw.h"
+#include "bit.h"
+#include "color.h"
+#include "ctx.h"
+#include "diag.h"
+#include "dir.h"
+#include "eval.h"
+#include "exec.h"
+#include "expr.h"
+#include "list.h"
+#include "pwcache.h"
+#include "xspawn.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static char *fake_and_arg = "-and";
+static char *fake_or_arg = "-or";
+static char *fake_not_arg = "-not";
+
+/**
+ * The data flow domain for predicates.
+ */
+enum df_pred {
+ /** The bottom state (unreachable). */
+ PRED_BOTTOM = 0,
+ /** The predicate is known to be false. */
+ PRED_FALSE = 1 << false,
+ /** The predicate is known to be true. */
+ PRED_TRUE = 1 << true,
+ /** The top state (unknown). */
+ PRED_TOP = PRED_FALSE | PRED_TRUE,
+};
+
+/** Make a predicate known. */
+static void constrain_pred(enum df_pred *pred, bool value) {
+ *pred &= 1 << value;
+}
+
+/** Compute the join (union) of two predicates. */
+static void pred_join(enum df_pred *dest, enum df_pred src) {
+ *dest |= src;
+}
+
+/**
+ * Types of predicates we track.
+ */
+enum pred_type {
+ /** -readable */
+ READABLE_PRED,
+ /** -writable */
+ WRITABLE_PRED,
+ /** -executable */
+ EXECUTABLE_PRED,
+ /** -acl */
+ ACL_PRED,
+ /** -capable */
+ CAPABLE_PRED,
+ /** -empty */
+ EMPTY_PRED,
+ /** -hidden */
+ HIDDEN_PRED,
+ /** -nogroup */
+ NOGROUP_PRED,
+ /** -nouser */
+ NOUSER_PRED,
+ /** -sparse */
+ SPARSE_PRED,
+ /** -xattr */
+ XATTR_PRED,
+ /** The number of pred_types. */
+ PRED_TYPES,
+};
+
+/** Predicate type names. */
+static const char *const pred_names[] = {
+ [READABLE_PRED] = "-readable",
+ [WRITABLE_PRED] = "-writable",
+ [EXECUTABLE_PRED] = "-executable",
+ [ACL_PRED] = "-acl",
+ [CAPABLE_PRED] = "-capable",
+ [EMPTY_PRED] = "-empty",
+ [HIDDEN_PRED] = "-hidden",
+ [NOGROUP_PRED] = "-nogroup",
+ [NOUSER_PRED] = "-nouser",
+ [SPARSE_PRED] = "-sparse",
+ [XATTR_PRED] = "-xattr",
+};
+
+/**
+ * A constrained integer range.
+ */
+struct df_range {
+ /** The (inclusive) minimum value. */
+ long long min;
+ /** The (inclusive) maximum value. */
+ long long max;
+};
+
+/** Initialize an empty range. */
+static void range_init_bottom(struct df_range *range) {
+ range->min = LLONG_MAX;
+ range->max = LLONG_MIN;
+}
+
+/** Check if a range is empty. */
+static bool range_is_bottom(const struct df_range *range) {
+ return range->min > range->max;
+}
+
+/** Initialize a full range. */
+static void range_init_top(struct df_range *range) {
+ // All ranges we currently track are non-negative
+ range->min = 0;
+ range->max = LLONG_MAX;
+}
+
+/** Check for an infinite range. */
+static bool range_is_top(const struct df_range *range) {
+ return range->min == 0 && range->max == LLONG_MAX;
+}
+
+/** Compute the minimum of two values. */
+static long long min_value(long long a, long long b) {
+ if (a < b) {
+ return a;
+ } else {
+ return b;
+ }
+}
+
+/** Compute the maximum of two values. */
+static long long max_value(long long a, long long b) {
+ if (a > b) {
+ return a;
+ } else {
+ return b;
+ }
+}
+
+/** Constrain the minimum of a range. */
+static void constrain_min(struct df_range *range, long long value) {
+ range->min = max_value(range->min, value);
+}
+
+/** Constrain the maximum of a range. */
+static void constrain_max(struct df_range *range, long long value) {
+ range->max = min_value(range->max, value);
+}
+
+/** Constrain a range to a single value. */
+static void constrain_range(struct df_range *range, long long value) {
+ constrain_min(range, value);
+ constrain_max(range, value);
+}
+
+/** Remove a single value from a range. */
+static void range_remove(struct df_range *range, long long value) {
+ if (range->min == value) {
+ if (range->min == LLONG_MAX) {
+ range->max = LLONG_MIN;
+ } else {
+ ++range->min;
+ }
+ }
+
+ if (range->max == value) {
+ if (range->max == LLONG_MIN) {
+ range->min = LLONG_MAX;
+ } else {
+ --range->max;
+ }
+ }
+}
+
+/** Compute the union of two ranges. */
+static void range_join(struct df_range *dest, const struct df_range *src) {
+ dest->min = min_value(dest->min, src->min);
+ dest->max = max_value(dest->max, src->max);
+}
+
+/**
+ * Types of ranges we track.
+ */
+enum range_type {
+ /** Search tree depth. */
+ DEPTH_RANGE,
+ /** Group ID. */
+ GID_RANGE,
+ /** Inode number. */
+ INUM_RANGE,
+ /** Hard link count. */
+ LINKS_RANGE,
+ /** File size. */
+ SIZE_RANGE,
+ /** User ID. */
+ UID_RANGE,
+ /** The number of range_types. */
+ RANGE_TYPES,
+};
+
+/** 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.
+ */
+struct df_domain {
+ /** The predicates we track. */
+ enum df_pred preds[PRED_TYPES];
+
+ /** The value ranges we track. */
+ struct df_range ranges[RANGE_TYPES];
+
+ /** Bitmask of possible -types. */
+ unsigned int types;
+ /** Bitmask of possible -xtypes. */
+ unsigned int xtypes;
+};
+
+/** Set a data flow value to bottom. */
+static void df_init_bottom(struct df_domain *value) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ value->preds[i] = PRED_BOTTOM;
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ range_init_bottom(&value->ranges[i]);
+ }
+
+ value->types = 0;
+ value->xtypes = 0;
+}
+
+/** Determine whether a fact set is impossible. */
+static bool df_is_bottom(const struct df_domain *value) {
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (range_is_bottom(&value->ranges[i])) {
+ return true;
+ }
+ }
+
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] == PRED_BOTTOM) {
+ return true;
+ }
+ }
+
+ if (!value->types || !value->xtypes) {
+ return true;
+ }
+
+ return false;
+}
+
+/** Initialize some data flow value. */
+static void df_init_top(struct df_domain *value) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ value->preds[i] = PRED_TOP;
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ range_init_top(&value->ranges[i]);
+ }
+
+ value->types = ~0;
+ value->xtypes = ~0;
+}
+
+/** Check for the top element. */
+static bool df_is_top(const struct df_domain *value) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] != PRED_TOP) {
+ return false;
+ }
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (!range_is_top(&value->ranges[i])) {
+ return false;
+ }
+ }
+
+ if (value->types != ~0U) {
+ return false;
+ }
+
+ if (value->xtypes != ~0U) {
+ return false;
+ }
+
+ return true;
+}
+
+/** Compute the union of two fact sets. */
+static void df_join(struct df_domain *dest, const struct df_domain *src) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ pred_join(&dest->preds[i], src->preds[i]);
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ range_join(&dest->ranges[i], &src->ranges[i]);
+ }
+
+ dest->types |= src->types;
+ dest->xtypes |= src->xtypes;
+}
+
+/**
+ * Optimizer state.
+ */
+struct bfs_opt {
+ /** The context we're optimizing. */
+ struct bfs_ctx *ctx;
+ /** Optimization level (ctx->optlevel). */
+ int level;
+ /** Recursion depth. */
+ int depth;
+
+ /** Whether to produce warnings. */
+ bool warn;
+ /** Whether the result of this expression is ignored. */
+ bool ignore_result;
+
+ /** Data flow state before this expression is evaluated. */
+ struct df_domain before;
+ /** Data flow state after this expression returns true. */
+ struct df_domain after_true;
+ /** Data flow state after this expression returns false. */
+ struct df_domain after_false;
+ /** Data flow state before any side-effecting expressions are evaluated. */
+ struct df_domain *impure;
+};
+
+/** Log an optimization. */
+_printf(2, 3)
+static bool opt_debug(struct bfs_opt *opt, const char *format, ...) {
+ if (bfs_debug_prefix(opt->ctx, DEBUG_OPT)) {
+ for (int i = 0; i < opt->depth; ++i) {
+ cfprintf(opt->ctx->cerr, "│ ");
+ }
+
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/** Log a recursive call. */
+_printf(2, 3)
+static bool opt_enter(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+ if (depth > 0) {
+ --opt->depth;
+ }
+
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─╮ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+
+ opt->depth = depth + 1;
+ return debug;
+}
+
+/** Log a recursive return. */
+_printf(2, 3)
+static bool opt_leave(struct bfs_opt *opt, const char *format, ...) {
+ bool debug = false;
+ int depth = opt->depth;
+
+ if (format) {
+ if (depth > 1) {
+ opt->depth -= 2;
+ }
+
+ debug = opt_debug(opt, "%s", depth > 1 ? "├─╯ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+ }
+
+ opt->depth = depth - 1;
+ return debug;
+}
+
+/** Log a shallow visit. */
+_printf(2, 3)
+static bool opt_visit(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+ if (depth > 0) {
+ --opt->depth;
+ }
+
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─◯ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+
+ opt->depth = depth;
+ return debug;
+}
+
+/** Log the deletion of an expression. */
+_printf(2, 3)
+static bool opt_delete(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+
+ if (depth > 0) {
+ --opt->depth;
+ }
+
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─✘ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+
+ opt->depth = depth;
+ return debug;
+}
+
+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_names[type]);
+
+ FILE *file = opt->ctx->cerr->file;
+ switch (value->preds[type]) {
+ case PRED_BOTTOM:
+ fprintf(file, "⊥\n");
+ break;
+ case PRED_TOP:
+ fprintf(file, "⊤\n");
+ break;
+ case PRED_TRUE:
+ fprintf(file, "true\n");
+ break;
+ case PRED_FALSE:
+ fprintf(file, "false\n");
+ break;
+ }
+}
+
+/** Print a df_range. */
+static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) {
+ dump(opt, "${blu}%s${rs}: ", range_names[type]);
+
+ FILE *file = opt->ctx->cerr->file;
+ const struct df_range *range = &value->ranges[type];
+ if (range_is_bottom(range)) {
+ fprintf(file, "⊥\n");
+ } else if (range_is_top(range)) {
+ fprintf(file, "⊤\n");
+ } else if (range->min == range->max) {
+ fprintf(file, "%lld\n", range->min);
+ } else {
+ if (range->min == LLONG_MIN) {
+ fprintf(file, "(-∞, ");
+ } else {
+ fprintf(file, "[%lld, ", range->min);
+ }
+ if (range->max == LLONG_MAX) {
+ fprintf(file, "∞)\n");
+ } else {
+ fprintf(file, "%lld]\n", range->max);
+ }
+ }
+}
+
+/** Print a set of types. */
+static void types_dump(dump_fn *dump, struct bfs_opt *opt, const char *name, unsigned int types) {
+ dump(opt, "${blu}%s${rs}: ", name);
+
+ FILE *file = opt->ctx->cerr->file;
+ if (types == 0) {
+ fprintf(file, " ⊥\n");
+ } else if (types == ~0U) {
+ fprintf(file, " ⊤\n");
+ } else if (count_ones(types) < count_ones(~types)) {
+ fprintf(file, " 0x%X\n", types);
+ } else {
+ fprintf(file, "~0x%X\n", ~types);
+ }
+}
+
+/** Calculate the number of lines of df_dump() output. */
+static int df_dump_lines(const struct df_domain *value) {
+ int lines = 0;
+
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ lines += value->preds[i] != PRED_TOP;
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ lines += !range_is_top(&value->ranges[i]);
+ }
+
+ lines += value->types != ~0U;
+ lines += value->xtypes != ~0U;
+
+ return lines;
+}
+
+/** Get the right debugging function for a df_dump() line. */
+static dump_fn *df_dump_line(int lines, int *line) {
+ ++*line;
+
+ if (lines == 1) {
+ return opt_visit;
+ } else if (*line == 1) {
+ return opt_enter;
+ } else if (*line == lines) {
+ return opt_leave;
+ } else {
+ return opt_debug;
+ }
+}
+
+/** Print a data flow value. */
+static void df_dump(struct bfs_opt *opt, const char *str, const struct df_domain *value) {
+ if (df_is_bottom(value)) {
+ opt_debug(opt, "%s: ⊥\n", str);
+ return;
+ } else if (df_is_top(value)) {
+ opt_debug(opt, "%s: ⊤\n", str);
+ return;
+ }
+
+ if (!opt_debug(opt, "%s:\n", str)) {
+ return;
+ }
+
+ int lines = df_dump_lines(value);
+ int line = 0;
+
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] != PRED_TOP) {
+ pred_dump(df_dump_line(lines, &line), opt, value, i);
+ }
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (!range_is_top(&value->ranges[i])) {
+ range_dump(df_dump_line(lines, &line), opt, value, i);
+ }
+ }
+
+ if (value->types != ~0U) {
+ types_dump(df_dump_line(lines, &line), opt, "-type", value->types);
+ }
+
+ if (value->xtypes != ~0U) {
+ types_dump(df_dump_line(lines, &line), opt, "-xtype", value->xtypes);
+ }
+}
+
+/** Check if an expression is constant. */
+static bool is_const(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_true || expr->eval_fn == eval_false;
+}
+
+/** Warn about an expression. */
+_printf(3, 4)
+static bool opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) {
+ if (!opt->warn) {
+ return false;
+ }
+
+ if (bfs_expr_is_parent(expr) || is_const(expr)) {
+ return false;
+ }
+
+ if (!bfs_expr_warning(opt->ctx, expr)) {
+ return false;
+ }
+
+ va_list args;
+ va_start(args, format);
+ bfs_vwarning(opt->ctx, format, args);
+ va_end(args);
+
+ return true;
+}
+
+/** Remove and return an expression's children. */
+static void foster_children(struct bfs_expr *expr, struct bfs_exprs *children) {
+ bfs_assert(bfs_expr_is_parent(expr));
+
+ SLIST_INIT(children);
+ SLIST_EXTEND(children, &expr->children);
+
+ expr->persistent_fds = 0;
+ expr->ephemeral_fds = 0;
+ expr->pure = true;
+}
+
+/** Return an expression's only child. */
+static struct bfs_expr *only_child(struct bfs_expr *expr) {
+ bfs_assert(bfs_expr_is_parent(expr));
+ struct bfs_expr *child = bfs_expr_children(expr);
+ bfs_assert(child && !child->next);
+ return child;
+}
+
+/** Foster an expression's only child. */
+static struct bfs_expr *foster_only_child(struct bfs_expr *expr) {
+ struct bfs_expr *child = only_child(expr);
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+ return child;
+}
+
+/** An expression visitor. */
+struct visitor;
+
+/** An expression-visiting function. */
+typedef struct bfs_expr *visit_fn(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor);
+
+/** An entry in a visitor lookup table. */
+struct visitor_table {
+ /** The evaluation function to match on. */
+ bfs_eval_fn *eval_fn;
+ /** The visitor function. */
+ visit_fn *visit;
+};
+
+/** Look up a visitor in a table. */
+static visit_fn *look_up_visitor(const struct bfs_expr *expr, const struct visitor_table table[]) {
+ for (size_t i = 0; table[i].eval_fn; ++i) {
+ if (expr->eval_fn == table[i].eval_fn) {
+ return table[i].visit;
+ }
+ }
+
+ return NULL;
+}
+
+struct visitor {
+ /** The name of this visitor. */
+ const char *name;
+
+ /** A function to call before visiting children. */
+ visit_fn *enter;
+ /** The default visitor. */
+ visit_fn *visit;
+ /** A function to call after visiting children. */
+ visit_fn *leave;
+
+ /** A visitor lookup table. */
+ const struct visitor_table *table;
+};
+
+/** Recursive visitor implementation. */
+static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor);
+
+/** Visit a negation. */
+static struct bfs_expr *visit_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = foster_only_child(expr);
+
+ struct bfs_opt nested = *opt;
+ rhs = visit_deep(&nested, rhs, visitor);
+ if (!rhs) {
+ return NULL;
+ }
+
+ opt->after_true = nested.after_false;
+ opt->after_false = nested.after_true;
+
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/** Visit a conjunction. */
+static struct bfs_expr *visit_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ // Base case (-and) == (-true)
+ df_init_bottom(&opt->after_false);
+ struct bfs_opt nested = *opt;
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = false;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
+ }
+
+ df_join(&opt->after_false, &nested.after_false);
+ nested.before = nested.after_true;
+
+ bfs_expr_append(expr, child);
+ }
+
+ opt->after_true = nested.after_true;
+
+ return expr;
+}
+
+/** Visit a disjunction. */
+static struct bfs_expr *visit_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ // Base case (-or) == (-false)
+ df_init_bottom(&opt->after_true);
+ struct bfs_opt nested = *opt;
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = false;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
+ }
+
+ df_join(&opt->after_true, &nested.after_true);
+ nested.before = nested.after_false;
+
+ bfs_expr_append(expr, child);
+ }
+
+ opt->after_false = nested.after_false;
+
+ return expr;
+}
+
+/** Visit a comma expression. */
+static struct bfs_expr *visit_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_opt nested = *opt;
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = true;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
+ }
+
+ nested.before = nested.after_true;
+ df_join(&nested.before, &nested.after_false);
+
+ bfs_expr_append(expr, child);
+ }
+
+ opt->after_true = nested.after_true;
+ opt->after_false = nested.after_false;
+
+ return expr;
+}
+
+/** Default enter() function. */
+static struct bfs_expr *visit_enter(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_enter(opt, "%pe\n", expr);
+ opt->after_true = opt->before;
+ opt->after_false = opt->before;
+ return expr;
+}
+
+/** Default leave() function. */
+static struct bfs_expr *visit_leave(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_leave(opt, "%pe\n", expr);
+ return expr;
+}
+
+static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ bool entered = false;
+
+ visit_fn *enter = visitor->enter ? visitor->enter : visit_enter;
+ visit_fn *leave = visitor->leave ? visitor->leave : visit_leave;
+
+ static const struct visitor_table table[] = {
+ {eval_not, visit_not},
+ {eval_and, visit_and},
+ {eval_or, visit_or},
+ {eval_comma, visit_comma},
+ {NULL, NULL},
+ };
+ visit_fn *recursive = look_up_visitor(expr, table);
+ if (recursive) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ entered = true;
+ }
+
+ expr = recursive(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ visit_fn *general = visitor->visit;
+ if (general) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ entered = true;
+ }
+
+ expr = general(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ visit_fn *specific = look_up_visitor(expr, visitor->table);
+ if (specific) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ entered = true;
+ }
+
+ expr = specific(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ if (entered) {
+ expr = leave(opt, expr, visitor);
+ } else {
+ opt_visit(opt, "%pe\n", expr);
+ }
+
+ return expr;
+}
+
+/** Visit an expression recursively. */
+static struct bfs_expr *visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_enter(opt, "%s()\n", visitor->name);
+ expr = visit_deep(opt, expr, visitor);
+ opt_leave(opt, "\n");
+ return expr;
+}
+
+/** Visit an expression non-recursively. */
+static struct bfs_expr *visit_shallow(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ visit_fn *general = visitor->visit;
+ if (expr && general) {
+ expr = general(opt, expr, visitor);
+ }
+
+ if (!expr) {
+ return NULL;
+ }
+
+ visit_fn *specific = look_up_visitor(expr, visitor->table);
+ if (specific) {
+ expr = specific(opt, expr, visitor);
+ }
+
+ return expr;
+}
+
+/** Annotate -{execut,read,writ}able. */
+static struct bfs_expr *annotate_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->probability = 1.0;
+ if (expr->num & R_OK) {
+ expr->probability *= 0.99;
+ }
+ if (expr->num & W_OK) {
+ expr->probability *= 0.8;
+ }
+ if (expr->num & X_OK) {
+ expr->probability *= 0.2;
+ }
+
+ return expr;
+}
+
+/** Annotate -empty. */
+static struct bfs_expr *annotate_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->level >= 4) {
+ // Since -empty attempts to open and read directories, it may
+ // have side effects such as reporting permission errors, and
+ // thus shouldn't be re-ordered without aggressive optimizations
+ expr->pure = true;
+ }
+
+ return expr;
+}
+
+/** Annotate -exec. */
+static struct bfs_expr *annotate_exec(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (expr->exec->flags & BFS_EXEC_MULTI) {
+ expr->always_true = true;
+ } else {
+ expr->cost = 1000000.0;
+ }
+
+ return expr;
+}
+
+/** Annotate -name/-lname/-path. */
+static struct bfs_expr *annotate_fnmatch(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (expr->literal) {
+ expr->probability = 0.1;
+ } else {
+ expr->probability = 0.5;
+ }
+
+ return expr;
+}
+
+/** Annotate -f?print. */
+static struct bfs_expr *annotate_fprint(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ const struct colors *colors = expr->cfile->colors;
+ expr->calls_stat = colors && colors_need_stat(colors);
+ return expr;
+}
+
+/** Estimate probability for -x?type. */
+static void estimate_type_probability(struct bfs_expr *expr) {
+ unsigned int types = expr->num;
+
+ expr->probability = 0.0;
+ if (types & (1 << BFS_BLK)) {
+ expr->probability += 0.00000721183;
+ }
+ if (types & (1 << BFS_CHR)) {
+ expr->probability += 0.0000499855;
+ }
+ if (types & (1 << BFS_DIR)) {
+ expr->probability += 0.114475;
+ }
+ if (types & (1 << BFS_DOOR)) {
+ expr->probability += 0.000001;
+ }
+ if (types & (1 << BFS_FIFO)) {
+ expr->probability += 0.00000248684;
+ }
+ if (types & (1 << BFS_REG)) {
+ expr->probability += 0.859772;
+ }
+ if (types & (1 << BFS_LNK)) {
+ expr->probability += 0.0256816;
+ }
+ if (types & (1 << BFS_SOCK)) {
+ expr->probability += 0.0000116881;
+ }
+ if (types & (1 << BFS_WHT)) {
+ expr->probability += 0.000001;
+ }
+}
+
+/** Annotate -type. */
+static struct bfs_expr *annotate_type(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ estimate_type_probability(expr);
+ return expr;
+}
+
+/** Annotate -xtype. */
+static struct bfs_expr *annotate_xtype(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->level >= 4) {
+ // Since -xtype dereferences symbolic links, it may have side
+ // effects such as reporting permission errors, and thus
+ // shouldn't be re-ordered without aggressive optimizations
+ expr->pure = true;
+ }
+
+ estimate_type_probability(expr);
+ return expr;
+}
+
+/** Annotate a negation. */
+static struct bfs_expr *annotate_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = only_child(expr);
+ expr->pure = rhs->pure;
+ expr->always_true = rhs->always_false;
+ expr->always_false = rhs->always_true;
+ expr->cost = rhs->cost;
+ expr->probability = 1.0 - rhs->probability;
+ return expr;
+}
+
+/** Annotate a conjunction. */
+static struct bfs_expr *annotate_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->pure = true;
+ expr->always_true = true;
+ expr->always_false = false;
+ expr->cost = 0.0;
+ expr->probability = 1.0;
+
+ for_expr (child, expr) {
+ expr->pure &= child->pure;
+ expr->always_true &= child->always_true;
+ expr->always_false |= child->always_false;
+ expr->cost += expr->probability * child->cost;
+ expr->probability *= child->probability;
+ }
+
+ return expr;
+}
+
+/** Annotate a disjunction. */
+static struct bfs_expr *annotate_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->pure = true;
+ expr->always_true = false;
+ expr->always_false = true;
+ expr->cost = 0.0;
+
+ float false_prob = 1.0;
+ for_expr (child, expr) {
+ expr->pure &= child->pure;
+ expr->always_true |= child->always_true;
+ expr->always_false &= child->always_false;
+ expr->cost += false_prob * child->cost;
+ false_prob *= (1.0 - child->probability);
+ }
+ expr->probability = 1.0 - false_prob;
+
+ return expr;
+}
+
+/** Annotate a comma expression. */
+static struct bfs_expr *annotate_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->pure = true;
+ expr->cost = 0.0;
+
+ for_expr (child, expr) {
+ expr->pure &= child->pure;
+ expr->always_true = child->always_true;
+ expr->always_false = child->always_false;
+ expr->cost += child->cost;
+ expr->probability = child->probability;
+ }
+
+ return expr;
+}
+
+/** Annotate an arbitrary expression. */
+static struct bfs_expr *annotate_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ /** Table of pure expressions. */
+ static bfs_eval_fn *const pure[] = {
+ eval_access,
+ eval_acl,
+ eval_capable,
+ eval_depth,
+ eval_false,
+ eval_flags,
+ eval_fstype,
+ eval_gid,
+ eval_hidden,
+ eval_inum,
+ eval_links,
+ eval_lname,
+ eval_name,
+ eval_newer,
+ eval_nogroup,
+ eval_nouser,
+ eval_path,
+ eval_perm,
+ eval_regex,
+ eval_samefile,
+ eval_size,
+ eval_sparse,
+ eval_time,
+ eval_true,
+ eval_type,
+ eval_uid,
+ eval_used,
+ eval_xattr,
+ eval_xattrname,
+ };
+
+ expr->pure = false;
+ for (size_t i = 0; i < countof(pure); ++i) {
+ if (expr->eval_fn == pure[i]) {
+ expr->pure = true;
+ break;
+ }
+ }
+
+ /** Table of always-true expressions. */
+ static bfs_eval_fn *const always_true[] = {
+ eval_fls,
+ eval_fprint,
+ eval_fprint0,
+ eval_fprintf,
+ eval_fprintx,
+ eval_limit,
+ eval_prune,
+ eval_true,
+ // Non-returning
+ eval_exit,
+ eval_quit,
+ };
+
+ expr->always_true = false;
+ for (size_t i = 0; i < countof(always_true); ++i) {
+ if (expr->eval_fn == always_true[i]) {
+ expr->always_true = true;
+ break;
+ }
+ }
+
+ /** Table of always-false expressions. */
+ static bfs_eval_fn *const always_false[] = {
+ eval_false,
+ // Non-returning
+ eval_exit,
+ eval_quit,
+ };
+
+ expr->always_false = false;
+ for (size_t i = 0; i < countof(always_false); ++i) {
+ if (expr->eval_fn == always_false[i]) {
+ expr->always_false = true;
+ break;
+ }
+ }
+
+ /** Table of stat-calling primaries. */
+ static bfs_eval_fn *const calls_stat[] = {
+ eval_empty,
+ eval_flags,
+ eval_fls,
+ eval_fprintf,
+ eval_fstype,
+ eval_gid,
+ eval_inum,
+ eval_links,
+ eval_newer,
+ eval_nogroup,
+ eval_nouser,
+ eval_perm,
+ eval_samefile,
+ eval_size,
+ eval_sparse,
+ eval_time,
+ eval_uid,
+ eval_used,
+ eval_xattr,
+ eval_xattrname,
+ };
+
+ expr->calls_stat = false;
+ for (size_t i = 0; i < countof(calls_stat); ++i) {
+ if (expr->eval_fn == calls_stat[i]) {
+ expr->calls_stat = true;
+ break;
+ }
+ }
+
+#define FAST_COST 40.0
+#define FNMATCH_COST 400.0
+#define STAT_COST 1000.0
+#define PRINT_COST 20000.0
+
+ /** Table of expression costs. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ float cost;
+ } costs[] = {
+ {eval_access, STAT_COST},
+ {eval_acl, STAT_COST},
+ {eval_capable, STAT_COST},
+ {eval_empty, 2 * STAT_COST}, // readdir() is worse than stat()
+ {eval_flags, STAT_COST},
+ {eval_fls, PRINT_COST},
+ {eval_fprint, PRINT_COST},
+ {eval_fprint0, PRINT_COST},
+ {eval_fprintf, PRINT_COST},
+ {eval_fprintx, PRINT_COST},
+ {eval_fstype, STAT_COST},
+ {eval_gid, STAT_COST},
+ {eval_inum, STAT_COST},
+ {eval_links, STAT_COST},
+ {eval_lname, FNMATCH_COST},
+ {eval_name, FNMATCH_COST},
+ {eval_newer, STAT_COST},
+ {eval_nogroup, STAT_COST},
+ {eval_nouser, STAT_COST},
+ {eval_path, FNMATCH_COST},
+ {eval_perm, STAT_COST},
+ {eval_samefile, STAT_COST},
+ {eval_size, STAT_COST},
+ {eval_sparse, STAT_COST},
+ {eval_time, STAT_COST},
+ {eval_uid, STAT_COST},
+ {eval_used, STAT_COST},
+ {eval_xattr, STAT_COST},
+ {eval_xattrname, STAT_COST},
+ };
+
+ expr->cost = FAST_COST;
+ for (size_t i = 0; i < countof(costs); ++i) {
+ if (expr->eval_fn == costs[i].eval_fn) {
+ expr->cost = costs[i].cost;
+ break;
+ }
+ }
+
+ /** Table of expression probabilities. */
+ static const struct {
+ /** The evaluation function with this cost. */
+ bfs_eval_fn *eval_fn;
+ /** The matching probability. */
+ float probability;
+ } probs[] = {
+ {eval_acl, 0.00002},
+ {eval_capable, 0.000002},
+ {eval_empty, 0.01},
+ {eval_false, 0.0},
+ {eval_hidden, 0.01},
+ {eval_nogroup, 0.01},
+ {eval_nouser, 0.01},
+ {eval_samefile, 0.01},
+ {eval_true, 1.0},
+ {eval_xattr, 0.01},
+ {eval_xattrname, 0.01},
+ };
+
+ expr->probability = 0.5;
+ for (size_t i = 0; i < countof(probs); ++i) {
+ if (expr->eval_fn == probs[i].eval_fn) {
+ expr->probability = probs[i].probability;
+ break;
+ }
+ }
+
+ return expr;
+}
+
+/**
+ * Annotating visitor.
+ */
+static const struct visitor annotate = {
+ .name = "annotate",
+ .visit = annotate_visit,
+ .table = (const struct visitor_table[]) {
+ {eval_access, annotate_access},
+ {eval_empty, annotate_empty},
+ {eval_exec, annotate_exec},
+ {eval_fprint, annotate_fprint},
+ {eval_lname, annotate_fnmatch},
+ {eval_name, annotate_fnmatch},
+ {eval_path, annotate_fnmatch},
+ {eval_type, annotate_type},
+ {eval_xtype, annotate_xtype},
+
+ {eval_not, annotate_not},
+ {eval_and, annotate_and},
+ {eval_or, annotate_or},
+ {eval_comma, annotate_comma},
+
+ {NULL, NULL},
+ },
+};
+
+/** Create a constant expression. */
+static struct bfs_expr *opt_const(struct bfs_opt *opt, bool value) {
+ static bfs_eval_fn *const fns[] = {eval_false, eval_true};
+ static char *fake_args[] = {"-false", "-true"};
+
+ struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value], BFS_TEST);
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/** Negate an expression, keeping it canonical. */
+static struct bfs_expr *negate_expr(struct bfs_opt *opt, struct bfs_expr *expr, char **argv) {
+ if (expr->eval_fn == eval_not) {
+ return only_child(expr);
+ } else if (expr->eval_fn == eval_true) {
+ return opt_const(opt, false);
+ } else if (expr->eval_fn == eval_false) {
+ return opt_const(opt, true);
+ }
+
+ struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv, BFS_OPERATOR);
+ if (!ret) {
+ return NULL;
+ }
+
+ bfs_expr_append(ret, expr);
+ return visit_shallow(opt, ret, &annotate);
+}
+
+/** Sink negations into a conjunction/disjunction using De Morgan's laws. */
+static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *expr) {
+ opt_debug(opt, "De Morgan's laws\n");
+
+ char **argv = expr->argv;
+ expr = only_child(expr);
+ opt_enter(opt, "%pe\n", expr);
+
+ if (expr->eval_fn == eval_and) {
+ expr->eval_fn = eval_or;
+ expr->argv = &fake_or_arg;
+ } else {
+ bfs_assert(expr->eval_fn == eval_or);
+ expr->eval_fn = eval_and;
+ expr->argv = &fake_and_arg;
+ }
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ opt_enter(opt, "%pe\n", child);
+
+ child = negate_expr(opt, child, argv);
+ if (!child) {
+ return NULL;
+ }
+
+ opt_leave(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+ }
+
+ opt_leave(opt, "%pe\n", expr);
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/** Sink a negation into a comma expression. */
+static struct bfs_expr *sink_not_comma(struct bfs_opt *opt, struct bfs_expr *expr) {
+ char **argv = expr->argv;
+ expr = only_child(expr);
+ opt_enter(opt, "%pe\n", expr);
+
+ bfs_assert(expr->eval_fn == eval_comma);
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (SLIST_EMPTY(&children)) {
+ opt_enter(opt, "%pe\n", child);
+ opt_debug(opt, "sink\n");
+
+ child = negate_expr(opt, child, argv);
+ if (!child) {
+ return NULL;
+ }
+
+ opt_leave(opt, "%pe\n", child);
+ } else {
+ opt_visit(opt, "%pe\n", child);
+ }
+
+ bfs_expr_append(expr, child);
+ }
+
+ opt_leave(opt, "%pe\n", expr);
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/** Canonicalize a negation. */
+static struct bfs_expr *canonicalize_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = only_child(expr);
+
+ if (rhs->eval_fn == eval_not) {
+ opt_debug(opt, "double negation\n");
+ return only_child(rhs);
+ } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) {
+ return sink_not_andor(opt, expr);
+ } else if (rhs->eval_fn == eval_comma) {
+ return sink_not_comma(opt, expr);
+ } else if (is_const(rhs)) {
+ opt_debug(opt, "constant propagation\n");
+ return opt_const(opt, rhs->eval_fn == eval_false);
+ } else {
+ return expr;
+ }
+}
+
+/** Canonicalize an associative operator. */
+static struct bfs_expr *canonicalize_assoc(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_exprs flat;
+ SLIST_INIT(&flat);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (child->eval_fn == expr->eval_fn) {
+ struct bfs_expr *head = SLIST_HEAD(&child->children);
+ struct bfs_expr *tail = SLIST_TAIL(&child->children);
+
+ if (!head) {
+ opt_delete(opt, "%pe [empty]\n", child);
+ } else {
+ opt_enter(opt, "%pe\n", child);
+ opt_debug(opt, "associativity\n");
+ if (head == tail) {
+ opt_leave(opt, "%pe\n", head);
+ } else if (head->next == tail) {
+ opt_leave(opt, "%pe %pe\n", head, tail);
+ } else {
+ opt_leave(opt, "%pe ... %pe\n", head, tail);
+ }
+ }
+
+ SLIST_EXTEND(&flat, &child->children);
+ } else {
+ opt_visit(opt, "%pe\n", child);
+ SLIST_APPEND(&flat, child);
+ }
+ }
+
+ bfs_expr_extend(expr, &flat);
+
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/**
+ * Canonicalizing visitor.
+ */
+static const struct visitor canonicalize = {
+ .name = "canonicalize",
+ .table = (const struct visitor_table[]) {
+ {eval_not, canonicalize_not},
+ {eval_and, canonicalize_assoc},
+ {eval_or, canonicalize_assoc},
+ {eval_comma, canonicalize_assoc},
+ {NULL, NULL},
+ },
+};
+
+/** Calculate the cost of an ordered pair of expressions. */
+static float expr_cost(const struct bfs_expr *parent, const struct bfs_expr *lhs, const struct bfs_expr *rhs) {
+ // https://cs.stackexchange.com/a/66921/21004
+ float prob = lhs->probability;
+ if (parent->eval_fn == eval_or) {
+ prob = 1.0 - prob;
+ }
+ return lhs->cost + prob * rhs->cost;
+}
+
+/** Sort a block of expressions. */
+static void sort_exprs(struct bfs_opt *opt, struct bfs_expr *parent, struct bfs_exprs *exprs) {
+ if (!exprs->head || !exprs->head->next) {
+ return;
+ }
+
+ struct bfs_exprs left, right;
+ SLIST_INIT(&left);
+ SLIST_INIT(&right);
+
+ // Split
+ for (struct bfs_expr *hare = exprs->head; hare && (hare = hare->next); hare = hare->next) {
+ struct bfs_expr *tortoise = SLIST_POP(exprs);
+ SLIST_APPEND(&left, tortoise);
+ }
+ SLIST_EXTEND(&right, exprs);
+
+ // Recurse
+ sort_exprs(opt, parent, &left);
+ sort_exprs(opt, parent, &right);
+
+ // Merge
+ while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) {
+ struct bfs_expr *lhs = left.head;
+ struct bfs_expr *rhs = right.head;
+
+ float cost = expr_cost(parent, lhs, rhs);
+ float swapped = expr_cost(parent, rhs, lhs);
+
+ if (cost <= swapped) {
+ SLIST_POP(&left);
+ SLIST_APPEND(exprs, lhs);
+ } else {
+ opt_enter(opt, "%pe %pe [${ylw}%g${rs}]\n", lhs, rhs, cost);
+ SLIST_POP(&right);
+ SLIST_APPEND(exprs, rhs);
+ opt_leave(opt, "%pe %pe [${ylw}%g${rs}]\n", rhs, lhs, swapped);
+ }
+ }
+ SLIST_EXTEND(exprs, &left);
+ SLIST_EXTEND(exprs, &right);
+}
+
+/** Reorder children to reduce cost. */
+static struct bfs_expr *reorder_andor(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ // Split into blocks of consecutive pure/impure expressions, and sort
+ // the pure blocks
+ struct bfs_exprs pure;
+ SLIST_INIT(&pure);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (child->pure) {
+ SLIST_APPEND(&pure, child);
+ } else {
+ sort_exprs(opt, expr, &pure);
+ bfs_expr_extend(expr, &pure);
+ bfs_expr_append(expr, child);
+ }
+ }
+ sort_exprs(opt, expr, &pure);
+ bfs_expr_extend(expr, &pure);
+
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/**
+ * Reordering visitor.
+ */
+static const struct visitor reorder = {
+ .name = "reorder",
+ .table = (const struct visitor_table[]) {
+ {eval_and, reorder_andor},
+ {eval_or, reorder_andor},
+ {NULL, NULL},
+ },
+};
+
+/** Transfer function for simple predicates. */
+static void data_flow_pred(struct bfs_opt *opt, enum pred_type pred, bool value) {
+ constrain_pred(&opt->after_true.preds[pred], value);
+ constrain_pred(&opt->after_false.preds[pred], !value);
+}
+
+/** Transfer function for icmp-style ([+-]N) expressions. */
+static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enum range_type type) {
+ struct df_range *true_range = &opt->after_true.ranges[type];
+ struct df_range *false_range = &opt->after_false.ranges[type];
+ long long value = expr->num;
+
+ switch (expr->int_cmp) {
+ case BFS_INT_EQUAL:
+ constrain_range(true_range, value);
+ range_remove(false_range, value);
+ break;
+
+ case BFS_INT_LESS:
+ constrain_min(false_range, value);
+ constrain_max(true_range, value);
+ range_remove(true_range, value);
+ break;
+
+ case BFS_INT_GREATER:
+ constrain_max(false_range, value);
+ constrain_min(true_range, value);
+ range_remove(true_range, value);
+ break;
+ }
+}
+
+/** Transfer function for -{execut,read,writ}able. */
+static struct bfs_expr *data_flow_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ switch (expr->num) {
+ case R_OK:
+ data_flow_pred(opt, READABLE_PRED, true);
+ break;
+ case W_OK:
+ data_flow_pred(opt, WRITABLE_PRED, true);
+ break;
+ case X_OK:
+ data_flow_pred(opt, EXECUTABLE_PRED, true);
+ break;
+ default:
+ bfs_bug("Unknown access() mode %lld", expr->num);
+ break;
+ }
+
+ return expr;
+}
+
+/** Transfer function for -empty. */
+static struct bfs_expr *data_flow_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt->after_true.types &= (1 << BFS_REG) | (1 << BFS_DIR);
+
+ if (opt->before.types == (1 << BFS_REG)) {
+ constrain_range(&opt->after_true.ranges[SIZE_RANGE], 0);
+ range_remove(&opt->after_false.ranges[SIZE_RANGE], 0);
+ }
+
+ return expr;
+}
+
+/** Transfer function for -gid. */
+static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[GID_RANGE];
+ if (range->min == range->max) {
+ gid_t gid = range->min;
+ bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid);
+ if (errno == 0) {
+ constrain_pred(&opt->after_true.preds[NOGROUP_PRED], nogroup);
+ }
+ }
+
+ return expr;
+}
+
+/** Transfer function for -inum. */
+static struct bfs_expr *data_flow_inum(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[INUM_RANGE];
+ if (range->min == range->max) {
+ expr->probability = 0.01;
+ } else {
+ expr->probability = 0.5;
+ }
+
+ return expr;
+}
+
+/** Transfer function for -links. */
+static struct bfs_expr *data_flow_links(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[LINKS_RANGE];
+ if (1 >= range->min && 1 <= range->max) {
+ expr->probability = 0.99;
+ } else {
+ expr->probability = 0.5;
+ }
+
+ return expr;
+}
+
+/** Transfer function for -lname. */
+static struct bfs_expr *data_flow_lname(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt->after_true.types &= 1 << BFS_LNK;
+ return expr;
+}
+
+/** Transfer function for -samefile. */
+static struct bfs_expr *data_flow_samefile(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *true_range = &opt->after_true.ranges[INUM_RANGE];
+ constrain_range(true_range, expr->ino);
+
+ struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE];
+ range_remove(false_range, expr->ino);
+
+ return expr;
+}
+
+/** Transfer function for -size. */
+static struct bfs_expr *data_flow_size(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[SIZE_RANGE];
+ if (range->min == range->max) {
+ expr->probability = 0.01;
+ } else {
+ expr->probability = 0.5;
+ }
+
+ return expr;
+}
+
+/** Transfer function for -type. */
+static struct bfs_expr *data_flow_type(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt->after_true.types &= expr->num;
+ opt->after_false.types &= ~expr->num;
+ return expr;
+}
+
+/** Transfer function for -uid. */
+static struct bfs_expr *data_flow_uid(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[UID_RANGE];
+ if (range->min == range->max) {
+ uid_t uid = range->min;
+ bool nouser = !bfs_getpwuid(opt->ctx->users, uid);
+ if (errno == 0) {
+ constrain_pred(&opt->after_true.preds[NOUSER_PRED], nouser);
+ }
+ }
+
+ return expr;
+}
+
+/** Transfer function for -xtype. */
+static struct bfs_expr *data_flow_xtype(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt->after_true.xtypes &= expr->num;
+ opt->after_false.xtypes &= ~expr->num;
+ return expr;
+}
+
+/** Data flow visitor entry. */
+static struct bfs_expr *data_flow_enter(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ visit_enter(opt, expr, visitor);
+
+ df_dump(opt, "before", &opt->before);
+
+ if (!bfs_expr_is_parent(expr) && !expr->pure) {
+ df_join(opt->impure, &opt->before);
+ df_dump(opt, "impure", opt->impure);
+ }
+
+ return expr;
+}
+
+/** Data flow visitor exit. */
+static struct bfs_expr *data_flow_leave(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (expr->always_true) {
+ expr->probability = 1.0;
+ df_init_bottom(&opt->after_false);
+ }
+
+ if (expr->always_false) {
+ expr->probability = 0.0;
+ df_init_bottom(&opt->after_true);
+ }
+
+ df_dump(opt, "after true", &opt->after_true);
+ df_dump(opt, "after false", &opt->after_false);
+
+ if (df_is_bottom(&opt->after_false)) {
+ if (!expr->pure) {
+ expr->always_true = true;
+ expr->probability = 1.0;
+ } else if (expr->eval_fn != eval_true) {
+ opt_warning(opt, expr, "This expression is always true.\n\n");
+ opt_debug(opt, "pure, always true\n");
+ expr = opt_const(opt, true);
+ if (!expr) {
+ return NULL;
+ }
+ }
+ }
+
+ if (df_is_bottom(&opt->after_true)) {
+ if (!expr->pure) {
+ expr->always_false = true;
+ expr->probability = 0.0;
+ } else if (expr->eval_fn != eval_false) {
+ opt_warning(opt, expr, "This expression is always false.\n\n");
+ opt_debug(opt, "pure, always false\n");
+ expr = opt_const(opt, false);
+ if (!expr) {
+ return NULL;
+ }
+ }
+ }
+
+ return visit_leave(opt, expr, visitor);
+}
+
+/** Ignore an expression (and possibly warn/prompt). */
+static struct bfs_expr *opt_ignore(struct bfs_opt *opt, struct bfs_expr *expr, bool delete) {
+ if (delete) {
+ opt_delete(opt, "%pe [ignored result]\n", expr);
+ } else {
+ opt_debug(opt, "ignored result\n");
+ }
+
+ if (expr->kind != BFS_TEST) {
+ goto done;
+ }
+
+ if (!opt_warning(opt, expr, "The result of this expression is ignored.\n")) {
+ goto done;
+ }
+
+ struct bfs_ctx *ctx = opt->ctx;
+ if (ctx->interactive && ctx->dangerous) {
+ bfs_warning(ctx, "Do you want to continue? ");
+ if (ynprompt() <= 0) {
+ errno = 0;
+ return NULL;
+ }
+ }
+
+ fprintf(stderr, "\n");
+
+done:
+ if (!delete && expr->pure) {
+ // If we're not deleting the expression entirely, replace it with -false
+ expr = opt_const(opt, false);
+ }
+ return expr;
+}
+
+/** Data flow visitor function. */
+static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->ignore_result) {
+ expr = opt_ignore(opt, expr, false);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ if (df_is_bottom(&opt->before)) {
+ opt_debug(opt, "unreachable\n");
+ opt_warning(opt, expr, "This expression is unreachable.\n\n");
+ expr = opt_const(opt, false);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ /** Table of simple predicates. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ enum pred_type pred;
+ } preds[] = {
+ {eval_acl, ACL_PRED},
+ {eval_capable, CAPABLE_PRED},
+ {eval_empty, EMPTY_PRED},
+ {eval_hidden, HIDDEN_PRED},
+ {eval_nogroup, NOGROUP_PRED},
+ {eval_nouser, NOUSER_PRED},
+ {eval_sparse, SPARSE_PRED},
+ {eval_xattr, XATTR_PRED},
+ };
+
+ for (size_t i = 0; i < countof(preds); ++i) {
+ if (preds[i].eval_fn == expr->eval_fn) {
+ data_flow_pred(opt, preds[i].pred, true);
+ break;
+ }
+ }
+
+ /** Table of simple range comparisons. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ enum range_type range;
+ } ranges[] = {
+ {eval_depth, DEPTH_RANGE},
+ {eval_gid, GID_RANGE},
+ {eval_inum, INUM_RANGE},
+ {eval_links, LINKS_RANGE},
+ {eval_size, SIZE_RANGE},
+ {eval_uid, UID_RANGE},
+ };
+
+ for (size_t i = 0; i < countof(ranges); ++i) {
+ if (ranges[i].eval_fn == expr->eval_fn) {
+ data_flow_icmp(opt, expr, ranges[i].range);
+ break;
+ }
+ }
+
+ return expr;
+}
+
+/**
+ * Data flow visitor.
+ */
+static const struct visitor data_flow = {
+ .name = "data_flow",
+ .enter = data_flow_enter,
+ .visit = data_flow_visit,
+ .leave = data_flow_leave,
+ .table = (const struct visitor_table[]) {
+ {eval_access, data_flow_access},
+ {eval_empty, data_flow_empty},
+ {eval_gid, data_flow_gid},
+ {eval_inum, data_flow_inum},
+ {eval_links, data_flow_links},
+ {eval_lname, data_flow_lname},
+ {eval_samefile, data_flow_samefile},
+ {eval_size, data_flow_size},
+ {eval_type, data_flow_type},
+ {eval_uid, data_flow_uid},
+ {eval_xtype, data_flow_xtype},
+ {NULL, NULL},
+ },
+};
+
+/** Simplify a negation. */
+static struct bfs_expr *simplify_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->ignore_result) {
+ opt_debug(opt, "ignored result\n");
+ expr = only_child(expr);
+ }
+
+ return expr;
+}
+
+/** Lift negations out of a conjunction/disjunction using De Morgan's laws. */
+static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *expr) {
+ // Only lift negations if it would reduce the number of (-not) expressions
+ size_t added = 0, removed = 0;
+ for_expr (child, expr) {
+ if (child->eval_fn == eval_not) {
+ ++removed;
+ } else {
+ ++added;
+ }
+ }
+ if (added >= removed) {
+ return visit_shallow(opt, expr, &annotate);
+ }
+
+ opt_debug(opt, "De Morgan's laws\n");
+
+ if (expr->eval_fn == eval_and) {
+ expr->eval_fn = eval_or;
+ expr->argv = &fake_or_arg;
+ } else {
+ bfs_assert(expr->eval_fn == eval_or);
+ expr->eval_fn = eval_and;
+ expr->argv = &fake_and_arg;
+ }
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ opt_enter(opt, "%pe\n", child);
+
+ child = negate_expr(opt, child, &fake_not_arg);
+ if (!child) {
+ return NULL;
+ }
+
+ opt_leave(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+ }
+
+ expr = visit_shallow(opt, expr, &annotate);
+ if (!expr) {
+ return NULL;
+ }
+
+ return negate_expr(opt, expr, &fake_not_arg);
+}
+
+/** Get the first ignorable expression in a conjunction/disjunction. */
+static struct bfs_expr *first_ignorable(struct bfs_opt *opt, struct bfs_expr *expr) {
+ if (opt->level < 2 || !opt->ignore_result) {
+ return NULL;
+ }
+
+ struct bfs_expr *ret = NULL;
+ for_expr (child, expr) {
+ if (!child->pure) {
+ ret = NULL;
+ } else if (!ret) {
+ ret = child;
+ }
+ }
+
+ return ret;
+}
+
+/** Simplify a conjunction. */
+static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *ignorable = first_ignorable(opt, expr);
+ bool ignore = false;
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (child == ignorable) {
+ ignore = true;
+ }
+
+ if (ignore) {
+ if (!opt_ignore(opt, child, true)) {
+ return NULL;
+ }
+ continue;
+ }
+
+ if (child->eval_fn == eval_true) {
+ opt_delete(opt, "%pe [conjunction elimination]\n", child);
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+
+ if (child->always_false) {
+ drain_slist (struct bfs_expr, dead, &children) {
+ opt_delete(opt, "%pe [short-circuit]\n", dead);
+ }
+ }
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (!child) {
+ opt_debug(opt, "nullary identity\n");
+ return opt_const(opt, true);
+ } else if (!child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return lift_andor_not(opt, expr);
+}
+
+/** Simplify a disjunction. */
+static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *ignorable = first_ignorable(opt, expr);
+ bool ignore = false;
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (child == ignorable) {
+ ignore = true;
+ }
+
+ if (ignore) {
+ if (!opt_ignore(opt, child, true)) {
+ return NULL;
+ }
+ continue;
+ }
+
+ if (child->eval_fn == eval_false) {
+ opt_delete(opt, "%pe [disjunctive syllogism]\n", child);
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+
+ if (child->always_true) {
+ drain_slist (struct bfs_expr, dead, &children) {
+ opt_delete(opt, "%pe [short-circuit]\n", dead);
+ }
+ }
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (!child) {
+ opt_debug(opt, "nullary identity\n");
+ return opt_const(opt, false);
+ } else if (!child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return lift_andor_not(opt, expr);
+}
+
+/** Simplify a comma expression. */
+static struct bfs_expr *simplify_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ drain_slist (struct bfs_expr, child, &children) {
+ if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) {
+ if (!opt_ignore(opt, child, true)) {
+ return NULL;
+ }
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (child && !child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return expr;
+}
+
+/**
+ * Logical simplification visitor.
+ */
+static const struct visitor simplify = {
+ .name = "simplify",
+ .table = (const struct visitor_table[]) {
+ {eval_not, simplify_not},
+ {eval_and, simplify_and},
+ {eval_or, simplify_or},
+ {eval_comma, simplify_comma},
+ {NULL, NULL},
+ },
+};
+
+/** Optimize an expression. */
+static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) {
+ opt_enter(opt, "pass 0:\n");
+ expr = visit(opt, expr, &annotate);
+ opt_leave(opt, NULL);
+
+ /** Table of optimization passes. */
+ static const struct {
+ /** Minimum optlevel for this pass. */
+ int level;
+ /** The visitor for this pass. */
+ const struct visitor *visitor;
+ } passes[] = {
+ {1, &canonicalize},
+ {3, &reorder},
+ {2, &data_flow},
+ {1, &simplify},
+ };
+
+ struct df_domain impure;
+ df_init_top(&opt->after_true);
+ df_init_top(&opt->after_false);
+
+ for (int i = 0; i < 3; ++i) {
+ struct bfs_opt nested = *opt;
+ nested.impure = &impure;
+ impure = *opt->impure;
+
+ opt_enter(&nested, "pass %d:\n", i + 1);
+
+ for (size_t j = 0; j < countof(passes); ++j) {
+ if (opt->level < passes[j].level) {
+ continue;
+ }
+
+ const struct visitor *visitor = passes[j].visitor;
+
+ // Skip reordering the first time through the passes, to
+ // make warnings more understandable
+ if (visitor == &reorder) {
+ if (i == 0) {
+ continue;
+ } else {
+ nested.warn = false;
+ }
+ }
+
+ expr = visit(&nested, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (visitor == &data_flow) {
+ opt->after_true = nested.after_true;
+ opt->after_false = nested.after_false;
+ }
+ }
+
+ opt_leave(&nested, NULL);
+
+ if (!bfs_expr_is_parent(expr)) {
+ break;
+ }
+ }
+
+ *opt->impure = impure;
+ return expr;
+}
+
+/** An expression predicate. */
+typedef bool expr_pred(const struct bfs_expr *expr);
+
+/** Estimate the odds that a matching expression will be evaluated. */
+static float estimate_odds(const struct bfs_expr *expr, expr_pred *pred) {
+ if (pred(expr)) {
+ return 1.0;
+ }
+
+ float nonmatch_odds = 1.0;
+ float reached_odds = 1.0;
+ for_expr (child, expr) {
+ float child_odds = estimate_odds(child, pred);
+ nonmatch_odds *= 1.0 - reached_odds * child_odds;
+
+ if (expr->eval_fn == eval_and) {
+ reached_odds *= child->probability;
+ } else if (expr->eval_fn == eval_or) {
+ reached_odds *= 1.0 - child->probability;
+ }
+ }
+
+ return 1.0 - nonmatch_odds;
+}
+
+/** Whether an expression calls stat(). */
+static bool calls_stat(const struct bfs_expr *expr) {
+ return expr->calls_stat;
+}
+
+/** Estimate the odds of calling stat(). */
+static float estimate_stat_odds(struct bfs_ctx *ctx) {
+ if (ctx->unique) {
+ return 1.0;
+ }
+
+ float nostat_odds = 1.0 - estimate_odds(ctx->exclude, calls_stat);
+
+ float reached_odds = 1.0 - ctx->exclude->probability;
+ float expr_odds = estimate_odds(ctx->expr, calls_stat);
+ nostat_odds *= 1.0 - reached_odds * expr_odds;
+
+ return 1.0 - nostat_odds;
+}
+
+/** Matches -(exec|ok) ... \; */
+static bool single_exec(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_exec && !(expr->exec->flags & BFS_EXEC_MULTI);
+}
+
+int bfs_optimize(struct bfs_ctx *ctx) {
+ bfs_ctx_dump(ctx, DEBUG_OPT);
+
+ struct df_domain impure;
+ df_init_bottom(&impure);
+
+ struct bfs_opt opt = {
+ .ctx = ctx,
+ .level = ctx->optlevel,
+ .depth = 0,
+ .warn = ctx->warn,
+ .ignore_result = false,
+ .impure = &impure,
+ };
+ df_init_top(&opt.before);
+
+ ctx->exclude = optimize(&opt, ctx->exclude);
+ if (!ctx->exclude) {
+ return -1;
+ }
+
+ // Only non-excluded files are evaluated
+ opt.before = opt.after_false;
+ opt.ignore_result = true;
+
+ struct df_range *depth = &opt.before.ranges[DEPTH_RANGE];
+ if (ctx->mindepth > 0) {
+ constrain_min(depth, ctx->mindepth);
+ }
+ if (ctx->maxdepth < INT_MAX) {
+ constrain_max(depth, ctx->maxdepth);
+ }
+
+ ctx->expr = optimize(&opt, ctx->expr);
+ if (!ctx->expr) {
+ return -1;
+ }
+
+ if (opt.level >= 2 && df_is_bottom(&impure)) {
+ bfs_warning(ctx, "This command won't do anything.\n\n");
+ }
+
+ const struct df_range *impure_depth = &impure.ranges[DEPTH_RANGE];
+ long long mindepth = impure_depth->min;
+ long long maxdepth = impure_depth->max;
+
+ opt_enter(&opt, "post-process:\n");
+
+ if (opt.level >= 2 && mindepth > ctx->mindepth) {
+ if (mindepth > INT_MAX) {
+ mindepth = INT_MAX;
+ }
+ opt_enter(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth);
+ ctx->mindepth = mindepth;
+ opt_leave(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth);
+ }
+
+ if (opt.level >= 4 && maxdepth < ctx->maxdepth) {
+ if (maxdepth < INT_MIN) {
+ maxdepth = INT_MIN;
+ }
+ opt_enter(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth);
+ ctx->maxdepth = maxdepth;
+ opt_leave(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth);
+ }
+
+ if (opt.level >= 3) {
+ // bfs_eval() can do lazy stat() calls, but only on one thread.
+ float lazy_cost = estimate_stat_odds(ctx);
+ // bftw() can do eager stat() calls in parallel
+ float eager_cost = 1.0 / ctx->threads;
+
+ if (eager_cost <= lazy_cost) {
+ opt_enter(&opt, "lazy stat cost: ${ylw}%g${rs}\n", lazy_cost);
+ ctx->flags |= BFTW_STAT;
+ opt_leave(&opt, "eager stat cost: ${ylw}%g${rs}\n", eager_cost);
+ }
+
+#ifndef POSIX_SPAWN_SETRLIMIT
+ // If bfs_spawn_setrlimit() would force us to use fork() over
+ // posix_spawn(), the extra cost may outweigh the benefit of a
+ // higher RLIMIT_NOFILE
+ float single_exec_odds = estimate_odds(ctx->expr, single_exec);
+ if (single_exec_odds >= 0.5) {
+ opt_enter(&opt, "single ${blu}-exec${rs} odds: ${ylw}%g${rs}\n", single_exec_odds);
+ ctx->raise_nofile = false;
+ opt_leave(&opt, "not raising RLIMIT_NOFILE\n");
+ }
+#endif
+ }
+
+ opt_leave(&opt, NULL);
+
+ return 0;
+}
diff --git a/src/opt.h b/src/opt.h
new file mode 100644
index 0000000..a5729b3
--- /dev/null
+++ b/src/opt.h
@@ -0,0 +1,23 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Optimization.
+ */
+
+#ifndef BFS_OPT_H
+#define BFS_OPT_H
+
+struct bfs_ctx;
+
+/**
+ * Apply optimizations to the command line.
+ *
+ * @ctx
+ * The bfs context to optimize.
+ * @return
+ * 0 if successful, -1 on error.
+ */
+int bfs_optimize(struct bfs_ctx *ctx);
+
+#endif // BFS_OPT_H
diff --git a/src/parse.c b/src/parse.c
new file mode 100644
index 0000000..5ec4c0e
--- /dev/null
+++ b/src/parse.c
@@ -0,0 +1,3978 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * The command line parser. Expressions are parsed by recursive descent, with a
+ * grammar described in the comments of the parse_*() functions. The parser
+ * also accepts flags and paths at any point in the expression, by treating
+ * flags like always-true options, and skipping over paths wherever they appear.
+ */
+
+#include "parse.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "bftw.h"
+#include "color.h"
+#include "ctx.h"
+#include "diag.h"
+#include "dir.h"
+#include "eval.h"
+#include "exec.h"
+#include "expr.h"
+#include "fsade.h"
+#include "list.h"
+#include "opt.h"
+#include "printf.h"
+#include "pwcache.h"
+#include "sanity.h"
+#include "stat.h"
+#include "typo.h"
+#include "xregex.h"
+#include "xspawn.h"
+#include "xtime.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <grp.h>
+#include <limits.h>
+#include <pwd.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+// Strings printed by -D tree for "fake" expressions
+static char *fake_and_arg = "-and";
+static char *fake_hidden_arg = "-hidden";
+static char *fake_or_arg = "-or";
+static char *fake_print_arg = "-print";
+static char *fake_true_arg = "-true";
+
+/**
+ * Color use flags.
+ */
+enum use_color {
+ COLOR_NEVER,
+ COLOR_AUTO,
+ COLOR_ALWAYS,
+};
+
+/**
+ * Command line parser state.
+ */
+struct bfs_parser {
+ /** The command line being constructed. */
+ struct bfs_ctx *ctx;
+ /** The command line arguments being parsed. */
+ char **argv;
+ /** The name of this program. */
+ const char *command;
+
+ /** The current regex flags to use. */
+ enum bfs_regex_type regex_type;
+
+ /** Whether stdout is a terminal. */
+ bool stdout_tty;
+ /** Whether -color or -nocolor has been passed. */
+ enum use_color use_color;
+ /** Whether a -print action is implied. */
+ bool implicit_print;
+ /** Whether the expression has started. */
+ bool expr_started;
+ /** Whether an information option like -help or -version was passed. */
+ bool just_info;
+ /** Whether we are currently parsing an -exclude expression. */
+ bool excluding;
+
+ /** The last non-path argument. */
+ char **last_arg;
+ /** A "-depth"-type expression, if any. */
+ const struct bfs_expr *depth_expr;
+ /** A "-limit" expression, if any. */
+ const struct bfs_expr *limit_expr;
+ /** A "-prune" expression, if any. */
+ const struct bfs_expr *prune_expr;
+ /** A "-mount" expression, if any. */
+ const struct bfs_expr *mount_expr;
+ /** An "-xdev" expression, if any. */
+ const struct bfs_expr *xdev_expr;
+ /** A "-files0-from" expression, if any. */
+ const struct bfs_expr *files0_expr;
+ /** An expression that consumes stdin, if any. */
+ const struct bfs_expr *stdin_expr;
+
+ /** The current time (maybe modified by -daystart). */
+ struct timespec now;
+};
+
+/**
+ * Print a low-level error message during parsing.
+ */
+static void parse_perror(const struct bfs_parser *parser, const char *str) {
+ bfs_perror(parser->ctx, str);
+}
+
+/** Initialize an empty highlighted range. */
+static void init_highlight(const struct bfs_ctx *ctx, bool *args) {
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ args[i] = false;
+ }
+}
+
+/** Highlight a range of command line arguments. */
+static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, bool *args) {
+ size_t i = argv - ctx->argv;
+ for (size_t j = 0; j < argc; ++j) {
+ bfs_assert(i + j < ctx->argc);
+ args[i + j] = true;
+ }
+}
+
+/**
+ * Print an error message during parsing.
+ */
+_printf(2, 3)
+static void parse_error(const struct bfs_parser *parser, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, parser->argv, 1, highlight);
+ bfs_argv_error(ctx, highlight);
+
+ va_list args;
+ va_start(args, format);
+ bfs_verror(parser->ctx, format, args);
+ va_end(args);
+}
+
+/**
+ * Print an error about some command line arguments.
+ */
+_printf(4, 5)
+static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, argv, argc, highlight);
+ bfs_argv_error(ctx, highlight);
+
+ va_list args;
+ va_start(args, format);
+ bfs_verror(ctx, format, args);
+ va_end(args);
+}
+
+/**
+ * Print an error about conflicting command line arguments.
+ */
+_printf(4, 5)
+static void parse_conflict_error(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, expr1->argv, expr1->argc, highlight);
+ highlight_args(ctx, expr2->argv, expr2->argc, highlight);
+ bfs_argv_error(ctx, highlight);
+
+ va_list args;
+ va_start(args, format);
+ bfs_verror(ctx, format, args);
+ va_end(args);
+}
+
+/**
+ * Print an error about an expression.
+ */
+_printf(3, 4)
+static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bfs_expr_error(ctx, expr);
+
+ va_list args;
+ va_start(args, format);
+ bfs_verror(ctx, format, args);
+ va_end(args);
+}
+
+/**
+ * Print a warning message during parsing.
+ */
+_printf(2, 3)
+static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, parser->argv, 1, highlight);
+ if (!bfs_argv_warning(ctx, highlight)) {
+ return false;
+ }
+
+ va_list args;
+ va_start(args, format);
+ bool ret = bfs_vwarning(parser->ctx, format, args);
+ va_end(args);
+ return ret;
+}
+
+/**
+ * Print a warning about conflicting command line arguments.
+ */
+_printf(4, 5)
+static bool parse_conflict_warning(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, expr1->argv, expr1->argc, highlight);
+ highlight_args(ctx, expr2->argv, expr2->argc, highlight);
+ if (!bfs_argv_warning(ctx, highlight)) {
+ return false;
+ }
+
+ va_list args;
+ va_start(args, format);
+ bool ret = bfs_vwarning(ctx, format, args);
+ va_end(args);
+ return ret;
+}
+
+/**
+ * Print a warning about an expression.
+ */
+_printf(3, 4)
+static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ if (!bfs_expr_warning(ctx, expr)) {
+ return false;
+ }
+
+ va_list args;
+ va_start(args, format);
+ bool ret = bfs_vwarning(ctx, format, args);
+ va_end(args);
+ return ret;
+}
+
+/**
+ * Report an error if stdin is already consumed, then consume it.
+ */
+static bool consume_stdin(struct bfs_parser *parser, const struct bfs_expr *expr) {
+ if (parser->stdin_expr) {
+ parse_conflict_error(parser, parser->stdin_expr, expr,
+ "%pX and %pX can't both use standard input.\n",
+ parser->stdin_expr, expr);
+ return false;
+ }
+
+ parser->stdin_expr = expr;
+ return true;
+}
+
+/**
+ * Allocate a new expression.
+ */
+static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) {
+ struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv, kind);
+ if (!expr) {
+ parse_perror(parser, "bfs_expr_new()");
+ }
+ return expr;
+}
+
+/**
+ * Create a new unary expression.
+ */
+static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) {
+ struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR);
+ if (!expr) {
+ return NULL;
+ }
+
+ bfs_assert(bfs_expr_is_parent(expr));
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/**
+ * Create a new binary expression.
+ */
+static struct bfs_expr *new_binary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) {
+ struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv, BFS_OPERATOR);
+ if (!expr) {
+ return NULL;
+ }
+
+ bfs_assert(bfs_expr_is_parent(expr));
+ bfs_expr_append(expr, lhs);
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/**
+ * Fill in a "-print"-type expression.
+ */
+static void init_print_expr(struct bfs_parser *parser, struct bfs_expr *expr) {
+ expr->cfile = parser->ctx->cout;
+ expr->path = NULL;
+}
+
+/**
+ * Open a file for an expression.
+ */
+static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const char *path) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ FILE *file = NULL;
+ CFILE *cfile = NULL;
+
+ file = xfopen(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC);
+ if (!file) {
+ goto fail;
+ }
+
+ cfile = cfwrap(file, parser->use_color ? ctx->colors : NULL, true);
+ if (!cfile) {
+ goto fail;
+ }
+
+ CFILE *dedup = bfs_ctx_dedup(ctx, cfile, path);
+ if (!dedup) {
+ goto fail;
+ }
+
+ if (dedup != cfile) {
+ cfclose(cfile);
+ }
+
+ expr->cfile = dedup;
+ expr->path = path;
+ return 0;
+
+fail:
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ if (cfile) {
+ cfclose(cfile);
+ } else if (file) {
+ fclose(file);
+ }
+ return -1;
+}
+
+/**
+ * Invoke bfs_stat() on an argument.
+ */
+static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat *sb) {
+ const struct bfs_ctx *ctx = parser->ctx;
+
+ bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
+ enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW;
+
+ int ret = bfs_stat(AT_FDCWD, *arg, flags, sb);
+ if (ret != 0) {
+ parse_argv_error(parser, arg, 1, "%s.\n", errstr());
+ }
+ return ret;
+}
+
+/**
+ * Parse the expression specified on the command line.
+ */
+static struct bfs_expr *parse_expr(struct bfs_parser *parser);
+
+/**
+ * Advance by a single token.
+ */
+static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size_t argc) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ if (kind != BFS_FLAG && kind != BFS_PATH) {
+ parser->expr_started = true;
+ }
+
+ if (kind != BFS_PATH) {
+ parser->last_arg = parser->argv;
+ }
+
+ size_t i = parser->argv - ctx->argv;
+ ctx->kinds[i] = kind;
+
+ char **argv = parser->argv;
+ parser->argv += argc;
+ return argv;
+}
+
+/**
+ * Parse a root path.
+ */
+static int parse_root(struct bfs_parser *parser, const char *path) {
+ struct bfs_ctx *ctx = parser->ctx;
+ const char **root = RESERVE(const char *, &ctx->paths, &ctx->npaths);
+ if (!root) {
+ parse_perror(parser, "RESERVE()");
+ return -1;
+ }
+
+ *root = strdup(path);
+ if (!*root) {
+ --ctx->npaths;
+ parse_perror(parser, "strdup()");
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * While parsing an expression, skip any paths and add them to ctx->paths.
+ */
+static int skip_paths(struct bfs_parser *parser) {
+ while (true) {
+ const char *arg = parser->argv[0];
+ if (!arg) {
+ return 0;
+ }
+
+ if (arg[0] == '-') {
+ if (strcmp(arg, "--") == 0) {
+ // find uses -- to separate flags from the rest
+ // of the command line. We allow mixing flags
+ // and paths/predicates, so we just ignore --.
+ parser_advance(parser, BFS_FLAG, 1);
+ continue;
+ }
+ if (strcmp(arg, "-") != 0) {
+ // - by itself is a file name. Anything else
+ // starting with - is a flag/predicate.
+ return 0;
+ }
+ }
+
+ // By POSIX, these are always options
+ if (strcmp(arg, "(") == 0 || strcmp(arg, "!") == 0) {
+ return 0;
+ }
+
+ if (parser->expr_started) {
+ // By POSIX, these can be paths. We only treat them as
+ // such at the beginning of the command line.
+ if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) {
+ return 0;
+ }
+ }
+
+ if (parser->excluding) {
+ parse_warning(parser, "This path will not be excluded. Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n");
+ bfs_warning(parser->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n");
+ }
+
+ if (parse_root(parser, arg) != 0) {
+ return -1;
+ }
+
+ parser_advance(parser, BFS_PATH, 1);
+ }
+}
+
+/** Integer parsing flags. */
+enum int_flags {
+ IF_BASE_MASK = 0x03F,
+ IF_INT = 0x040,
+ IF_LONG = 0x080,
+ IF_LONG_LONG = 0x0C0,
+ IF_SIZE_MASK = 0x0C0,
+ IF_UNSIGNED = 0x100,
+ IF_PARTIAL_OK = 0x200,
+ IF_QUIET = 0x400,
+};
+
+/**
+ * Parse an integer.
+ */
+static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) {
+ int base = flags & IF_BASE_MASK;
+ if (base == 0) {
+ base = 10;
+ }
+
+ char *endptr;
+ long long value;
+ if (xstrtoll(str, &endptr, base, &value) != 0) {
+ if (errno == ERANGE) {
+ goto range;
+ } else {
+ goto bad;
+ }
+ }
+
+ if (!(flags & IF_PARTIAL_OK) && *endptr != '\0') {
+ goto bad;
+ }
+
+ if ((flags & IF_UNSIGNED) && value < 0) {
+ goto negative;
+ }
+
+ switch (flags & IF_SIZE_MASK) {
+ case IF_INT:
+ if (value < INT_MIN || value > INT_MAX) {
+ goto range;
+ }
+ *(int *)result = value;
+ break;
+
+ case IF_LONG:
+ if (value < LONG_MIN || value > LONG_MAX) {
+ goto range;
+ }
+ *(long *)result = value;
+ break;
+
+ case IF_LONG_LONG:
+ *(long long *)result = value;
+ break;
+
+ default:
+ bfs_bug("Invalid int size");
+ goto bad;
+ }
+
+ return endptr;
+
+bad:
+ if (!(flags & IF_QUIET)) {
+ parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is not a valid integer.\n", str);
+ }
+ return NULL;
+
+negative:
+ if (!(flags & IF_QUIET)) {
+ parse_argv_error(parser, arg, 1, "Negative integer ${bld}%pq${rs} is not allowed here.\n", str);
+ }
+ return NULL;
+
+range:
+ if (!(flags & IF_QUIET)) {
+ parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is too large an integer.\n", str);
+ }
+ return NULL;
+}
+
+/**
+ * Parse an integer and a comparison flag.
+ */
+static const char *parse_icmp(const struct bfs_parser *parser, struct bfs_expr *expr, enum int_flags flags) {
+ char **arg = &expr->argv[1];
+ const char *str = *arg;
+ switch (str[0]) {
+ case '-':
+ expr->int_cmp = BFS_INT_LESS;
+ ++str;
+ break;
+ case '+':
+ expr->int_cmp = BFS_INT_GREATER;
+ ++str;
+ break;
+ default:
+ expr->int_cmp = BFS_INT_EQUAL;
+ break;
+ }
+
+ return parse_int(parser, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED);
+}
+
+/**
+ * Check if a string could be an integer comparison.
+ */
+static bool looks_like_icmp(const char *str) {
+ int i;
+
+ // One +/- for the comparison flag, one for the sign
+ for (i = 0; i < 2; ++i) {
+ if (str[i] != '-' && str[i] != '+') {
+ break;
+ }
+ }
+
+ return str[i] >= '0' && str[i] <= '9';
+}
+
+/**
+ * Parse a single flag.
+ */
+static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) {
+ char **argv = parser_advance(parser, BFS_FLAG, argc);
+ return parse_new_expr(parser, eval_true, argc, argv, BFS_FLAG);
+}
+
+/**
+ * Parse a flag that doesn't take a value.
+ */
+static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) {
+ return parse_flag(parser, 1);
+}
+
+/**
+ * Parse a flag that takes a value.
+ */
+static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) {
+ const char *arg = parser->argv[0];
+ char flag = arg[strlen(arg) - 1];
+
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
+ return NULL;
+ }
+
+ return parse_flag(parser, 2);
+}
+
+/**
+ * Parse a prefix flag like -O3, -j8, etc.
+ */
+static struct bfs_expr *parse_prefix_flag(struct bfs_parser *parser, char flag, bool allow_separate, const char **value) {
+ const char *arg = parser->argv[0];
+
+ const char *suffix = strchr(arg, flag) + 1;
+ if (*suffix) {
+ *value = suffix;
+ return parse_nullary_flag(parser);
+ }
+
+ suffix = parser->argv[1];
+ if (allow_separate && suffix) {
+ *value = suffix;
+ } else {
+ parse_error(parser, "${cyn}-%c${rs} needs a value.\n", flag);
+ return NULL;
+ }
+
+ return parse_unary_flag(parser);
+}
+
+/**
+ * Parse a single option.
+ */
+static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) {
+ char **argv = parser_advance(parser, BFS_OPTION, argc);
+ return parse_new_expr(parser, eval_true, argc, argv, BFS_OPTION);
+}
+
+/**
+ * Parse an option that doesn't take a value.
+ */
+static struct bfs_expr *parse_nullary_option(struct bfs_parser *parser) {
+ return parse_option(parser, 1);
+}
+
+/**
+ * Parse an option that takes a value.
+ */
+static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_option(parser, 2);
+}
+
+/**
+ * Parse a single test.
+ */
+static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
+ char **argv = parser_advance(parser, BFS_TEST, argc);
+ return parse_new_expr(parser, eval_fn, argc, argv, BFS_TEST);
+}
+
+/**
+ * Parse a test that doesn't take a value.
+ */
+static struct bfs_expr *parse_nullary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ return parse_test(parser, eval_fn, 1);
+}
+
+/**
+ * Parse a test that takes a value.
+ */
+static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_test(parser, eval_fn, 2);
+}
+
+/**
+ * Parse a single action.
+ */
+static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
+ char **argv = parser_advance(parser, BFS_ACTION, argc);
+
+ if (parser->excluding) {
+ parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n");
+ return NULL;
+ }
+
+ if (eval_fn != eval_limit && eval_fn != eval_prune && eval_fn != eval_quit) {
+ parser->implicit_print = false;
+ }
+
+ return parse_new_expr(parser, eval_fn, argc, argv, BFS_ACTION);
+}
+
+/**
+ * Parse an action that takes no arguments.
+ */
+static struct bfs_expr *parse_nullary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ return parse_action(parser, eval_fn, 1);
+}
+
+/**
+ * Parse an action that takes one argument.
+ */
+static struct bfs_expr *parse_unary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_action(parser, eval_fn, 2);
+}
+
+/**
+ * Parse a test expression with integer data and a comparison flag.
+ */
+static struct bfs_expr *parse_test_icmp(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_fn);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (!parse_icmp(parser, expr, 0)) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Print usage information for -D.
+ */
+static void debug_help(CFILE *cfile) {
+ cfprintf(cfile, "Supported debug flags:\n\n");
+
+ cfprintf(cfile, " ${bld}help${rs}: This message.\n");
+ cfprintf(cfile, " ${bld}cost${rs}: Show cost estimates.\n");
+ cfprintf(cfile, " ${bld}exec${rs}: Print executed command details.\n");
+ cfprintf(cfile, " ${bld}opt${rs}: Print optimization details.\n");
+ cfprintf(cfile, " ${bld}rates${rs}: Print predicate success rates.\n");
+ cfprintf(cfile, " ${bld}search${rs}: Trace the filesystem traversal.\n");
+ cfprintf(cfile, " ${bld}stat${rs}: Trace all stat() calls.\n");
+ cfprintf(cfile, " ${bld}tree${rs}: Print the parse tree.\n");
+ cfprintf(cfile, " ${bld}all${rs}: All debug flags at once.\n");
+}
+
+/** Check if a substring matches a debug flag. */
+static bool parse_debug_flag(const char *flag, size_t len, const char *expected) {
+ if (len == strlen(expected)) {
+ return strncmp(flag, expected, len) == 0;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Parse -D FLAG.
+ */
+static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ const char *flags;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'D', true, &flags);
+ if (!expr) {
+ cfprintf(ctx->cerr, "\n");
+ debug_help(ctx->cerr);
+ return NULL;
+ }
+
+ bool unrecognized = false;
+
+ for (const char *flag = flags, *next; flag; flag = next) {
+ size_t len = strcspn(flag, ",");
+ if (flag[len]) {
+ next = flag + len + 1;
+ } else {
+ next = NULL;
+ }
+
+ if (parse_debug_flag(flag, len, "help")) {
+ debug_help(ctx->cout);
+ parser->just_info = true;
+ return NULL;
+ } else if (parse_debug_flag(flag, len, "all")) {
+ ctx->debug = DEBUG_ALL;
+ continue;
+ }
+
+ enum debug_flags i;
+ for (i = 1; DEBUG_ALL & i; i <<= 1) {
+ const char *name = debug_flag_name(i);
+ if (parse_debug_flag(flag, len, name)) {
+ break;
+ }
+ }
+
+ if (DEBUG_ALL & i) {
+ ctx->debug |= i;
+ } else {
+ if (parse_expr_warning(parser, expr, "Unrecognized debug flag ${bld}")) {
+ fwrite(flag, 1, len, stderr);
+ cfprintf(ctx->cerr, "${rs}.\n\n");
+ unrecognized = true;
+ }
+ }
+ }
+
+ if (unrecognized) {
+ debug_help(ctx->cerr);
+ cfprintf(ctx->cerr, "\n");
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -On.
+ */
+static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'O', false, &arg);
+ if (!expr) {
+ return NULL;
+ }
+
+ int *optlevel = &parser->ctx->optlevel;
+
+ if (strcmp(arg, "fast") == 0) {
+ *optlevel = 4;
+ } 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", arg);
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -[PHL], -follow.
+ */
+static struct bfs_expr *parse_follow(struct bfs_parser *parser, int flags, int option) {
+ struct bfs_ctx *ctx = parser->ctx;
+ ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
+ ctx->flags |= flags;
+ if (option) {
+ return parse_nullary_option(parser);
+ } else {
+ return parse_nullary_flag(parser);
+ }
+}
+
+/**
+ * Parse -X.
+ */
+static struct bfs_expr *parse_xargs_safe(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->xargs_safe = true;
+ return parse_nullary_flag(parser);
+}
+
+/**
+ * Parse -executable, -readable, -writable
+ */
+static struct bfs_expr *parse_access(struct bfs_parser *parser, int flag, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_access);
+ if (expr) {
+ expr->num = flag;
+ }
+ return expr;
+}
+
+/**
+ * Parse -acl.
+ */
+static struct bfs_expr *parse_acl(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_ACL
+ return parse_nullary_test(parser, eval_acl);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -[aBcm]?newer.
+ */
+static struct bfs_expr *parse_newer(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
+ return NULL;
+ }
+
+ expr->reftime = sb.mtime;
+ expr->stat_field = field;
+ return expr;
+}
+
+/**
+ * Parse -[aBcm]min.
+ */
+static struct bfs_expr *parse_min(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_test_icmp(parser, eval_time);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->reftime = parser->now;
+ expr->stat_field = field;
+ expr->time_unit = BFS_MINUTES;
+ return expr;
+}
+
+/**
+ * Parse -[aBcm]time.
+ */
+static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_time);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->reftime = parser->now;
+ expr->stat_field = field;
+
+ const char *tail = parse_icmp(parser, expr, IF_PARTIAL_OK);
+ if (!tail) {
+ return NULL;
+ }
+
+ if (!*tail) {
+ expr->time_unit = BFS_DAYS;
+ return expr;
+ }
+
+ unsigned long long time = expr->num;
+ expr->num = 0;
+
+ while (true) {
+ switch (*tail) {
+ case 'w':
+ time *= 7;
+ _fallthrough;
+ case 'd':
+ time *= 24;
+ _fallthrough;
+ case 'h':
+ time *= 60;
+ _fallthrough;
+ case 'm':
+ time *= 60;
+ _fallthrough;
+ case 's':
+ break;
+ default:
+ parse_expr_error(parser, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail);
+ return NULL;
+ }
+
+ expr->num += time;
+
+ if (!*++tail) {
+ break;
+ }
+
+ tail = parse_int(parser, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED);
+ if (!tail) {
+ return NULL;
+ }
+ if (!*tail) {
+ parse_expr_error(parser, expr, "Missing time unit.\n");
+ return NULL;
+ }
+ }
+
+ expr->time_unit = BFS_SECONDS;
+ return expr;
+}
+
+/**
+ * Parse -capable.
+ */
+static struct bfs_expr *parse_capable(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_CAPABILITIES
+ return parse_nullary_test(parser, eval_capable);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -(no)?color.
+ */
+static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_ctx *ctx = parser->ctx;
+ struct colors *colors = ctx->colors;
+
+ if (color) {
+ if (!colors) {
+ parse_expr_error(parser, expr, "Error parsing $$LS_COLORS: %s.\n", xstrerror(ctx->colors_error));
+ return NULL;
+ }
+
+ parser->use_color = COLOR_ALWAYS;
+ ctx->cout->colors = colors;
+ ctx->cerr->colors = colors;
+ } else {
+ parser->use_color = COLOR_NEVER;
+ ctx->cout->colors = NULL;
+ ctx->cerr->colors = NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Common code for fnmatch() tests.
+ */
+static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->pattern = expr->argv[1];
+
+ if (casefold) {
+#ifdef FNM_CASEFOLD
+ expr->fnm_flags = FNM_CASEFOLD;
+#else
+ parse_expr_error(parser, expr, "Missing platform support.\n");
+ return NULL;
+#endif
+ } else {
+ expr->fnm_flags = 0;
+ }
+
+ // POSIX says, about fnmatch():
+ //
+ // If pattern ends with an unescaped <backslash>, fnmatch() shall
+ // return a non-zero value (indicating either no match or an error).
+ //
+ // But not all implementations obey this, so check for it ourselves.
+ size_t i, len = strlen(expr->pattern);
+ for (i = 0; i < len; ++i) {
+ if (expr->pattern[len - i - 1] != '\\') {
+ break;
+ }
+ }
+ if (i % 2 != 0) {
+ parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
+ expr->eval_fn = eval_false;
+ return expr;
+ }
+
+ // strcmp() can be much faster than fnmatch() since it doesn't have to
+ // parse the pattern, so special-case patterns with no wildcards.
+ //
+ // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_14_01
+ expr->literal = strcspn(expr->pattern, "?*\\[") == len;
+
+ return expr;
+}
+
+/**
+ * Parse -context.
+ */
+static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_CONTEXT
+ struct bfs_expr *expr = parse_unary_test(parser, eval_context);
+ return parse_fnmatch(parser, expr, false);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -{false,true}.
+ */
+static struct bfs_expr *parse_const(struct bfs_parser *parser, int value, int arg2) {
+ return parse_nullary_test(parser, value ? eval_true : eval_false);
+}
+
+/**
+ * Parse -daystart.
+ */
+static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int arg2) {
+ struct tm tm;
+ if (!localtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "localtime_r()");
+ return NULL;
+ }
+
+ if (tm.tm_hour || tm.tm_min || tm.tm_sec || parser->now.tv_nsec) {
+ ++tm.tm_mday;
+ }
+ tm.tm_hour = 0;
+ tm.tm_min = 0;
+ tm.tm_sec = 0;
+
+ time_t time;
+ if (xmktime(&tm, &time) != 0) {
+ parse_perror(parser, "xmktime()");
+ return NULL;
+ }
+
+ parser->now.tv_sec = time;
+ parser->now.tv_nsec = 0;
+
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -delete.
+ */
+static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_delete);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_ctx *ctx = parser->ctx;
+ ctx->flags |= BFTW_POST_ORDER;
+ ctx->dangerous = true;
+
+ parser->depth_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -d.
+ */
+static struct bfs_expr *parse_depth(struct bfs_parser *parser, int flag, int arg2) {
+ struct bfs_expr *expr = flag
+ ? parse_nullary_flag(parser)
+ : parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ parser->ctx->flags |= BFTW_POST_ORDER;
+ parser->depth_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -depth [N].
+ */
+static struct bfs_expr *parse_depth_n(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[1];
+ if (arg && looks_like_icmp(arg)) {
+ return parse_test_icmp(parser, eval_depth);
+ } else {
+ return parse_depth(parser, arg1, arg2);
+ }
+}
+
+/**
+ * Parse -{min,max}depth N.
+ */
+static struct bfs_expr *parse_depth_limit(struct bfs_parser *parser, int is_min, int arg2) {
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_ctx *ctx = parser->ctx;
+ int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth;
+ char **arg = &expr->argv[1];
+ if (!parse_int(parser, arg, *arg, depth, IF_INT | IF_UNSIGNED)) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -empty.
+ */
+static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_empty);
+ if (expr) {
+ // For opendir()
+ expr->ephemeral_fds = 1;
+ }
+ return expr;
+}
+
+/** Check for unsafe relative paths in $PATH. */
+static const char *unsafe_path(const struct bfs_exec *execbuf) {
+ if (!(execbuf->flags & BFS_EXEC_CHDIR)) {
+ // Not -execdir or -okdir
+ return NULL;
+ }
+
+ const char *exe = execbuf->tmpl_argv[0];
+ if (strchr(exe, '/')) {
+ // No $PATH lookups for /foo or foo/bar
+ return NULL;
+ }
+
+ if (strstr(exe, "{}")) {
+ // Substituted paths always contain a /
+ return NULL;
+ }
+
+ const char *path = getenv("PATH");
+ while (path) {
+ if (path[0] != '/') {
+ // Relative $PATH component!
+ return path;
+ }
+
+ path = strchr(path, ':');
+ if (path) {
+ ++path;
+ }
+ }
+
+ // No relative components in $PATH
+ return NULL;
+}
+
+/**
+ * Parse -exec(dir)?/-ok(dir)?.
+ */
+static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ struct bfs_exec *execbuf = bfs_exec_parse(ctx, parser->argv, flags);
+ if (!execbuf) {
+ return NULL;
+ }
+
+ struct bfs_expr *expr = parse_action(parser, eval_exec, execbuf->tmpl_argc + 2);
+ if (!expr) {
+ bfs_exec_free(execbuf);
+ return NULL;
+ }
+
+ expr->exec = execbuf;
+
+ // For pipe() in bfs_spawn()
+ expr->ephemeral_fds = 2;
+
+ const char *unsafe = unsafe_path(execbuf);
+ if (unsafe) {
+ size_t len = strcspn(unsafe, ":");
+ char *comp = strndup(unsafe, len);
+ if (comp) {
+ parse_expr_error(parser, expr,
+ "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp);
+ free(comp);
+ } else {
+ parse_perror(parser, "strndup()");
+ }
+ return NULL;
+ }
+
+ if (execbuf->flags & BFS_EXEC_CHDIR) {
+ // To dup() the parent directory
+ if (execbuf->flags & BFS_EXEC_MULTI) {
+ ++expr->persistent_fds;
+ } else {
+ ++expr->ephemeral_fds;
+ }
+ }
+
+ if (execbuf->flags & BFS_EXEC_CONFIRM) {
+ if (!consume_stdin(parser, expr)) {
+ return NULL;
+ }
+ } else {
+ ctx->dangerous = true;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -exit [STATUS].
+ */
+static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2) {
+ size_t argc = 1;
+ const char *value = parser->argv[1];
+
+ int status = EXIT_SUCCESS;
+ if (value && parse_int(parser, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ argc = 2;
+ }
+
+ struct bfs_expr *expr = parse_action(parser, eval_exit, argc);
+ if (expr) {
+ expr->num = status;
+ }
+ return expr;
+}
+
+/**
+ * Parse -f PATH.
+ */
+static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ struct bfs_expr *expr = parse_unary_flag(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ // Mark the path as a path, not a regular argument
+ size_t i = expr->argv - ctx->argv;
+ ctx->kinds[i + 1] = BFS_PATH;
+
+ if (parse_root(parser, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -files0-from PATH.
+ */
+static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ // For compatibility with GNU find,
+ //
+ // bfs -files0-from a -files0-from b
+ //
+ // should *only* use b, not a. So stash the expression here and only
+ // process the last one at the end of parsing.
+ parser->files0_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -flags FLAGS.
+ */
+static struct bfs_expr *parse_flags(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_flags);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *flags = expr->argv[1];
+ switch (flags[0]) {
+ case '-':
+ expr->flags_cmp = BFS_MODE_ALL;
+ ++flags;
+ break;
+ case '+':
+ expr->flags_cmp = BFS_MODE_ANY;
+ ++flags;
+ break;
+ default:
+ expr->flags_cmp = BFS_MODE_EQUAL;
+ break;
+ }
+
+ if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) {
+ if (errno == ENOTSUP) {
+ parse_expr_error(parser, expr, "Missing platform support.\n");
+ } else {
+ parse_expr_error(parser, expr, "Invalid flags.\n");
+ }
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -fls FILE.
+ */
+static struct bfs_expr *parse_fls(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fls);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -fprint FILE.
+ */
+static struct bfs_expr *parse_fprint(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprint);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -fprint0 FILE.
+ */
+static struct bfs_expr *parse_fprint0(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprint0);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -fprintf FILE FORMAT.
+ */
+static struct bfs_expr *parse_fprintf(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[0];
+
+ const char *file = parser->argv[1];
+ if (!file) {
+ parse_error(parser, "${blu}%s${rs} needs a file.\n", arg);
+ return NULL;
+ }
+
+ const char *format = parser->argv[2];
+ if (!format) {
+ parse_error(parser, "${blu}%s${rs} needs a format string.\n", arg);
+ return NULL;
+ }
+
+ struct bfs_expr *expr = parse_action(parser, eval_fprintf, 3);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (expr_open(parser, expr, file) != 0) {
+ return NULL;
+ }
+
+ if (bfs_printf_parse(parser->ctx, expr, format) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -fstype TYPE.
+ */
+static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_fstype);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (!bfs_ctx_mtab(parser->ctx)) {
+ parse_expr_error(parser, expr, "Couldn't parse the mount table: %s.\n", errstr());
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -gid/-group.
+ */
+static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_gid);
+ if (!expr) {
+ return NULL;
+ }
+
+ const struct group *grp = bfs_getgrnam(parser->ctx->groups, expr->argv[1]);
+ if (grp) {
+ expr->num = grp->gr_gid;
+ expr->int_cmp = BFS_INT_EQUAL;
+ } else if (looks_like_icmp(expr->argv[1])) {
+ if (!parse_icmp(parser, expr, 0)) {
+ return NULL;
+ }
+ } else if (errno) {
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ return NULL;
+ } else {
+ parse_expr_error(parser, expr, "No such group.\n");
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -unique.
+ */
+static struct bfs_expr *parse_unique(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->unique = true;
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -used N.
+ */
+static struct bfs_expr *parse_used(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_used);
+}
+
+/**
+ * Parse -uid/-user.
+ */
+static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_uid);
+ if (!expr) {
+ return NULL;
+ }
+
+ const struct passwd *pwd = bfs_getpwnam(parser->ctx->users, expr->argv[1]);
+ if (pwd) {
+ expr->num = pwd->pw_uid;
+ expr->int_cmp = BFS_INT_EQUAL;
+ } else if (looks_like_icmp(expr->argv[1])) {
+ if (!parse_icmp(parser, expr, 0)) {
+ return NULL;
+ }
+ } else if (errno) {
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ return NULL;
+ } else {
+ parse_expr_error(parser, expr, "No such user.\n");
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -hidden.
+ */
+static struct bfs_expr *parse_hidden(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_test(parser, eval_hidden);
+}
+
+/**
+ * Parse -(no)?ignore_readdir_race.
+ */
+static struct bfs_expr *parse_ignore_races(struct bfs_parser *parser, int ignore, int arg2) {
+ parser->ctx->ignore_races = ignore;
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -inum N.
+ */
+static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_inum);
+}
+
+/**
+ * Parse -j<n>.
+ */
+static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'j', false, &arg);
+ if (!expr) {
+ return NULL;
+ }
+
+ unsigned int n;
+ if (!parse_int(parser, expr->argv, arg, &n, IF_INT | IF_UNSIGNED)) {
+ return NULL;
+ }
+
+ if (n == 0) {
+ parse_expr_error(parser, expr, "${bld}0${rs} is not enough threads.\n");
+ return NULL;
+ }
+
+ parser->ctx->threads = n;
+ return expr;
+}
+
+/**
+ * Parse -limit N.
+ */
+static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_limit);
+ if (!expr) {
+ return NULL;
+ }
+
+ char **arg = &expr->argv[1];
+ if (!parse_int(parser, arg, *arg, &expr->num, IF_LONG_LONG)) {
+ return NULL;
+ }
+
+ if (expr->num <= 0) {
+ parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr);
+ return NULL;
+ }
+
+ parser->limit_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -links N.
+ */
+static struct bfs_expr *parse_links(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_links);
+}
+
+/**
+ * Parse -ls.
+ */
+static struct bfs_expr *parse_ls(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fls);
+ if (!expr) {
+ return NULL;
+ }
+
+ init_print_expr(parser, expr);
+ return expr;
+}
+
+/**
+ * Parse -mount.
+ */
+static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ parser->ctx->flags |= BFTW_SKIP_MOUNTS;
+ parser->mount_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -i?name.
+ */
+static struct bfs_expr *parse_name(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_name);
+ return parse_fnmatch(parser, expr, casefold);
+}
+
+/**
+ * Parse -i?path, -i?wholename.
+ */
+static struct bfs_expr *parse_path(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_path);
+ return parse_fnmatch(parser, expr, casefold);
+}
+
+/**
+ * Parse -i?lname.
+ */
+static struct bfs_expr *parse_lname(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_lname);
+ return parse_fnmatch(parser, expr, casefold);
+}
+
+/** Get the bfs_stat_field for X/Y in -newerXY. */
+static enum bfs_stat_field parse_newerxy_field(char c) {
+ switch (c) {
+ case 'a':
+ return BFS_STAT_ATIME;
+ case 'B':
+ return BFS_STAT_BTIME;
+ case 'c':
+ return BFS_STAT_CTIME;
+ case 'm':
+ return BFS_STAT_MTIME;
+ default:
+ return 0;
+ }
+}
+
+/** Parse an explicit reference timestamp for -newerXt and -*since. */
+static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr) {
+ if (xgetdate(expr->argv[1], &expr->reftime) == 0) {
+ return 0;
+ } else if (errno != EINVAL) {
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ return -1;
+ }
+
+ parse_expr_error(parser, expr, "Invalid timestamp.\n\n");
+ fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n");
+
+ struct tm tm;
+ if (!localtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "localtime_r()");
+ return -1;
+ }
+
+ int year = tm.tm_year + 1900;
+ int month = tm.tm_mon + 1;
+ fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday);
+ fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+
+#if BFS_HAS_TM_GMTOFF
+ int gmtoff = tm.tm_gmtoff;
+#else
+ int gmtoff = -timezone;
+#endif
+ int tz_hour = gmtoff / 3600;
+ int tz_min = (labs(gmtoff) / 60) % 60;
+ fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n",
+ year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min);
+
+ if (!gmtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "gmtime_r()");
+ return -1;
+ }
+
+ year = tm.tm_year + 1900;
+ month = tm.tm_mon + 1;
+ fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02dZ\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+
+ return -1;
+}
+
+/**
+ * Parse -newerXY.
+ */
+static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[0];
+ if (strlen(arg) != 8) {
+ parse_error(parser, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%pq${rs}.\n", arg + 6);
+ return NULL;
+ }
+
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->stat_field = parse_newerxy_field(arg[6]);
+ if (!expr->stat_field) {
+ parse_expr_error(parser, expr,
+ "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n",
+ arg[6]);
+ return NULL;
+ }
+
+ if (arg[7] == 't') {
+ if (parse_reftime(parser, expr) != 0) {
+ return NULL;
+ }
+ } else {
+ enum bfs_stat_field field = parse_newerxy_field(arg[7]);
+ if (!field) {
+ parse_expr_error(parser, expr,
+ "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n",
+ arg[7]);
+ return NULL;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
+ return NULL;
+ }
+
+ const struct timespec *reftime = bfs_stat_time(&sb, field);
+ if (!reftime) {
+ parse_expr_error(parser, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field));
+ return NULL;
+ }
+
+ expr->reftime = *reftime;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -noerror.
+ */
+static struct bfs_expr *parse_noerror(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->ignore_errors = true;
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -nogroup.
+ */
+static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_nogroup);
+ if (expr) {
+ // Who knows how many FDs getgrgid_r() needs?
+ expr->ephemeral_fds = 3;
+ }
+ return expr;
+}
+
+/**
+ * Parse -nohidden.
+ */
+static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg, BFS_TEST);
+ if (!hidden) {
+ return NULL;
+ }
+
+ bfs_expr_append(parser->ctx->exclude, hidden);
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -noleaf.
+ */
+static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ parse_expr_warning(parser, expr,
+ "${ex}%s${rs} does not apply the optimization that %px inhibits.\n\n",
+ BFS_COMMAND, expr);
+ return expr;
+}
+
+/**
+ * Parse -nouser.
+ */
+static struct bfs_expr *parse_nouser(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_nouser);
+ if (expr) {
+ // Who knows how many FDs getpwuid_r() needs?
+ expr->ephemeral_fds = 3;
+ }
+ return expr;
+}
+
+/**
+ * Parse a permission mode like chmod(1).
+ */
+static int parse_mode(const struct bfs_parser *parser, const char *mode, struct bfs_expr *expr) {
+ if (mode[0] >= '0' && mode[0] <= '9') {
+ unsigned int parsed;
+ if (!parse_int(parser, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ goto fail;
+ }
+ if (parsed > 07777) {
+ goto fail;
+ }
+
+ expr->file_mode = parsed;
+ expr->dir_mode = parsed;
+ return 0;
+ }
+
+ mode_t umask = parser->ctx->umask;
+
+ expr->file_mode = 0;
+ expr->dir_mode = 0;
+
+ // Parse the same grammar as chmod(1), which looks like this:
+ //
+ // MODE : CLAUSE ["," CLAUSE]*
+ //
+ // CLAUSE : WHO* ACTION+
+ //
+ // WHO : "u" | "g" | "o" | "a"
+ //
+ // ACTION : OP PERM*
+ // | OP PERMCOPY
+ //
+ // OP : "+" | "-" | "="
+ //
+ // PERM : "r" | "w" | "x" | "X" | "s" | "t"
+ //
+ // PERMCOPY : "u" | "g" | "o"
+
+ // State machine state
+ enum {
+ MODE_CLAUSE,
+ MODE_WHO,
+ MODE_ACTION,
+ MODE_ACTION_APPLY,
+ MODE_OP,
+ MODE_PERM,
+ } state = MODE_CLAUSE;
+
+ enum {
+ MODE_PLUS,
+ MODE_MINUS,
+ MODE_EQUALS,
+ } op uninit(MODE_EQUALS);
+
+ mode_t who uninit(0);
+ mode_t mask uninit(0);
+ mode_t file_change uninit(0);
+ mode_t dir_change uninit(0);
+
+ const char *i = mode;
+ while (true) {
+ switch (state) {
+ case MODE_CLAUSE:
+ who = 0;
+ mask = 0777;
+ state = MODE_WHO;
+ _fallthrough;
+
+ case MODE_WHO:
+ switch (*i) {
+ case 'u':
+ who |= 0700;
+ break;
+ case 'g':
+ who |= 0070;
+ break;
+ case 'o':
+ who |= 0007;
+ break;
+ case 'a':
+ who |= 0777;
+ break;
+ default:
+ state = MODE_ACTION;
+ continue;
+ }
+ break;
+
+ case MODE_ACTION_APPLY:
+ switch (op) {
+ case MODE_EQUALS:
+ expr->file_mode &= ~who;
+ expr->dir_mode &= ~who;
+ _fallthrough;
+ case MODE_PLUS:
+ expr->file_mode |= file_change;
+ expr->dir_mode |= dir_change;
+ break;
+ case MODE_MINUS:
+ expr->file_mode &= ~file_change;
+ expr->dir_mode &= ~dir_change;
+ break;
+ }
+ _fallthrough;
+
+ case MODE_ACTION:
+ if (who == 0) {
+ who = 0777;
+ mask = who & ~umask;
+ } else {
+ mask = who;
+ }
+
+ switch (*i) {
+ case '+':
+ op = MODE_PLUS;
+ state = MODE_OP;
+ break;
+ case '-':
+ op = MODE_MINUS;
+ state = MODE_OP;
+ break;
+ case '=':
+ op = MODE_EQUALS;
+ state = MODE_OP;
+ break;
+
+ case ',':
+ if (state == MODE_ACTION_APPLY) {
+ state = MODE_CLAUSE;
+ } else {
+ goto fail;
+ }
+ break;
+
+ case '\0':
+ if (state == MODE_ACTION_APPLY) {
+ goto done;
+ } else {
+ goto fail;
+ }
+
+ default:
+ goto fail;
+ }
+ break;
+
+ case MODE_OP:
+ switch (*i) {
+ case 'u':
+ file_change = (expr->file_mode >> 6) & 07;
+ dir_change = (expr->dir_mode >> 6) & 07;
+ break;
+ case 'g':
+ file_change = (expr->file_mode >> 3) & 07;
+ dir_change = (expr->dir_mode >> 3) & 07;
+ break;
+ case 'o':
+ file_change = expr->file_mode & 07;
+ dir_change = expr->dir_mode & 07;
+ break;
+
+ default:
+ file_change = 0;
+ dir_change = 0;
+ state = MODE_PERM;
+ continue;
+ }
+
+ file_change |= (file_change << 6) | (file_change << 3);
+ file_change &= mask;
+ dir_change |= (dir_change << 6) | (dir_change << 3);
+ dir_change &= mask;
+ state = MODE_ACTION_APPLY;
+ break;
+
+ case MODE_PERM:
+ switch (*i) {
+ case 'r':
+ file_change |= mask & 0444;
+ dir_change |= mask & 0444;
+ break;
+ case 'w':
+ file_change |= mask & 0222;
+ dir_change |= mask & 0222;
+ break;
+ case 'x':
+ file_change |= mask & 0111;
+ _fallthrough;
+ case 'X':
+ dir_change |= mask & 0111;
+ break;
+ case 's':
+ if (who & 0700) {
+ file_change |= S_ISUID;
+ dir_change |= S_ISUID;
+ }
+ if (who & 0070) {
+ file_change |= S_ISGID;
+ dir_change |= S_ISGID;
+ }
+ break;
+ case 't':
+ if (who & 0007) {
+ file_change |= S_ISVTX;
+ dir_change |= S_ISVTX;
+ }
+ break;
+ default:
+ state = MODE_ACTION_APPLY;
+ continue;
+ }
+ break;
+ }
+
+ ++i;
+ }
+
+done:
+ return 0;
+
+fail:
+ parse_expr_error(parser, expr, "Invalid mode.\n");
+ return -1;
+}
+
+/**
+ * Parse -perm MODE.
+ */
+static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_perm);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *mode = expr->argv[1];
+ switch (mode[0]) {
+ case '-':
+ expr->mode_cmp = BFS_MODE_ALL;
+ ++mode;
+ break;
+ case '/':
+ expr->mode_cmp = BFS_MODE_ANY;
+ ++mode;
+ break;
+ case '+':
+ if (mode[1] >= '0' && mode[1] <= '9') {
+ expr->mode_cmp = BFS_MODE_ANY;
+ ++mode;
+ break;
+ }
+ _fallthrough;
+ default:
+ expr->mode_cmp = BFS_MODE_EQUAL;
+ break;
+ }
+
+ if (parse_mode(parser, mode, expr) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -print.
+ */
+static struct bfs_expr *parse_print(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint);
+ if (expr) {
+ init_print_expr(parser, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -print0.
+ */
+static struct bfs_expr *parse_print0(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint0);
+ if (expr) {
+ init_print_expr(parser, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -printf FORMAT.
+ */
+static struct bfs_expr *parse_printf(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprintf);
+ if (!expr) {
+ return NULL;
+ }
+
+ init_print_expr(parser, expr);
+
+ if (bfs_printf_parse(parser->ctx, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -printx.
+ */
+static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprintx);
+ if (expr) {
+ init_print_expr(parser, expr);
+ }
+ return expr;
+}
+
+/**
+ * Parse -prune.
+ */
+static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_prune);
+ if (!expr) {
+ return NULL;
+ }
+
+ parser->prune_expr = expr;
+ return expr;
+}
+
+/**
+ * Parse -quit.
+ */
+static struct bfs_expr *parse_quit(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_action(parser, eval_quit);
+}
+
+/**
+ * Parse -i?regex.
+ */
+static struct bfs_expr *parse_regex(struct bfs_parser *parser, int flags, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_regex);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (bfs_regcomp(&expr->regex, expr->argv[1], parser->regex_type, flags) != 0) {
+ if (expr->regex) {
+ char *str = bfs_regerror(expr->regex);
+ if (str) {
+ parse_expr_error(parser, expr, "%s.\n", str);
+ free(str);
+ } else {
+ parse_perror(parser, "bfs_regerror()");
+ }
+ } else {
+ parse_perror(parser, "bfs_regcomp()");
+ }
+
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -E.
+ */
+static struct bfs_expr *parse_regex_extended(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
+ return parse_nullary_flag(parser);
+}
+
+/**
+ * Parse -regextype TYPE.
+ */
+static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+ CFILE *cfile = ctx->cerr;
+
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
+ cfprintf(cfile, "\n");
+ goto list_types;
+ }
+
+ // 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;
+ cfile = ctx->cout;
+ goto list_types;
+ } else {
+ parse_expr_error(parser, expr, "Unsupported regex type.\n\n");
+ goto list_types;
+ }
+
+ return expr;
+
+list_types:
+ cfprintf(cfile, "Supported types are:\n\n");
+ cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
+ cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
+ cfprintf(cfile, " ${bld}sed${rs}: Like ${grn}sed${rs} (same as ${bld}posix-basic${rs})\n\n");
+
+ cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n\n");
+
+#if BFS_WITH_ONIGURUMA
+ cfprintf(cfile, " [${bld}posix-${rs}]${bld}awk${rs}: Like ${grn}awk${rs}\n");
+ cfprintf(cfile, " ${bld}gnu-awk${rs}: Like GNU ${grn}awk${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n");
+ cfprintf(cfile, " [${bld}posix-${rs}]${bld}egrep${rs}: Like ${grn}grep${rs} ${cyn}-E${rs}\n\n");
+
+ cfprintf(cfile, " ${bld}findutils-default${rs}: Like GNU ${grn}find${rs}\n");
+#endif
+ return NULL;
+}
+
+/**
+ * Parse -s.
+ */
+static struct bfs_expr *parse_s(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->flags |= BFTW_SORT;
+ return parse_nullary_flag(parser);
+}
+
+/**
+ * Parse -samefile FILE.
+ */
+static struct bfs_expr *parse_samefile(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_samefile);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_stat sb;
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
+ return NULL;
+ }
+
+ expr->dev = sb.dev;
+ expr->ino = sb.ino;
+ return expr;
+}
+
+/**
+ * Parse -S STRATEGY.
+ */
+static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+ CFILE *cfile = ctx->cerr;
+
+ const char *arg;
+ struct bfs_expr *expr = parse_prefix_flag(parser, 'S', true, &arg);
+ if (!expr) {
+ cfprintf(cfile, "\n");
+ goto list_strategies;
+ }
+
+ if (strcmp(arg, "bfs") == 0) {
+ ctx->strategy = BFTW_BFS;
+ } else if (strcmp(arg, "dfs") == 0) {
+ ctx->strategy = BFTW_DFS;
+ } else if (strcmp(arg, "ids") == 0) {
+ ctx->strategy = BFTW_IDS;
+ } else if (strcmp(arg, "eds") == 0) {
+ ctx->strategy = BFTW_EDS;
+ } else if (strcmp(arg, "help") == 0) {
+ parser->just_info = true;
+ cfile = ctx->cout;
+ goto list_strategies;
+ } else {
+ parse_expr_error(parser, expr, "Unrecognized search strategy.\n\n");
+ goto list_strategies;
+ }
+
+ return expr;
+
+list_strategies:
+ cfprintf(cfile, "Supported search strategies:\n\n");
+ cfprintf(cfile, " ${bld}bfs${rs}: breadth-first search\n");
+ cfprintf(cfile, " ${bld}dfs${rs}: depth-first search\n");
+ cfprintf(cfile, " ${bld}ids${rs}: iterative deepening search\n");
+ cfprintf(cfile, " ${bld}eds${rs}: exponential deepening search\n");
+ return NULL;
+}
+
+/**
+ * Parse -[aBcm]?since.
+ */
+static struct bfs_expr *parse_since(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (parse_reftime(parser, expr) != 0) {
+ return NULL;
+ }
+
+ expr->stat_field = field;
+ return expr;
+}
+
+/**
+ * Parse -size N[cwbkMGTP]?.
+ */
+static struct bfs_expr *parse_size(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_size);
+ if (!expr) {
+ return NULL;
+ }
+
+ const char *unit = parse_icmp(parser, expr, IF_PARTIAL_OK);
+ if (!unit) {
+ return NULL;
+ }
+
+ if (strlen(unit) > 1) {
+ goto bad_unit;
+ }
+
+ switch (*unit) {
+ case '\0':
+ case 'b':
+ expr->size_unit = BFS_BLOCKS;
+ break;
+ case 'c':
+ expr->size_unit = BFS_BYTES;
+ break;
+ case 'w':
+ expr->size_unit = BFS_WORDS;
+ break;
+ case 'k':
+ expr->size_unit = BFS_KB;
+ break;
+ case 'M':
+ expr->size_unit = BFS_MB;
+ break;
+ case 'G':
+ expr->size_unit = BFS_GB;
+ break;
+ case 'T':
+ expr->size_unit = BFS_TB;
+ break;
+ case 'P':
+ expr->size_unit = BFS_PB;
+ break;
+
+ default:
+ bad_unit:
+ parse_expr_error(parser, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%pq${rs}.\n", unit);
+ return NULL;
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -sparse.
+ */
+static struct bfs_expr *parse_sparse(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_test(parser, eval_sparse);
+}
+
+/**
+ * Parse -status.
+ */
+static struct bfs_expr *parse_status(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->status = true;
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -x?type [bcdpflsD].
+ */
+static struct bfs_expr *parse_type(struct bfs_parser *parser, int x, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ bfs_eval_fn *eval = x ? eval_xtype : eval_type;
+ struct bfs_expr *expr = parse_unary_test(parser, eval);
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->num = 0;
+
+ const char *c = expr->argv[1];
+ while (true) {
+ switch (*c) {
+ case 'b':
+ expr->num |= 1 << BFS_BLK;
+ break;
+ case 'c':
+ expr->num |= 1 << BFS_CHR;
+ break;
+ case 'd':
+ expr->num |= 1 << BFS_DIR;
+ break;
+ case 'D':
+ expr->num |= 1 << BFS_DOOR;
+ break;
+ case 'p':
+ expr->num |= 1 << BFS_FIFO;
+ break;
+ case 'f':
+ expr->num |= 1 << BFS_REG;
+ break;
+ case 'l':
+ expr->num |= 1 << BFS_LNK;
+ break;
+ case 's':
+ expr->num |= 1 << BFS_SOCK;
+ break;
+ case 'w':
+ expr->num |= 1 << BFS_WHT;
+ ctx->flags |= BFTW_WHITEOUTS;
+ break;
+
+ case '\0':
+ parse_expr_error(parser, expr, "Expected a type flag.\n");
+ return NULL;
+
+ default:
+ parse_expr_error(parser, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c);
+ return NULL;
+ }
+
+ ++c;
+ if (*c == '\0') {
+ break;
+ } else if (*c == ',') {
+ ++c;
+ continue;
+ } else {
+ parse_expr_error(parser, expr, "Types must be comma-separated.\n");
+ return NULL;
+ }
+ }
+
+ return expr;
+}
+
+/**
+ * Parse -(no)?warn.
+ */
+static struct bfs_expr *parse_warn(struct bfs_parser *parser, int warn, int arg2) {
+ parser->ctx->warn = warn;
+ return parse_nullary_option(parser);
+}
+
+/**
+ * Parse -xattr.
+ */
+static struct bfs_expr *parse_xattr(struct bfs_parser *parser, int arg1, int arg2) {
+#if BFS_CAN_CHECK_XATTRS
+ return parse_nullary_test(parser, eval_xattr);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -xattrname.
+ */
+static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int arg2) {
+#if BFS_CAN_CHECK_XATTRS
+ return parse_unary_test(parser, eval_xattrname);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
+}
+
+/**
+ * Parse -xdev.
+ */
+static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ parser->ctx->flags |= BFTW_PRUNE_MOUNTS;
+ parser->xdev_expr = expr;
+ return expr;
+}
+
+/**
+ * Launch a pager for the help output.
+ */
+static CFILE *launch_pager(pid_t *pid, CFILE *cout) {
+ char *pager = getenv("PAGER");
+
+ char *exe;
+ if (pager && pager[0]) {
+ exe = bfs_spawn_resolve(pager);
+ } else {
+ exe = bfs_spawn_resolve("less");
+ if (!exe) {
+ exe = bfs_spawn_resolve("more");
+ }
+ }
+ if (!exe) {
+ goto fail;
+ }
+
+ int pipefd[2];
+ if (pipe(pipefd) != 0) {
+ goto fail_exe;
+ }
+
+ FILE *file = fdopen(pipefd[1], "w");
+ if (!file) {
+ goto fail_pipe;
+ }
+ pipefd[1] = -1;
+
+ CFILE *ret = cfwrap(file, NULL, true);
+ if (!ret) {
+ goto fail_file;
+ }
+ file = NULL;
+
+ struct bfs_spawn ctx;
+ if (bfs_spawn_init(&ctx) != 0) {
+ goto fail_ret;
+ }
+
+ if (bfs_spawn_addclose(&ctx, fileno(ret->file)) != 0) {
+ goto fail_ctx;
+ }
+ if (bfs_spawn_adddup2(&ctx, pipefd[0], STDIN_FILENO) != 0) {
+ goto fail_ctx;
+ }
+ if (bfs_spawn_addclose(&ctx, pipefd[0]) != 0) {
+ goto fail_ctx;
+ }
+
+ char *argv[] = {
+ exe,
+ NULL,
+ NULL,
+ };
+
+ const char *cmd = exe + xbaseoff(exe);
+ if (strcmp(cmd, "less") == 0) {
+ // We know less supports colors, other pagers may not
+ ret->colors = cout->colors;
+ argv[1] = "-FKRX";
+ }
+
+ *pid = bfs_spawn(exe, &ctx, argv, NULL);
+ if (*pid < 0) {
+ goto fail_ctx;
+ }
+
+ xclose(pipefd[0]);
+ bfs_spawn_destroy(&ctx);
+ free(exe);
+ return ret;
+
+fail_ctx:
+ bfs_spawn_destroy(&ctx);
+fail_ret:
+ cfclose(ret);
+fail_file:
+ if (file) {
+ fclose(file);
+ }
+fail_pipe:
+ if (pipefd[1] >= 0) {
+ xclose(pipefd[1]);
+ }
+ if (pipefd[0] >= 0) {
+ xclose(pipefd[0]);
+ }
+fail_exe:
+ free(exe);
+fail:
+ return cout;
+}
+
+/**
+ * "Parse" -help.
+ */
+static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2) {
+ CFILE *cout = parser->ctx->cout;
+
+ pid_t pager = -1;
+ if (parser->stdout_tty) {
+ cout = launch_pager(&pager, cout);
+ }
+
+ cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n",
+ parser->command);
+
+ cfprintf(cout, "${ex}%s${rs} is compatible with ${ex}find${rs}, with some extensions. "
+ "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n"
+ "and ${blu}expressions${rs} may be freely mixed in any order.\n\n",
+ BFS_COMMAND);
+
+ cfprintf(cout, "${bld}Flags:${rs}\n\n");
+
+ cfprintf(cout, " ${cyn}-H${rs}\n");
+ cfprintf(cout, " Follow symbolic links on the command line, but not while searching\n");
+ cfprintf(cout, " ${cyn}-L${rs}\n");
+ cfprintf(cout, " Follow all symbolic links\n");
+ cfprintf(cout, " ${cyn}-P${rs}\n");
+ cfprintf(cout, " Never follow symbolic links (the default)\n");
+
+ cfprintf(cout, " ${cyn}-E${rs}\n");
+ cfprintf(cout, " Use extended regular expressions (same as ${blu}-regextype${rs} ${bld}posix-extended${rs})\n");
+ cfprintf(cout, " ${cyn}-X${rs}\n");
+ cfprintf(cout, " Filter out files with non-${ex}xargs${rs}-safe names\n");
+ cfprintf(cout, " ${cyn}-d${rs}\n");
+ cfprintf(cout, " Search in post-order (same as ${blu}-depth${rs})\n");
+ cfprintf(cout, " ${cyn}-s${rs}\n");
+ cfprintf(cout, " Visit directory entries in sorted order\n");
+ cfprintf(cout, " ${cyn}-x${rs}\n");
+ cfprintf(cout, " Don't descend into other mount points (same as ${blu}-xdev${rs})\n");
+
+ cfprintf(cout, " ${cyn}-f${rs} ${mag}PATH${rs}\n");
+ cfprintf(cout, " Treat ${mag}PATH${rs} as a path to search (useful if begins with a dash)\n");
+ cfprintf(cout, " ${cyn}-D${rs} ${bld}FLAG${rs}\n");
+ cfprintf(cout, " Turn on a debugging flag (see ${cyn}-D${rs} ${bld}help${rs})\n");
+ cfprintf(cout, " ${cyn}-O${bld}N${rs}\n");
+ cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n");
+ cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n");
+ cfprintf(cout, " Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n");
+ cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n");
+ cfprintf(cout, " ${cyn}-j${bld}N${rs}\n");
+ cfprintf(cout, " Search with ${bld}N${rs} threads in parallel (default: number of CPUs, up to ${bld}8${rs})\n\n");
+
+ cfprintf(cout, "${bld}Operators:${rs}\n\n");
+
+ cfprintf(cout, " ${red}(${rs} ${blu}expression${rs} ${red})${rs}\n\n");
+
+ cfprintf(cout, " ${red}!${rs} ${blu}expression${rs}\n");
+ cfprintf(cout, " ${red}-not${rs} ${blu}expression${rs}\n\n");
+
+ cfprintf(cout, " ${blu}expression${rs} ${blu}expression${rs}\n");
+ cfprintf(cout, " ${blu}expression${rs} ${red}-a${rs} ${blu}expression${rs}\n");
+ cfprintf(cout, " ${blu}expression${rs} ${red}-and${rs} ${blu}expression${rs}\n\n");
+
+ cfprintf(cout, " ${blu}expression${rs} ${red}-o${rs} ${blu}expression${rs}\n");
+ cfprintf(cout, " ${blu}expression${rs} ${red}-or${rs} ${blu}expression${rs}\n\n");
+
+ cfprintf(cout, " ${blu}expression${rs} ${red},${rs} ${blu}expression${rs}\n\n");
+
+ cfprintf(cout, "${bld}Special forms:${rs}\n\n");
+
+ cfprintf(cout, " ${red}-exclude${rs} ${blu}expression${rs}\n");
+ cfprintf(cout, " Exclude all paths matching the ${blu}expression${rs} from the search.\n\n");
+
+ cfprintf(cout, "${bld}Options:${rs}\n\n");
+
+ cfprintf(cout, " ${blu}-color${rs}\n");
+ cfprintf(cout, " ${blu}-nocolor${rs}\n");
+ cfprintf(cout, " Turn colors on or off (default: ${blu}-color${rs} if outputting to a terminal,\n");
+ cfprintf(cout, " ${blu}-nocolor${rs} otherwise)\n");
+ cfprintf(cout, " ${blu}-daystart${rs}\n");
+ cfprintf(cout, " Measure times relative to the start of today\n");
+ cfprintf(cout, " ${blu}-depth${rs}\n");
+ cfprintf(cout, " Search in post-order (descendents first)\n");
+ cfprintf(cout, " ${blu}-files0-from${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " Search the NUL ('\\0')-separated paths from ${bld}FILE${rs} (${bld}-${rs} for standard input).\n");
+ cfprintf(cout, " ${blu}-follow${rs}\n");
+ cfprintf(cout, " Follow all symbolic links (same as ${cyn}-L${rs})\n");
+ cfprintf(cout, " ${blu}-ignore_readdir_race${rs}\n");
+ cfprintf(cout, " ${blu}-noignore_readdir_race${rs}\n");
+ cfprintf(cout, " Whether to report an error if ${ex}%s${rs} detects that the file tree is modified\n",
+ BFS_COMMAND);
+ cfprintf(cout, " during the search (default: ${blu}-noignore_readdir_race${rs})\n");
+ cfprintf(cout, " ${blu}-maxdepth${rs} ${bld}N${rs}\n");
+ cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n");
+ cfprintf(cout, " Ignore files deeper/shallower than ${bld}N${rs}\n");
+ cfprintf(cout, " ${blu}-mount${rs}\n");
+ cfprintf(cout, " Exclude mount points entirely from the results\n");
+ cfprintf(cout, " ${blu}-noerror${rs}\n");
+ cfprintf(cout, " Ignore any errors that occur during traversal\n");
+ cfprintf(cout, " ${blu}-nohidden${rs}\n");
+ cfprintf(cout, " Exclude hidden files\n");
+ cfprintf(cout, " ${blu}-noleaf${rs}\n");
+ cfprintf(cout, " Ignored; for compatibility with GNU find\n");
+ cfprintf(cout, " ${blu}-regextype${rs} ${bld}TYPE${rs}\n");
+ cfprintf(cout, " Use ${bld}TYPE${rs}-flavored regexes (default: ${bld}posix-basic${rs}; see ${blu}-regextype${rs} ${bld}help${rs})\n");
+ cfprintf(cout, " ${blu}-status${rs}\n");
+ cfprintf(cout, " Display a status bar while searching\n");
+ cfprintf(cout, " ${blu}-unique${rs}\n");
+ cfprintf(cout, " Skip any files that have already been seen\n");
+ cfprintf(cout, " ${blu}-warn${rs}\n");
+ cfprintf(cout, " ${blu}-nowarn${rs}\n");
+ cfprintf(cout, " Turn on or off warnings about the command line\n");
+ cfprintf(cout, " ${blu}-xdev${rs}\n");
+ cfprintf(cout, " Don't descend into other mount points\n\n");
+
+ cfprintf(cout, "${bld}Tests:${rs}\n\n");
+
+#if BFS_CAN_CHECK_ACL
+ cfprintf(cout, " ${blu}-acl${rs}\n");
+ cfprintf(cout, " Find files with a non-trivial Access Control List\n");
+#endif
+ cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}min${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} minutes ago\n");
+ cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}newer${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}FILE${rs} was\n"
+ " modified\n");
+ cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}since${rs} ${bld}TIME${rs}\n");
+ cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified more recently than ${bld}TIME${rs}\n");
+ cfprintf(cout, " ${blu}-${rs}[${blu}aBcm${rs}]${blu}time${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files ${blu}a${rs}ccessed/${blu}B${rs}irthed/${blu}c${rs}hanged/${blu}m${rs}odified ${bld}N${rs} days ago\n");
+#if BFS_CAN_CHECK_CAPABILITIES
+ cfprintf(cout, " ${blu}-capable${rs}\n");
+ cfprintf(cout, " Find files with POSIX.1e capabilities set\n");
+#endif
+#if BFS_CAN_CHECK_CONTEXT
+ cfprintf(cout, " ${blu}-context${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find files with SELinux context matching a glob pattern\n");
+#endif
+ cfprintf(cout, " ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files with depth ${bld}N${rs}\n");
+ cfprintf(cout, " ${blu}-empty${rs}\n");
+ cfprintf(cout, " Find empty files/directories\n");
+ cfprintf(cout, " ${blu}-executable${rs}\n");
+ cfprintf(cout, " ${blu}-readable${rs}\n");
+ cfprintf(cout, " ${blu}-writable${rs}\n");
+ cfprintf(cout, " Find files the current user can execute/read/write\n");
+ cfprintf(cout, " ${blu}-false${rs}\n");
+ cfprintf(cout, " ${blu}-true${rs}\n");
+ cfprintf(cout, " Always false/true\n");
+ cfprintf(cout, " ${blu}-fstype${rs} ${bld}TYPE${rs}\n");
+ cfprintf(cout, " Find files on file systems with the given ${bld}TYPE${rs}\n");
+ cfprintf(cout, " ${blu}-gid${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " ${blu}-uid${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files owned by group/user ID ${bld}N${rs}\n");
+ cfprintf(cout, " ${blu}-group${rs} ${bld}NAME${rs}\n");
+ cfprintf(cout, " ${blu}-user${rs} ${bld}NAME${rs}\n");
+ cfprintf(cout, " Find files owned by the group/user ${bld}NAME${rs}\n");
+ cfprintf(cout, " ${blu}-hidden${rs}\n");
+ cfprintf(cout, " Find hidden files\n");
+#ifdef FNM_CASEFOLD
+ cfprintf(cout, " ${blu}-ilname${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-iname${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-ipath${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-iregex${rs} ${bld}REGEX${rs}\n");
+ cfprintf(cout, " ${blu}-iwholename${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Case-insensitive versions of ${blu}-lname${rs}/${blu}-name${rs}/${blu}-path${rs}"
+ "/${blu}-regex${rs}/${blu}-wholename${rs}\n");
+#endif
+ cfprintf(cout, " ${blu}-inum${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files with inode number ${bld}N${rs}\n");
+ cfprintf(cout, " ${blu}-links${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files with ${bld}N${rs} hard links\n");
+ cfprintf(cout, " ${blu}-lname${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find symbolic links whose target matches the ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-name${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find files whose name matches the ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-newer${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " Find files newer than ${bld}FILE${rs}\n");
+ cfprintf(cout, " ${blu}-newer${bld}XY${rs} ${bld}REFERENCE${rs}\n");
+ cfprintf(cout, " Find files whose ${bld}X${rs} time is newer than the ${bld}Y${rs} time of"
+ " ${bld}REFERENCE${rs}. ${bld}X${rs} and ${bld}Y${rs}\n");
+ cfprintf(cout, " can be any of [${bld}aBcm${rs}]. ${bld}Y${rs} may also be ${bld}t${rs} to parse ${bld}REFERENCE${rs} an explicit\n");
+ cfprintf(cout, " timestamp.\n");
+ cfprintf(cout, " ${blu}-nogroup${rs}\n");
+ cfprintf(cout, " ${blu}-nouser${rs}\n");
+ cfprintf(cout, " Find files owned by nonexistent groups/users\n");
+ cfprintf(cout, " ${blu}-path${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-wholename${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find files whose entire path matches the ${bld}GLOB${rs}\n");
+ cfprintf(cout, " ${blu}-perm${rs} ${bld}[-]MODE${rs}\n");
+ cfprintf(cout, " Find files with a matching mode\n");
+ cfprintf(cout, " ${blu}-regex${rs} ${bld}REGEX${rs}\n");
+ cfprintf(cout, " Find files whose entire path matches the regular expression ${bld}REGEX${rs}\n");
+ cfprintf(cout, " ${blu}-samefile${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " Find hard links to ${bld}FILE${rs}\n");
+ cfprintf(cout, " ${blu}-since${rs} ${bld}TIME${rs}\n");
+ cfprintf(cout, " Find files modified since ${bld}TIME${rs}\n");
+ cfprintf(cout, " ${blu}-size${rs} ${bld}[-+]N[cwbkMGTP]${rs}\n");
+ cfprintf(cout, " Find files with the given size, in 1-byte ${bld}c${rs}haracters, 2-byte ${bld}w${rs}ords,\n");
+ cfprintf(cout, " 512-byte ${bld}b${rs}locks (default), or ${bld}k${rs}iB/${bld}M${rs}iB/${bld}G${rs}iB/${bld}T${rs}iB/${bld}P${rs}iB\n");
+ cfprintf(cout, " ${blu}-sparse${rs}\n");
+ cfprintf(cout, " Find files that occupy fewer disk blocks than expected\n");
+ cfprintf(cout, " ${blu}-type${rs} ${bld}[bcdlpfswD]${rs}\n");
+ cfprintf(cout, " Find files of the given type\n");
+ cfprintf(cout, " ${blu}-used${rs} ${bld}[-+]N${rs}\n");
+ cfprintf(cout, " Find files last accessed ${bld}N${rs} days after they were changed\n");
+#if BFS_CAN_CHECK_XATTRS
+ cfprintf(cout, " ${blu}-xattr${rs}\n");
+ cfprintf(cout, " Find files with extended attributes\n");
+ cfprintf(cout, " ${blu}-xattrname${rs} ${bld}NAME${rs}\n");
+ cfprintf(cout, " Find files with the extended attribute ${bld}NAME${rs}\n");
+#endif
+ cfprintf(cout, " ${blu}-xtype${rs} ${bld}[bcdlpfswD]${rs}\n");
+ cfprintf(cout, " Find files of the given type, following links when ${blu}-type${rs} would not, and\n");
+ cfprintf(cout, " vice versa\n\n");
+
+ cfprintf(cout, "${bld}Actions:${rs}\n\n");
+
+ cfprintf(cout, " ${blu}-delete${rs}\n");
+ cfprintf(cout, " ${blu}-rm${rs}\n");
+ cfprintf(cout, " Delete any found files (implies ${blu}-depth${rs})\n");
+ cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} ;${rs}\n");
+ cfprintf(cout, " Execute a command\n");
+ cfprintf(cout, " ${blu}-exec${rs} ${bld}command ... {} +${rs}\n");
+ cfprintf(cout, " Execute a command with multiple files at once\n");
+ cfprintf(cout, " ${blu}-ok${rs} ${bld}command ... {} ;${rs}\n");
+ cfprintf(cout, " Prompt the user whether to execute a command\n");
+ cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} ;${rs}\n");
+ cfprintf(cout, " ${blu}-execdir${rs} ${bld}command ... {} +${rs}\n");
+ cfprintf(cout, " ${blu}-okdir${rs} ${bld}command ... {} ;${rs}\n");
+ cfprintf(cout, " Like ${blu}-exec${rs}/${blu}-ok${rs}, but run the command in the same directory as the found\n");
+ cfprintf(cout, " file(s)\n");
+ cfprintf(cout, " ${blu}-exit${rs} [${bld}STATUS${rs}]\n");
+ cfprintf(cout, " Exit immediately with the given status (%d if unspecified)\n", EXIT_SUCCESS);
+ cfprintf(cout, " ${blu}-fls${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " ${blu}-fprint${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " ${blu}-fprint0${rs} ${bld}FILE${rs}\n");
+ cfprintf(cout, " ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n");
+ cfprintf(cout, " Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n"
+ " output\n");
+ cfprintf(cout, " ${blu}-limit${rs} ${bld}N${rs}\n");
+ cfprintf(cout, " Quit after this action is evaluated ${bld}N${rs} times\n");
+ cfprintf(cout, " ${blu}-ls${rs}\n");
+ cfprintf(cout, " List files like ${ex}ls${rs} ${bld}-dils${rs}\n");
+ cfprintf(cout, " ${blu}-print${rs}\n");
+ cfprintf(cout, " Print the path to the found file\n");
+ cfprintf(cout, " ${blu}-print0${rs}\n");
+ cfprintf(cout, " Like ${blu}-print${rs}, but use the null character ('\\0') as a separator rather than\n");
+ cfprintf(cout, " newlines\n");
+ cfprintf(cout, " ${blu}-printf${rs} ${bld}FORMAT${rs}\n");
+ cfprintf(cout, " Print according to a format string (see ${ex}man${rs} ${bld}find${rs}). The additional format\n");
+ cfprintf(cout, " directives %%w and %%W${bld}k${rs} for printing file birth times are supported.\n");
+ cfprintf(cout, " ${blu}-printx${rs}\n");
+ cfprintf(cout, " Like ${blu}-print${rs}, but escape whitespace and quotation characters, to make the\n");
+ cfprintf(cout, " output safe for ${ex}xargs${rs}. Consider using ${blu}-print0${rs} and ${ex}xargs${rs} ${bld}-0${rs} instead.\n");
+ cfprintf(cout, " ${blu}-prune${rs}\n");
+ cfprintf(cout, " Don't descend into this directory\n");
+ cfprintf(cout, " ${blu}-quit${rs}\n");
+ cfprintf(cout, " Quit immediately\n");
+ cfprintf(cout, " ${blu}-version${rs}\n");
+ cfprintf(cout, " Print version information\n");
+ cfprintf(cout, " ${blu}-help${rs}\n");
+ cfprintf(cout, " Print this help message\n\n");
+
+ cfprintf(cout, "%s\n", BFS_HOMEPAGE);
+
+ if (pager > 0) {
+ cfclose(cout);
+ xwaitpid(pager, NULL, 0);
+ }
+
+ parser->just_info = true;
+ return NULL;
+}
+
+/** Print the bfs "logo". */
+static void print_logo(CFILE *cout) {
+ if (!cout->colors) {
+ goto boring;
+ }
+
+ size_t vwidth = xstrwidth(bfs_version);
+ dchar *spaces = dstrepeat(" ", vwidth);
+ dchar *lines = dstrepeat("─", vwidth);
+ if (!spaces || !lines) {
+ dstrfree(lines);
+ dstrfree(spaces);
+ goto boring;
+ }
+
+ // We do ----\r<emoji> rather than <emoji>--- so we don't have to assume
+ // anything about the width of the emoji
+ cfprintf(cout, "╭─────%s╮\r📂\n", lines);
+ cfprintf(cout, "├${ex}b${rs} %s │\n", spaces);
+ cfprintf(cout, "╰├${ex}f${rs} ${bld}%s${rs} │\n", bfs_version);
+ cfprintf(cout, " ╰├${ex}s${rs} %s │\n", spaces);
+ cfprintf(cout, " ╰──%s─╯\n\n", lines);
+
+ dstrfree(lines);
+ dstrfree(spaces);
+ return;
+
+boring:
+ printf("%s %s\n\n", BFS_COMMAND, bfs_version);
+}
+
+/**
+ * "Parse" -version.
+ */
+static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) {
+ print_logo(parser->ctx->cout);
+
+ printf("Copyright © Tavian Barnes and the bfs contributors\n");
+ printf("No rights reserved (https://opensource.org/license/0BSD)\n\n");
+
+ printf("CONFFLAGS := %s\n", bfs_confflags);
+ printf("CC := %s\n", bfs_cc);
+ printf("CPPFLAGS := %s\n", bfs_cppflags);
+ printf("CFLAGS := %s\n", bfs_cflags);
+ printf("LDFLAGS := %s\n", bfs_ldflags);
+ printf("LDLIBS := %s\n", bfs_ldlibs);
+
+ printf("\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);
+
+/**
+ * An entry in the parse table for primary expressions.
+ */
+struct table_entry {
+ char *arg;
+ enum bfs_kind kind;
+ parse_fn *parse;
+ int arg1;
+ int arg2;
+ bool prefix;
+ bool needs_arg;
+};
+
+/**
+ * The parse table for primary expressions.
+ */
+static const struct table_entry parse_table[] = {
+ {"--", BFS_FLAG},
+ {"--help", BFS_ACTION, parse_help},
+ {"--version", BFS_ACTION, parse_version},
+ {"-Bmin", BFS_TEST, parse_min, BFS_STAT_BTIME},
+ {"-Bnewer", BFS_TEST, parse_newer, BFS_STAT_BTIME},
+ {"-Bsince", BFS_TEST, parse_since, BFS_STAT_BTIME},
+ {"-Btime", BFS_TEST, parse_time, BFS_STAT_BTIME},
+ {"-D", BFS_FLAG, parse_debug, .prefix = true},
+ {"-E", BFS_FLAG, parse_regex_extended},
+ {"-H", BFS_FLAG, parse_follow, BFTW_FOLLOW_ROOTS, false},
+ {"-L", BFS_FLAG, parse_follow, BFTW_FOLLOW_ALL, false},
+ {"-O", BFS_FLAG, parse_optlevel, .prefix = true},
+ {"-P", BFS_FLAG, parse_follow, 0, false},
+ {"-S", BFS_FLAG, parse_search_strategy, .prefix = true},
+ {"-X", BFS_FLAG, parse_xargs_safe},
+ {"-a", BFS_OPERATOR},
+ {"-acl", BFS_TEST, parse_acl},
+ {"-amin", BFS_TEST, parse_min, BFS_STAT_ATIME},
+ {"-and", BFS_OPERATOR},
+ {"-anewer", BFS_TEST, parse_newer, BFS_STAT_ATIME},
+ {"-asince", BFS_TEST, parse_since, BFS_STAT_ATIME},
+ {"-atime", BFS_TEST, parse_time, BFS_STAT_ATIME},
+ {"-capable", BFS_TEST, parse_capable},
+ {"-cmin", BFS_TEST, parse_min, BFS_STAT_CTIME},
+ {"-cnewer", BFS_TEST, parse_newer, BFS_STAT_CTIME},
+ {"-color", BFS_OPTION, parse_color, true},
+ {"-context", BFS_TEST, parse_context, true},
+ {"-csince", BFS_TEST, parse_since, BFS_STAT_CTIME},
+ {"-ctime", BFS_TEST, parse_time, BFS_STAT_CTIME},
+ {"-d", BFS_FLAG, parse_depth, true},
+ {"-daystart", BFS_OPTION, parse_daystart},
+ {"-delete", BFS_ACTION, parse_delete},
+ {"-depth", BFS_OPTION, parse_depth_n, false},
+ {"-empty", BFS_TEST, parse_empty},
+ {"-exclude", BFS_OPERATOR},
+ {"-exec", BFS_ACTION, parse_exec, 0},
+ {"-execdir", BFS_ACTION, parse_exec, BFS_EXEC_CHDIR},
+ {"-executable", BFS_TEST, parse_access, X_OK},
+ {"-exit", BFS_ACTION, parse_exit},
+ {"-f", BFS_FLAG, parse_f, .needs_arg = true},
+ {"-false", BFS_TEST, parse_const, false},
+ {"-files0-from", BFS_OPTION, parse_files0_from},
+ {"-flags", BFS_TEST, parse_flags},
+ {"-fls", BFS_ACTION, parse_fls},
+ {"-follow", BFS_OPTION, parse_follow, BFTW_FOLLOW_ALL, true},
+ {"-fprint", BFS_ACTION, parse_fprint},
+ {"-fprint0", BFS_ACTION, parse_fprint0},
+ {"-fprintf", BFS_ACTION, parse_fprintf},
+ {"-fstype", BFS_TEST, parse_fstype},
+ {"-gid", BFS_TEST, parse_group},
+ {"-group", BFS_TEST, parse_group},
+ {"-help", BFS_ACTION, parse_help},
+ {"-hidden", BFS_TEST, parse_hidden},
+ {"-ignore_readdir_race", BFS_OPTION, parse_ignore_races, true},
+ {"-ilname", BFS_TEST, parse_lname, true},
+ {"-iname", BFS_TEST, parse_name, true},
+ {"-inum", BFS_TEST, parse_inum},
+ {"-ipath", BFS_TEST, parse_path, true},
+ {"-iregex", BFS_TEST, parse_regex, BFS_REGEX_ICASE},
+ {"-iwholename", BFS_TEST, parse_path, true},
+ {"-j", BFS_FLAG, parse_jobs, .prefix = true},
+ {"-limit", BFS_ACTION, parse_limit},
+ {"-links", BFS_TEST, parse_links},
+ {"-lname", BFS_TEST, parse_lname, false},
+ {"-ls", BFS_ACTION, parse_ls},
+ {"-maxdepth", BFS_OPTION, parse_depth_limit, false},
+ {"-mindepth", BFS_OPTION, parse_depth_limit, true},
+ {"-mmin", BFS_TEST, parse_min, BFS_STAT_MTIME},
+ {"-mnewer", BFS_TEST, parse_newer, BFS_STAT_MTIME},
+ {"-mount", BFS_OPTION, parse_mount},
+ {"-msince", BFS_TEST, parse_since, BFS_STAT_MTIME},
+ {"-mtime", BFS_TEST, parse_time, BFS_STAT_MTIME},
+ {"-name", BFS_TEST, parse_name, false},
+ {"-newer", BFS_TEST, parse_newer, BFS_STAT_MTIME},
+ {"-newer", BFS_TEST, parse_newerxy, .prefix = true},
+ {"-nocolor", BFS_OPTION, parse_color, false},
+ {"-noerror", BFS_OPTION, parse_noerror},
+ {"-nogroup", BFS_TEST, parse_nogroup},
+ {"-nohidden", BFS_TEST, parse_nohidden},
+ {"-noignore_readdir_race", BFS_OPTION, parse_ignore_races, false},
+ {"-noleaf", BFS_OPTION, parse_noleaf},
+ {"-not", BFS_OPERATOR},
+ {"-nouser", BFS_TEST, parse_nouser},
+ {"-nowarn", BFS_OPTION, parse_warn, false},
+ {"-o", BFS_OPERATOR},
+ {"-ok", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM},
+ {"-okdir", BFS_ACTION, parse_exec, BFS_EXEC_CONFIRM | BFS_EXEC_CHDIR},
+ {"-or", BFS_OPERATOR},
+ {"-path", BFS_TEST, parse_path, false},
+ {"-perm", BFS_TEST, parse_perm},
+ {"-print", BFS_ACTION, parse_print},
+ {"-print0", BFS_ACTION, parse_print0},
+ {"-printf", BFS_ACTION, parse_printf},
+ {"-printx", BFS_ACTION, parse_printx},
+ {"-prune", BFS_ACTION, parse_prune},
+ {"-quit", BFS_ACTION, parse_quit},
+ {"-readable", BFS_TEST, parse_access, R_OK},
+ {"-regex", BFS_TEST, parse_regex, 0},
+ {"-regextype", BFS_OPTION, parse_regextype},
+ {"-rm", BFS_ACTION, parse_delete},
+ {"-s", BFS_FLAG, parse_s},
+ {"-samefile", BFS_TEST, parse_samefile},
+ {"-since", BFS_TEST, parse_since, BFS_STAT_MTIME},
+ {"-size", BFS_TEST, parse_size},
+ {"-sparse", BFS_TEST, parse_sparse},
+ {"-status", BFS_OPTION, parse_status},
+ {"-true", BFS_TEST, parse_const, true},
+ {"-type", BFS_TEST, parse_type, false},
+ {"-uid", BFS_TEST, parse_user},
+ {"-unique", BFS_OPTION, parse_unique},
+ {"-used", BFS_TEST, parse_used},
+ {"-user", BFS_TEST, parse_user},
+ {"-version", BFS_ACTION, parse_version},
+ {"-warn", BFS_OPTION, parse_warn, true},
+ {"-wholename", BFS_TEST, parse_path, false},
+ {"-writable", BFS_TEST, parse_access, W_OK},
+ {"-x", BFS_FLAG, parse_xdev},
+ {"-xattr", BFS_TEST, parse_xattr},
+ {"-xattrname", BFS_TEST, parse_xattrname},
+ {"-xdev", BFS_OPTION, parse_xdev},
+ {"-xtype", BFS_TEST, parse_type, true},
+ {0},
+};
+
+/** Look up an argument in the 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) {
+ match = strncmp(arg, entry->arg, strlen(entry->arg)) == 0;
+ } else {
+ match = strcmp(arg, entry->arg) == 0;
+ }
+ if (match) {
+ return entry;
+ }
+ }
+
+ return NULL;
+}
+
+/** Look up a single-character flag in the parse table. */
+static const struct table_entry *flag_lookup(char flag) {
+ for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
+ enum bfs_kind kind = entry->kind;
+ if (kind == BFS_FLAG && entry->arg[1] == flag && !entry->arg[2]) {
+ return entry;
+ }
+ }
+
+ return NULL;
+}
+
+/** Check for a multi-flag argument like -LEXO2. */
+static bool is_flag_group(const char *arg) {
+ // We enforce that at least one flag in a flag group must be a capital
+ // letter, to avoid ambiguity with primary expressions
+ bool has_upper = false;
+
+ // Flags that take an argument must appear last
+ bool needs_arg = false;
+
+ for (size_t i = 1; arg[i]; ++i) {
+ char c = arg[i];
+ if (c >= 'A' && c <= 'Z') {
+ has_upper = true;
+ }
+
+ if (needs_arg) {
+ return false;
+ }
+
+ const struct table_entry *entry = flag_lookup(c);
+ if (!entry || !entry->parse) {
+ return false;
+ }
+
+ if (entry->prefix) {
+ // The rest is the flag's argument
+ break;
+ }
+
+ needs_arg |= entry->needs_arg;
+ }
+
+ return has_upper;
+}
+
+/** Parse a multi-flag argument. */
+static struct bfs_expr *parse_flag_group(struct bfs_parser *parser) {
+ struct bfs_expr *expr = NULL;
+
+ char **start = parser->argv;
+ char **end = start;
+ const char *arg = start[0];
+
+ for (size_t i = 1; arg[i]; ++i) {
+ parser->argv = start;
+
+ const struct table_entry *entry = flag_lookup(arg[i]);
+ expr = entry->parse(parser, entry->arg1, entry->arg2);
+
+ if (parser->argv > end) {
+ end = parser->argv;
+ }
+
+ if (!expr || entry->prefix) {
+ break;
+ }
+ }
+
+ if (expr) {
+ bfs_assert(parser->argv == end, "Didn't eat enough tokens");
+ }
+
+ return expr;
+}
+
+/** Search for a fuzzy match in the parse table. */
+static const struct table_entry *table_lookup_fuzzy(const char *arg) {
+ const struct table_entry *best = NULL;
+ int best_dist = INT_MAX;
+
+ for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
+ int dist = typo_distance(arg, entry->arg);
+ if (!best || dist < best_dist) {
+ best = entry;
+ best_dist = dist;
+ }
+ }
+
+ return best;
+}
+
+/**
+ * PRIMARY : OPTION
+ * | TEST
+ * | ACTION
+ */
+static struct bfs_expr *parse_primary(struct bfs_parser *parser) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ // Paths are already skipped at this point
+ const char *arg = parser->argv[0];
+
+ if (arg[0] != '-') {
+ goto unexpected;
+ }
+
+ const struct table_entry *match = table_lookup(arg);
+ if (match) {
+ if (match->parse) {
+ goto matched;
+ } else {
+ goto unexpected;
+ }
+ }
+
+ if (is_flag_group(arg)) {
+ return parse_flag_group(parser);
+ }
+
+ match = table_lookup_fuzzy(arg);
+
+ CFILE *cerr = ctx->cerr;
+ parse_error(parser, "Unknown argument; did you mean ");
+ switch (match->kind) {
+ case BFS_FLAG:
+ cfprintf(cerr, "${cyn}%s${rs}?", match->arg);
+ break;
+ case BFS_OPERATOR:
+ cfprintf(cerr, "${red}%s${rs}?", match->arg);
+ break;
+ default:
+ cfprintf(cerr, "${blu}%s${rs}?", match->arg);
+ break;
+ }
+
+ if (!ctx->interactive || !match->parse) {
+ fprintf(stderr, "\n");
+ goto unmatched;
+ }
+
+ fprintf(stderr, " ");
+ if (ynprompt() <= 0) {
+ goto unmatched;
+ }
+
+ fprintf(stderr, "\n");
+ parser->argv[0] = match->arg;
+
+matched:
+ return match->parse(parser, match->arg1, match->arg2);
+
+unmatched:
+ return NULL;
+
+unexpected:
+ parse_error(parser, "Expected a predicate.\n");
+ return NULL;
+}
+
+/**
+ * FACTOR : "(" EXPR ")"
+ * | "!" FACTOR | "-not" FACTOR
+ * | "-exclude" FACTOR
+ * | PRIMARY
+ */
+static struct bfs_expr *parse_factor(struct bfs_parser *parser) {
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ const char *arg = parser->argv[0];
+ if (!arg) {
+ parse_argv_error(parser, parser->last_arg, 1, "Expression terminated prematurely here.\n");
+ return NULL;
+ }
+
+ if (strcmp(arg, "(") == 0) {
+ parser_advance(parser, BFS_OPERATOR, 1);
+
+ struct bfs_expr *expr = parse_expr(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ arg = parser->argv[0];
+ if (!arg || strcmp(arg, ")") != 0) {
+ parse_argv_error(parser, parser->last_arg, 1, "Expected a ${red})${rs}.\n");
+ return NULL;
+ }
+
+ parser_advance(parser, BFS_OPERATOR, 1);
+ return expr;
+ } else if (strcmp(arg, "-exclude") == 0) {
+ if (parser->excluding) {
+ parse_error(parser, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg);
+ return NULL;
+ }
+
+ char **argv = parser_advance(parser, BFS_OPERATOR, 1);
+ parser->excluding = true;
+
+ struct bfs_expr *factor = parse_factor(parser);
+ if (!factor) {
+ return NULL;
+ }
+
+ parser->excluding = false;
+
+ bfs_expr_append(parser->ctx->exclude, factor);
+ return parse_new_expr(parser, eval_true, parser->argv - argv, argv, BFS_OPERATOR);
+ } else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) {
+ char **argv = parser_advance(parser, BFS_OPERATOR, 1);
+
+ struct bfs_expr *factor = parse_factor(parser);
+ if (!factor) {
+ return NULL;
+ }
+
+ return new_unary_expr(parser, eval_not, factor, argv);
+ } else {
+ return parse_primary(parser);
+ }
+}
+
+/**
+ * TERM : FACTOR
+ * | TERM FACTOR
+ * | TERM "-a" FACTOR
+ * | TERM "-and" FACTOR
+ */
+static struct bfs_expr *parse_term(struct bfs_parser *parser) {
+ struct bfs_expr *term = parse_factor(parser);
+
+ while (term) {
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ const char *arg = parser->argv[0];
+ if (!arg) {
+ break;
+ }
+
+ if (strcmp(arg, "-o") == 0 || strcmp(arg, "-or") == 0
+ || strcmp(arg, ",") == 0
+ || strcmp(arg, ")") == 0) {
+ break;
+ }
+
+ char **argv = &fake_and_arg;
+ if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) {
+ argv = parser_advance(parser, BFS_OPERATOR, 1);
+ }
+
+ struct bfs_expr *lhs = term;
+ struct bfs_expr *rhs = parse_factor(parser);
+ if (!rhs) {
+ return NULL;
+ }
+
+ term = new_binary_expr(parser, eval_and, lhs, rhs, argv);
+ }
+
+ return term;
+}
+
+/**
+ * CLAUSE : TERM
+ * | CLAUSE "-o" TERM
+ * | CLAUSE "-or" TERM
+ */
+static struct bfs_expr *parse_clause(struct bfs_parser *parser) {
+ struct bfs_expr *clause = parse_term(parser);
+
+ while (clause) {
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ const char *arg = parser->argv[0];
+ if (!arg) {
+ break;
+ }
+
+ if (strcmp(arg, "-o") != 0 && strcmp(arg, "-or") != 0) {
+ break;
+ }
+
+ char **argv = parser_advance(parser, BFS_OPERATOR, 1);
+
+ struct bfs_expr *lhs = clause;
+ struct bfs_expr *rhs = parse_term(parser);
+ if (!rhs) {
+ return NULL;
+ }
+
+ clause = new_binary_expr(parser, eval_or, lhs, rhs, argv);
+ }
+
+ return clause;
+}
+
+/**
+ * EXPR : CLAUSE
+ * | EXPR "," CLAUSE
+ */
+static struct bfs_expr *parse_expr(struct bfs_parser *parser) {
+ struct bfs_expr *expr = parse_clause(parser);
+
+ while (expr) {
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ const char *arg = parser->argv[0];
+ if (!arg) {
+ break;
+ }
+
+ if (strcmp(arg, ",") != 0) {
+ break;
+ }
+
+ char **argv = parser_advance(parser, BFS_OPERATOR, 1);
+
+ struct bfs_expr *lhs = expr;
+ struct bfs_expr *rhs = parse_clause(parser);
+ if (!rhs) {
+ return NULL;
+ }
+
+ expr = new_binary_expr(parser, eval_comma, lhs, rhs, argv);
+ }
+
+ return expr;
+}
+
+/** Handle -files0-from after parsing. */
+static int parse_files0_roots(struct bfs_parser *parser) {
+ const struct bfs_ctx *ctx = parser->ctx;
+ const struct bfs_expr *expr = parser->files0_expr;
+
+ if (ctx->npaths > 0) {
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, expr->argv, expr->argc, highlight);
+
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (ctx->kinds[i] == BFS_PATH) {
+ highlight[i] = true;
+ }
+ }
+
+ bfs_argv_error(ctx, highlight);
+ bfs_error(ctx, "Cannot combine %pX with explicit root paths.\n", expr);
+ return -1;
+ }
+
+ const char *from = expr->argv[1];
+
+ FILE *file;
+ if (strcmp(from, "-") == 0) {
+ if (!consume_stdin(parser, expr)) {
+ return -1;
+ }
+ file = stdin;
+ } else {
+ file = xfopen(from, O_RDONLY | O_CLOEXEC);
+ }
+ if (!file) {
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ return -1;
+ }
+
+ while (true) {
+ char *path = xgetdelim(file, '\0');
+ if (!path) {
+ if (errno) {
+ goto fail;
+ } else {
+ break;
+ }
+ }
+
+ int ret = parse_root(parser, path);
+ free(path);
+ if (ret != 0) {
+ goto fail;
+ }
+ }
+
+ if (file != stdin) {
+ fclose(file);
+ }
+
+ return 0;
+
+fail:
+ if (file != stdin) {
+ fclose(file);
+ }
+ return -1;
+}
+
+/**
+ * Parse the top-level expression.
+ */
+static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
+ struct bfs_ctx *ctx = parser->ctx;
+
+ if (skip_paths(parser) != 0) {
+ return NULL;
+ }
+
+ struct bfs_expr *expr;
+ if (parser->argv[0]) {
+ expr = parse_expr(parser);
+ } else {
+ expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg, BFS_TEST);
+ }
+ if (!expr) {
+ return NULL;
+ }
+
+ if (parser->argv[0]) {
+ parse_error(parser, "Unexpected argument.\n");
+ return NULL;
+ }
+
+ if (parser->files0_expr) {
+ if (parse_files0_roots(parser) != 0) {
+ return NULL;
+ }
+ } else if (ctx->npaths == 0) {
+ if (parse_root(parser, ".") != 0) {
+ return NULL;
+ }
+ }
+
+ if (parser->implicit_print) {
+ const struct bfs_expr *limit = parser->limit_expr;
+ if (limit) {
+ parse_expr_error(parser, limit,
+ "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n",
+ limit, limit);
+ return NULL;
+ }
+
+ struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg, BFS_ACTION);
+ if (!print) {
+ return NULL;
+ }
+ init_print_expr(parser, print);
+
+ expr = new_binary_expr(parser, eval_and, expr, print, &fake_and_arg);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ if (parser->mount_expr && parser->xdev_expr) {
+ parse_conflict_warning(parser, parser->mount_expr, parser->xdev_expr,
+ "%px is redundant in the presence of %px.\n\n",
+ parser->xdev_expr, parser->mount_expr);
+ }
+
+ if (ctx->warn && parser->depth_expr && parser->prune_expr) {
+ parse_conflict_warning(parser, parser->depth_expr, parser->prune_expr,
+ "%px does not work in the presence of %px.\n",
+ parser->prune_expr, parser->depth_expr);
+
+ if (ctx->interactive) {
+ bfs_warning(ctx, "Do you want to continue? ");
+ if (ynprompt() <= 0) {
+ return NULL;
+ }
+ }
+
+ fprintf(stderr, "\n");
+ }
+
+ return expr;
+}
+
+static const char *bftw_strategy_name(enum bftw_strategy strategy) {
+ switch (strategy) {
+ case BFTW_BFS:
+ return "bfs";
+ case BFTW_DFS:
+ return "dfs";
+ case BFTW_IDS:
+ return "ids";
+ case BFTW_EDS:
+ return "eds";
+ }
+
+ bfs_bug("Invalid strategy");
+ return "???";
+}
+
+static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag, const struct bfs_expr *expr, int indent, int rparens) {
+ bfs_debug_prefix(ctx, flag);
+
+ for (int i = 0; i < indent; ++i) {
+ cfprintf(ctx->cerr, " ");
+ }
+
+ bool close = true;
+
+ if (bfs_expr_is_parent(expr)) {
+ if (SLIST_EMPTY(&expr->children)) {
+ cfprintf(ctx->cerr, "(${red}%s${rs}", expr->argv[0]);
+ ++rparens;
+ } else {
+ cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]);
+ for_expr (child, expr) {
+ int parens = child->next ? 0 : rparens + 1;
+ dump_expr_multiline(ctx, flag, child, indent + 1, parens);
+ }
+ close = false;
+ }
+ } else {
+ if (flag == DEBUG_RATES) {
+ cfprintf(ctx->cerr, "%pE", expr);
+ } else {
+ cfprintf(ctx->cerr, "%pe", expr);
+ }
+ }
+
+ if (close) {
+ for (int i = 0; i < rparens; ++i) {
+ cfprintf(ctx->cerr, ")");
+ }
+ cfprintf(ctx->cerr, "\n");
+ }
+}
+
+void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
+ if (!bfs_debug_prefix(ctx, flag)) {
+ return;
+ }
+
+ CFILE *cerr = ctx->cerr;
+
+ cfprintf(cerr, "${ex}%s${rs}", ctx->argv[0]);
+
+ if (ctx->flags & BFTW_FOLLOW_ALL) {
+ cfprintf(cerr, " ${cyn}-L${rs}");
+ } else if (ctx->flags & BFTW_FOLLOW_ROOTS) {
+ cfprintf(cerr, " ${cyn}-H${rs}");
+ } else {
+ cfprintf(cerr, " ${cyn}-P${rs}");
+ }
+
+ if (ctx->xargs_safe) {
+ cfprintf(cerr, " ${cyn}-X${rs}");
+ }
+
+ if (ctx->flags & BFTW_SORT) {
+ cfprintf(cerr, " ${cyn}-s${rs}");
+ }
+
+ cfprintf(cerr, " ${cyn}-j${bld}%d${rs}", ctx->threads);
+
+ if (ctx->optlevel != 3) {
+ cfprintf(cerr, " ${cyn}-O${bld}%d${rs}", ctx->optlevel);
+ }
+
+ cfprintf(cerr, " ${cyn}-S${rs} ${bld}%s${rs}", bftw_strategy_name(ctx->strategy));
+
+ enum debug_flags debug = ctx->debug;
+ if (debug == DEBUG_ALL) {
+ cfprintf(cerr, " ${cyn}-D${rs} ${bld}all${rs}");
+ } else if (debug) {
+ cfprintf(cerr, " ${cyn}-D${rs} ");
+ for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) {
+ if (debug & i) {
+ cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i));
+ debug ^= i;
+ if (debug) {
+ cfprintf(cerr, ",");
+ }
+ }
+ }
+ }
+
+ for (size_t i = 0; i < ctx->npaths; ++i) {
+ const char *path = ctx->paths[i];
+ char c = path[0];
+ if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') {
+ cfprintf(cerr, " ${cyn}-f${rs}");
+ }
+ cfprintf(cerr, " ${mag}%pq${rs}", path);
+ }
+
+ if (ctx->cout->colors) {
+ cfprintf(cerr, " ${blu}-color${rs}");
+ } else {
+ cfprintf(cerr, " ${blu}-nocolor${rs}");
+ }
+ if (ctx->flags & BFTW_POST_ORDER) {
+ cfprintf(cerr, " ${blu}-depth${rs}");
+ }
+ if (ctx->ignore_races) {
+ cfprintf(cerr, " ${blu}-ignore_readdir_race${rs}");
+ }
+ if (ctx->mindepth != 0) {
+ cfprintf(cerr, " ${blu}-mindepth${rs} ${bld}%d${rs}", ctx->mindepth);
+ }
+ if (ctx->maxdepth != INT_MAX) {
+ cfprintf(cerr, " ${blu}-maxdepth${rs} ${bld}%d${rs}", ctx->maxdepth);
+ }
+ if (ctx->flags & BFTW_SKIP_MOUNTS) {
+ cfprintf(cerr, " ${blu}-mount${rs}");
+ }
+ if (ctx->status) {
+ cfprintf(cerr, " ${blu}-status${rs}");
+ }
+ if (ctx->unique) {
+ cfprintf(cerr, " ${blu}-unique${rs}");
+ }
+ if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) {
+ cfprintf(cerr, " ${blu}-xdev${rs}");
+ }
+
+ fputs("\n", stderr);
+
+ bfs_debug(ctx, flag, "(${red}-exclude${rs}\n");
+ dump_expr_multiline(ctx, flag, ctx->exclude, 1, 1);
+
+ dump_expr_multiline(ctx, flag, ctx->expr, 0, 0);
+}
+
+/**
+ * Dump the estimated costs.
+ */
+static void dump_costs(const struct bfs_ctx *ctx) {
+ const struct bfs_expr *expr = ctx->expr;
+ bfs_debug(ctx, DEBUG_COST, " Cost: ~${ylw}%g${rs}\n", expr->cost);
+ bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0 * expr->probability);
+}
+
+struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
+ struct bfs_ctx *ctx = bfs_ctx_new();
+ if (!ctx) {
+ perror("bfs_ctx_new()");
+ goto fail;
+ }
+
+ static char *default_argv[] = {BFS_COMMAND, NULL};
+ if (argc < 1) {
+ argc = 1;
+ argv = default_argv;
+ }
+
+ ctx->argc = argc;
+ ctx->argv = xmemdup(argv, sizeof_array(char *, argc + 1));
+ if (!ctx->argv) {
+ perror("xmemdup()");
+ goto fail;
+ }
+
+ ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc);
+ if (!ctx->kinds) {
+ perror("zalloc()");
+ goto fail;
+ }
+
+ enum use_color use_color = COLOR_AUTO;
+ const char *no_color = getenv("NO_COLOR");
+ if (no_color && *no_color) {
+ // https://no-color.org/
+ use_color = COLOR_NEVER;
+ }
+
+ ctx->colors = parse_colors();
+ if (!ctx->colors) {
+ ctx->colors_error = errno;
+ }
+
+ ctx->cerr = cfwrap(stderr, use_color ? ctx->colors : NULL, false);
+ if (!ctx->cerr) {
+ perror("cfwrap()");
+ goto fail;
+ }
+
+ ctx->cout = cfwrap(stdout, use_color ? ctx->colors : NULL, false);
+ if (!ctx->cout) {
+ bfs_perror(ctx, "cfwrap()");
+ goto fail;
+ }
+
+ if (!bfs_ctx_dedup(ctx, ctx->cout, NULL) || !bfs_ctx_dedup(ctx, ctx->cerr, NULL)) {
+ bfs_perror(ctx, "bfs_ctx_dedup()");
+ goto fail;
+ }
+
+ bool stdin_tty = isatty(STDIN_FILENO);
+ bool stdout_tty = isatty(STDOUT_FILENO);
+ bool stderr_tty = isatty(STDERR_FILENO);
+
+ if (getenv("POSIXLY_CORRECT")) {
+ ctx->posixly_correct = true;
+ } else {
+ ctx->warn = stdin_tty;
+ }
+ ctx->interactive = stdin_tty && stderr_tty;
+
+ struct bfs_parser parser = {
+ .ctx = ctx,
+ .argv = ctx->argv + 1,
+ .command = ctx->argv[0],
+ .regex_type = BFS_REGEX_POSIX_BASIC,
+ .stdout_tty = stdout_tty,
+ .use_color = use_color,
+ .implicit_print = true,
+ .just_info = false,
+ .excluding = false,
+ .last_arg = NULL,
+ .depth_expr = NULL,
+ .prune_expr = NULL,
+ .mount_expr = NULL,
+ .xdev_expr = NULL,
+ .stdin_expr = NULL,
+ .now = ctx->now,
+ };
+
+ ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg, BFS_OPERATOR);
+ if (!ctx->exclude) {
+ goto fail;
+ }
+
+ ctx->expr = parse_whole_expr(&parser);
+ if (!ctx->expr) {
+ if (parser.just_info) {
+ goto done;
+ } else {
+ goto fail;
+ }
+ }
+
+ if (parser.use_color == COLOR_AUTO && !ctx->colors) {
+ bfs_warning(ctx, "Error parsing $$LS_COLORS: %s.\n\n", xstrerror(ctx->colors_error));
+ }
+
+ if (bfs_optimize(ctx) != 0) {
+ if (errno != 0) {
+ bfs_perror(ctx, "bfs_optimize()");
+ }
+ goto fail;
+ }
+
+ if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) {
+ // We need bftw() to detect cycles unless -unique does it for us
+ ctx->flags |= BFTW_DETECT_CYCLES;
+ }
+
+ bfs_ctx_dump(ctx, DEBUG_TREE);
+ dump_costs(ctx);
+
+done:
+ return ctx;
+
+fail:
+ bfs_ctx_free(ctx);
+ return NULL;
+}
diff --git a/src/parse.h b/src/parse.h
new file mode 100644
index 0000000..fcc8234
--- /dev/null
+++ b/src/parse.h
@@ -0,0 +1,23 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * bfs command line parsing.
+ */
+
+#ifndef BFS_PARSE_H
+#define BFS_PARSE_H
+
+/**
+ * Parse the command line.
+ *
+ * @argc
+ * The number of arguments.
+ * @argv
+ * The arguments to parse.
+ * @return
+ * A new bfs context, or NULL on failure.
+ */
+struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]);
+
+#endif // BFS_PARSE_H
diff --git a/src/prelude.h b/src/prelude.h
new file mode 100644
index 0000000..de89a6c
--- /dev/null
+++ b/src/prelude.h
@@ -0,0 +1,130 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Praeludium.
+ *
+ * This header is automatically included in every translation unit, before any
+ * other headers, so it can set feature test macros[1][2]. This sets up our own
+ * mini-dialect of C, which includes
+ *
+ * - Standard C17 and POSIX.1 2024 features
+ * - Portable and platform-specific extensions
+ * - Convenience macros like `bool`, `alignof`, etc.
+ * - Common compiler extensions like __has_include()
+ *
+ * Further bfs-specific utilities are defined in "bfs.h".
+ *
+ * [1]: https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html
+ * [2]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html
+ */
+
+#ifndef BFS_PRELUDE_H
+#define BFS_PRELUDE_H
+
+// Feature test macros
+
+/**
+ * Linux and BSD handle _POSIX_C_SOURCE differently: on Linux, it enables POSIX
+ * interfaces that are not visible by default. On BSD, it also *disables* most
+ * extensions, giving a strict POSIX environment. Since we want the extensions,
+ * we don't set _POSIX_C_SOURCE.
+ */
+// #define _POSIX_C_SOURCE 202405L
+
+/** openat() etc. */
+#define _ATFILE_SOURCE 1
+
+/** BSD-derived extensions. */
+#define _BSD_SOURCE 1
+
+/** glibc successor to _BSD_SOURCE. */
+#define _DEFAULT_SOURCE 1
+
+/** GNU extensions. */
+#define _GNU_SOURCE 1
+
+/** Use 64-bit off_t. */
+#define _FILE_OFFSET_BITS 64
+
+/** Use 64-bit time_t. */
+#define _TIME_BITS 64
+
+/** macOS extensions. */
+#if __APPLE__
+# define _DARWIN_C_SOURCE 1
+#endif
+
+/** Solaris extensions. */
+#if __sun
+# define __EXTENSIONS__ 1
+// https://illumos.org/man/3C/getpwnam#standard-conforming
+# define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+
+/** QNX extensions. */
+#if __QNX__
+# define _QNX_SOURCE 1
+#endif
+
+// Get the convenience macros that became standard spellings in C23
+#if __STDC_VERSION__ < 202311L
+
+/** _Static_assert() => static_assert() */
+#include <assert.h>
+/** _Alignas(), _Alignof() => alignas(), alignof() */
+#include <stdalign.h>
+/** _Bool => bool, true, false */
+#include <stdbool.h>
+
+/**
+ * C23 deprecates `noreturn void` in favour of `[[noreturn]] void`, so we expose
+ * _noreturn instead with the other attributes in "bfs.h".
+ */
+// #include <stdnoreturn.h>
+
+/** Part of <threads.h>, but we don't use anything else from it. */
+#define thread_local _Thread_local
+
+#endif // !C23
+
+// Feature detection
+
+// https://clang.llvm.org/docs/LanguageExtensions.html#has-attribute
+#ifndef __has_attribute
+# define __has_attribute(attr) false
+#endif
+
+// https://clang.llvm.org/docs/LanguageExtensions.html#has-builtin
+#ifndef __has_builtin
+# define __has_builtin(builtin) false
+#endif
+
+// https://en.cppreference.com/w/c/language/attributes#Attribute_testing
+#ifndef __has_c_attribute
+# define __has_c_attribute(attr) false
+#endif
+
+// https://clang.llvm.org/docs/LanguageExtensions.html#has-feature-and-has-extension
+#ifndef __has_feature
+# define __has_feature(feat) false
+#endif
+
+// https://en.cppreference.com/w/c/preprocessor/include
+#ifndef __has_include
+# define __has_include(header) false
+#endif
+
+// Sanitizer macros (GCC defines these but Clang does not)
+
+#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__)
+# define __SANITIZE_ADDRESS__ true
+#endif
+#if __has_feature(memory_sanitizer) && !defined(__SANITIZE_MEMORY__)
+# define __SANITIZE_MEMORY__ true
+#endif
+#if __has_feature(thread_sanitizer) && !defined(__SANITIZE_THREAD__)
+# define __SANITIZE_THREAD__ true
+#endif
+
+#endif // BFS_PRELUDE_H
diff --git a/src/printf.c b/src/printf.c
new file mode 100644
index 0000000..30ec201
--- /dev/null
+++ b/src/printf.c
@@ -0,0 +1,965 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "printf.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "bftw.h"
+#include "color.h"
+#include "ctx.h"
+#include "diag.h"
+#include "dir.h"
+#include "dstring.h"
+#include "expr.h"
+#include "fsade.h"
+#include "mtab.h"
+#include "pwcache.h"
+#include "stat.h"
+
+#include <errno.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+struct bfs_fmt;
+
+/**
+ * A function implementing a printf directive.
+ */
+typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf);
+
+/**
+ * A single formatting directive like %f or %#4m.
+ */
+struct bfs_fmt {
+ /** The printing function to invoke. */
+ bfs_printf_fn *fn;
+ /** String data associated with this directive. */
+ dchar *str;
+ /** The stat field to print. */
+ enum bfs_stat_field stat_field;
+ /** Character data associated with this directive. */
+ char c;
+ /** Some data used by the directive. */
+ void *ptr;
+};
+
+/**
+ * An entire format string.
+ */
+struct bfs_printf {
+ /** An array of formatting directives. */
+ struct bfs_fmt *fmts;
+ /** The number of directives. */
+ size_t nfmts;
+};
+
+/** Print some text as-is. */
+static int bfs_printf_literal(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ size_t len = dstrlen(fmt->str);
+ if (fwrite(fmt->str, 1, len, cfile->file) == len) {
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+/** \c: flush */
+static int bfs_printf_flush(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ return fflush(cfile->file);
+}
+
+/** Check if we can safely colorize this directive. */
+static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
+ return cfile->colors && strcmp(fmt->str, "%s") == 0;
+}
+
+/**
+ * Print a value to a temporary buffer before formatting it.
+ */
+#define BFS_PRINTF_BUF(buf, format, ...) \
+ char buf[256]; \
+ int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \
+ bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
+ (void)ret
+
+/** Return a dynamic format string. */
+_format_arg(2)
+static const char *dyn_fmt(const char *str, const char *fake) {
+ bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
+ "Mismatched format specifiers: '%s' vs. '%s'", str, fake);
+ return str;
+}
+
+/** Wrapper for fprintf(). */
+_printf(3, 4)
+static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
+ va_list args;
+ va_start(args, fake);
+ int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args);
+ va_end(args);
+ return ret;
+}
+
+/** %a, %c, %t: ctime() */
+static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ // Not using ctime() itself because GNU find adds nanoseconds
+ static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+ static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
+ if (!ts) {
+ return -1;
+ }
+
+ struct tm tm;
+ if (!localtime_r(&ts->tv_sec, &tm)) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d",
+ days[tm.tm_wday],
+ months[tm.tm_mon],
+ tm.tm_mday,
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec,
+ 1900 + tm.tm_year);
+
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %A, %B/%W, %C, %T: strftime() */
+static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
+ if (!ts) {
+ return -1;
+ }
+
+ struct tm tm;
+ if (!localtime_r(&ts->tv_sec, &tm)) {
+ return -1;
+ }
+
+ int ret;
+ char buf[256];
+ char format[] = "% ";
+ switch (fmt->c) {
+ // Non-POSIX strftime() features
+ case '@':
+ ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec);
+ break;
+ case '+':
+ ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0",
+ 1900 + tm.tm_year,
+ tm.tm_mon + 1,
+ tm.tm_mday,
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec);
+ break;
+ case 'k':
+ ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour);
+ break;
+ case 'l':
+ ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11) % 12 + 1);
+ break;
+ case 's':
+ ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec);
+ break;
+ case 'S':
+ ret = snprintf(buf, sizeof(buf), "%.2d.%09ld0", tm.tm_sec, (long)ts->tv_nsec);
+ break;
+ case 'T':
+ ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0",
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec);
+ break;
+
+ // POSIX strftime() features
+ default:
+ format[1] = fmt->c;
+#if __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+ ret = strftime(buf, sizeof(buf), format, &tm);
+#if __GNUC__
+# pragma GCC diagnostic pop
+#endif
+ break;
+ }
+
+ bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf));
+ (void)ret;
+
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %b: blocks */
+static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512;
+ BFS_PRINTF_BUF(buf, "%ju", blocks);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %d: depth */
+static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth);
+}
+
+/** %D: device */
+static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %f: file name */
+static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
+ return cfprintf(cfile, "%pF", ftwbuf);
+ } else {
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff);
+ }
+}
+
+/** %F: file system type */
+static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ const char *type = bfs_fstype(fmt->ptr, statbuf);
+ if (!type) {
+ return -1;
+ }
+
+ return bfs_fprintf(cfile, fmt, "%s", type);
+}
+
+/** %G: gid */
+static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %g: group name */
+static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ struct bfs_groups *groups = fmt->ptr;
+ const struct group *grp = bfs_getgrgid(groups, statbuf->gid);
+ if (!grp) {
+ return bfs_printf_G(cfile, fmt, ftwbuf);
+ }
+
+ return bfs_fprintf(cfile, fmt, "%s", grp->gr_name);
+}
+
+/** %h: leading directories */
+static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ char *copy = NULL;
+ const char *buf;
+
+ if (ftwbuf->nameoff > 0) {
+ size_t len = ftwbuf->nameoff;
+ if (len > 1) {
+ --len;
+ }
+
+ buf = copy = strndup(ftwbuf->path, len);
+ } else if (ftwbuf->path[0] == '/') {
+ buf = "/";
+ } else {
+ buf = ".";
+ }
+
+ if (!buf) {
+ return -1;
+ }
+
+ int ret;
+ if (should_color(cfile, fmt)) {
+ ret = cfprintf(cfile, "${di}%pQ${rs}", buf);
+ } else {
+ ret = bfs_fprintf(cfile, fmt, "%s", buf);
+ }
+
+ free(copy);
+ return ret;
+}
+
+/** %H: current root */
+static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
+ if (ftwbuf->depth == 0) {
+ return cfprintf(cfile, "%pP", ftwbuf);
+ } else {
+ return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root);
+ }
+ } else {
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root);
+ }
+}
+
+/** %i: inode */
+static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %k: 1K blocks */
+static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024;
+ BFS_PRINTF_BUF(buf, "%ju", blocks);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %l: link target */
+static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ char *buf = NULL;
+ const char *target = "";
+
+ if (ftwbuf->type == BFS_LNK) {
+ if (should_color(cfile, fmt)) {
+ return cfprintf(cfile, "%pL", ftwbuf);
+ }
+
+ const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
+ size_t len = statbuf ? statbuf->size : 0;
+
+ target = buf = xreadlinkat(ftwbuf->at_fd, ftwbuf->at_path, len);
+ if (!target) {
+ return -1;
+ }
+ }
+
+ int ret = bfs_fprintf(cfile, fmt, "%s", target);
+ free(buf);
+ return ret;
+}
+
+/** %m: mode */
+static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777));
+}
+
+/** %M: symbolic mode */
+static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ char buf[11];
+ xstrmode(statbuf->mode, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %n: link count */
+static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %p: full path */
+static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
+ return cfprintf(cfile, "%pP", ftwbuf);
+ } else {
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path);
+ }
+}
+
+/** %P: path after root */
+static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ size_t offset = strlen(ftwbuf->root);
+ if (ftwbuf->path[offset] == '/') {
+ ++offset;
+ }
+
+ if (should_color(cfile, fmt)) {
+ if (ftwbuf->depth == 0) {
+ return 0;
+ }
+
+ struct BFTW copybuf = *ftwbuf;
+ copybuf.path += offset;
+ copybuf.nameoff -= offset;
+ return cfprintf(cfile, "%pP", &copybuf);
+ } else {
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset);
+ }
+}
+
+/** %s: size */
+static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %S: sparseness */
+static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ double sparsity;
+ if (statbuf->size == 0 && statbuf->blocks == 0) {
+ sparsity = 1.0;
+ } else {
+ sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size;
+ }
+ return bfs_fprintf(cfile, fmt, "%g", sparsity);
+}
+
+/** %U: uid */
+static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
+}
+
+/** %u: user name */
+static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
+ if (!statbuf) {
+ return -1;
+ }
+
+ struct bfs_users *users = fmt->ptr;
+ const struct passwd *pwd = bfs_getpwuid(users, statbuf->uid);
+ if (!pwd) {
+ return bfs_printf_U(cfile, fmt, ftwbuf);
+ }
+
+ return bfs_fprintf(cfile, fmt, "%s", pwd->pw_name);
+}
+
+static const char *bfs_printf_type(enum bfs_type type) {
+ const char *const names[] = {
+ [BFS_BLK] = "b",
+ [BFS_CHR] = "c",
+ [BFS_DIR] = "d",
+ [BFS_DOOR] = "D",
+ [BFS_FIFO] = "p",
+ [BFS_LNK] = "l",
+ [BFS_PORT] = "P",
+ [BFS_REG] = "f",
+ [BFS_SOCK] = "s",
+ [BFS_WHT] = "w",
+ };
+
+ const char *name = NULL;
+ if ((size_t)type < countof(names)) {
+ name = names[type];
+ }
+
+ return name ? name : "U";
+}
+
+/** %y: type */
+static int bfs_printf_y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ const char *type = bfs_printf_type(ftwbuf->type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
+}
+
+/** %Y: target type */
+static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ enum bfs_type type = bftw_type(ftwbuf, BFS_STAT_FOLLOW);
+ const char *str;
+
+ int error = 0;
+ if (type == BFS_ERROR) {
+ if (errno == ELOOP) {
+ str = "L";
+ } else if (errno_is_like(ENOENT)) {
+ str = "N";
+ } else {
+ str = "?";
+ error = errno;
+ }
+ } else {
+ str = bfs_printf_type(type);
+ }
+
+ int ret = bfs_fprintf(cfile, fmt, "%s", str);
+ if (error != 0) {
+ ret = -1;
+ errno = error;
+ }
+ return ret;
+}
+
+/** %Z: SELinux context */
+_maybe_unused
+static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ char *con = bfs_getfilecon(ftwbuf);
+ if (!con) {
+ return -1;
+ }
+
+ int ret = bfs_fprintf(cfile, fmt, "%s", con);
+ bfs_freecon(con);
+ return ret;
+}
+
+/**
+ * Append a literal string to the chain.
+ */
+static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal) {
+ if (dstrlen(*literal) == 0) {
+ return 0;
+ }
+
+ struct bfs_fmt *fmt = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
+ if (!fmt) {
+ bfs_perror(ctx, "RESERVE()");
+ return -1;
+ }
+
+ fmt->fn = bfs_printf_literal;
+ fmt->str = *literal;
+
+ *literal = dstralloc(0);
+ if (!*literal) {
+ bfs_perror(ctx, "dstralloc()");
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * Append a printf directive to the chain.
+ */
+static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal, struct bfs_fmt *fmt) {
+ if (append_literal(ctx, format, literal) != 0) {
+ return -1;
+ }
+
+ struct bfs_fmt *dest = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
+ if (!dest) {
+ bfs_perror(ctx, "RESERVE()");
+ return -1;
+ }
+
+ *dest = *fmt;
+ return 0;
+}
+
+int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format) {
+ expr->printf = ZALLOC(struct bfs_printf);
+ if (!expr->printf) {
+ bfs_perror(ctx, "zalloc()");
+ return -1;
+ }
+
+ dchar *literal = dstralloc(0);
+ if (!literal) {
+ bfs_perror(ctx, "dstralloc()");
+ goto error;
+ }
+
+ for (const char *i = format; *i; ++i) {
+ char c = *i;
+
+ if (c == '\\') {
+ c = *++i;
+
+ if (c >= '0' && c < '8') {
+ c = 0;
+ for (int j = 0; j < 3 && *i >= '0' && *i < '8'; ++i, ++j) {
+ c *= 8;
+ c += *i - '0';
+ }
+ --i;
+ goto one_char;
+ }
+
+ switch (c) {
+ case 'a': c = '\a'; break;
+ case 'b': c = '\b'; break;
+ case 'f': c = '\f'; break;
+ case 'n': c = '\n'; break;
+ case 'r': c = '\r'; break;
+ case 't': c = '\t'; break;
+ case 'v': c = '\v'; break;
+ case '\\': c = '\\'; break;
+
+ case 'c':
+ {
+ struct bfs_fmt fmt = {
+ .fn = bfs_printf_flush,
+ };
+ if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
+ goto error;
+ }
+ goto done;
+ }
+
+ case '\0':
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Incomplete escape sequence '\\'.\n");
+ goto error;
+
+ default:
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Unrecognized escape sequence '\\%c'.\n", c);
+ goto error;
+ }
+ } else if (c == '%') {
+ if (i[1] == '%') {
+ c = *++i;
+ goto one_char;
+ }
+
+ struct bfs_fmt fmt = {
+ .str = dstralloc(2),
+ };
+ if (!fmt.str) {
+ goto fmt_error;
+ }
+ if (dstrapp(&fmt.str, c) != 0) {
+ bfs_perror(ctx, "dstrapp()");
+ goto fmt_error;
+ }
+
+ const char *specifier = "s";
+
+ // Parse any flags
+ bool must_be_numeric = false;
+ while (true) {
+ c = *++i;
+
+ switch (c) {
+ case '#':
+ case '0':
+ case '+':
+ case ' ':
+ must_be_numeric = true;
+ _fallthrough;
+ case '-':
+ if (strchr(fmt.str, c)) {
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Duplicate flag '%c'.\n", c);
+ goto fmt_error;
+ }
+ if (dstrapp(&fmt.str, c) != 0) {
+ bfs_perror(ctx, "dstrapp()");
+ goto fmt_error;
+ }
+ continue;
+ }
+
+ break;
+ }
+
+ // Parse the field width
+ while (c >= '0' && c <= '9') {
+ if (dstrapp(&fmt.str, c) != 0) {
+ bfs_perror(ctx, "dstrapp()");
+ goto fmt_error;
+ }
+ c = *++i;
+ }
+
+ // Parse the precision
+ if (c == '.') {
+ do {
+ if (dstrapp(&fmt.str, c) != 0) {
+ bfs_perror(ctx, "dstrapp()");
+ goto fmt_error;
+ }
+ c = *++i;
+ } while (c >= '0' && c <= '9');
+ }
+
+ switch (c) {
+ case 'a':
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_ATIME;
+ break;
+ case 'b':
+ fmt.fn = bfs_printf_b;
+ break;
+ case 'c':
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_CTIME;
+ break;
+ case 'd':
+ fmt.fn = bfs_printf_d;
+ specifier = "jd";
+ break;
+ case 'D':
+ fmt.fn = bfs_printf_D;
+ break;
+ case 'f':
+ fmt.fn = bfs_printf_f;
+ break;
+ case 'F':
+ fmt.fn = bfs_printf_F;
+ fmt.ptr = (void *)bfs_ctx_mtab(ctx);
+ if (!fmt.ptr) {
+ int error = errno;
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Couldn't parse the mount table: %s.\n", xstrerror(error));
+ goto fmt_error;
+ }
+ break;
+ case 'g':
+ fmt.fn = bfs_printf_g;
+ fmt.ptr = ctx->groups;
+ break;
+ case 'G':
+ fmt.fn = bfs_printf_G;
+ break;
+ case 'h':
+ fmt.fn = bfs_printf_h;
+ break;
+ case 'H':
+ fmt.fn = bfs_printf_H;
+ break;
+ case 'i':
+ fmt.fn = bfs_printf_i;
+ break;
+ case 'k':
+ fmt.fn = bfs_printf_k;
+ break;
+ case 'l':
+ fmt.fn = bfs_printf_l;
+ break;
+ case 'm':
+ fmt.fn = bfs_printf_m;
+ specifier = "o";
+ break;
+ case 'M':
+ fmt.fn = bfs_printf_M;
+ break;
+ case 'n':
+ fmt.fn = bfs_printf_n;
+ break;
+ case 'p':
+ fmt.fn = bfs_printf_p;
+ break;
+ case 'P':
+ fmt.fn = bfs_printf_P;
+ break;
+ case 's':
+ fmt.fn = bfs_printf_s;
+ break;
+ case 'S':
+ fmt.fn = bfs_printf_S;
+ specifier = "g";
+ break;
+ case 't':
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_MTIME;
+ break;
+ case 'u':
+ fmt.fn = bfs_printf_u;
+ fmt.ptr = ctx->users;
+ break;
+ case 'U':
+ fmt.fn = bfs_printf_U;
+ break;
+ case 'w':
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_BTIME;
+ break;
+ case 'y':
+ fmt.fn = bfs_printf_y;
+ break;
+ case 'Y':
+ fmt.fn = bfs_printf_Y;
+ break;
+ case 'Z':
+#if BFS_CAN_CHECK_CONTEXT
+ fmt.fn = bfs_printf_Z;
+ break;
+#else
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Missing platform support for '%%%c'.\n", c);
+ goto fmt_error;
+#endif
+
+ case 'A':
+ fmt.stat_field = BFS_STAT_ATIME;
+ goto fmt_strftime;
+ case 'B':
+ case 'W':
+ fmt.stat_field = BFS_STAT_BTIME;
+ goto fmt_strftime;
+ case 'C':
+ fmt.stat_field = BFS_STAT_CTIME;
+ goto fmt_strftime;
+ case 'T':
+ fmt.stat_field = BFS_STAT_MTIME;
+ goto fmt_strftime;
+
+ fmt_strftime:
+ fmt.fn = bfs_printf_strftime;
+ c = *++i;
+ if (!c) {
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", fmt.str, i[-1]);
+ goto fmt_error;
+ } else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) {
+ fmt.c = c;
+ } else {
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Unrecognized time specifier '%%%c%c'.\n", i[-1], c);
+ goto fmt_error;
+ }
+ break;
+
+ case '\0':
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Incomplete format specifier '%s'.\n", fmt.str);
+ goto fmt_error;
+
+ default:
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Unrecognized format specifier '%%%c'.\n", c);
+ goto fmt_error;
+ }
+
+ if (must_be_numeric && strcmp(specifier, "s") == 0) {
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", fmt.str + 1, c);
+ goto fmt_error;
+ }
+
+ if (dstrcat(&fmt.str, specifier) != 0) {
+ bfs_perror(ctx, "dstrcat()");
+ goto fmt_error;
+ }
+
+ if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
+ goto fmt_error;
+ }
+
+ continue;
+
+ fmt_error:
+ dstrfree(fmt.str);
+ goto error;
+ }
+
+ one_char:
+ if (dstrapp(&literal, c) != 0) {
+ bfs_perror(ctx, "dstrapp()");
+ goto error;
+ }
+ }
+
+done:
+ if (append_literal(ctx, expr->printf, &literal) != 0) {
+ goto error;
+ }
+ dstrfree(literal);
+ return 0;
+
+error:
+ dstrfree(literal);
+ bfs_printf_free(expr->printf);
+ expr->printf = NULL;
+ return -1;
+}
+
+int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf) {
+ int ret = 0, error = 0;
+
+ for (size_t i = 0; i < format->nfmts; ++i) {
+ const struct bfs_fmt *fmt = &format->fmts[i];
+ if (fmt->fn(cfile, fmt, ftwbuf) < 0) {
+ ret = -1;
+ error = errno;
+ }
+ }
+
+ errno = error;
+ return ret;
+}
+
+void bfs_printf_free(struct bfs_printf *format) {
+ if (!format) {
+ return;
+ }
+
+ for (size_t i = 0; i < format->nfmts; ++i) {
+ dstrfree(format->fmts[i].str);
+ }
+ free(format->fmts);
+ free(format);
+}
diff --git a/src/printf.h b/src/printf.h
new file mode 100644
index 0000000..e8d862e
--- /dev/null
+++ b/src/printf.h
@@ -0,0 +1,55 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Implementation of -printf/-fprintf.
+ */
+
+#ifndef BFS_PRINTF_H
+#define BFS_PRINTF_H
+
+#include "color.h"
+
+struct BFTW;
+struct bfs_ctx;
+struct bfs_expr;
+
+/**
+ * A printf command, the result of parsing a single format string.
+ */
+struct bfs_printf;
+
+/**
+ * Parse a -printf format string.
+ *
+ * @ctx
+ * The bfs context.
+ * @expr
+ * The expression to fill in.
+ * @format
+ * The format string to parse.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format);
+
+/**
+ * Evaluate a parsed format string.
+ *
+ * @cfile
+ * The CFILE to print to.
+ * @format
+ * The parsed printf format.
+ * @ftwbuf
+ * The bftw() data for the current file.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf);
+
+/**
+ * Free a parsed format string.
+ */
+void bfs_printf_free(struct bfs_printf *format);
+
+#endif // BFS_PRINTF_H
diff --git a/src/pwcache.c b/src/pwcache.c
new file mode 100644
index 0000000..fa19dad
--- /dev/null
+++ b/src/pwcache.c
@@ -0,0 +1,219 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "pwcache.h"
+
+#include "alloc.h"
+#include "trie.h"
+
+#include <errno.h>
+#include <grp.h>
+#include <pwd.h>
+#include <stdlib.h>
+
+/** Represents cache hits for negative results. */
+static void *MISSING = &MISSING;
+
+/** Callback type for bfs_getent(). */
+typedef void *bfs_getent_fn(const void *key, void *ptr, size_t bufsize);
+
+/** Shared scaffolding for get{pw,gr}{nam,?id}_r(). */
+static void *bfs_getent(bfs_getent_fn *fn, const void *key, struct trie_leaf *leaf, struct varena *varena) {
+ if (leaf->value) {
+ errno = 0;
+ return leaf->value == MISSING ? NULL : leaf->value;
+ }
+
+ // _SC_GET{PW,GR}_R_SIZE_MAX tend to be fairly large (~1K). That's okay
+ // for temporary allocations, but for these long-lived ones, let's start
+ // with a smaller buffer.
+ size_t bufsize = 128;
+ void *ptr = varena_alloc(varena, bufsize);
+ if (!ptr) {
+ return NULL;
+ }
+
+ while (true) {
+ void *ret = fn(key, ptr, bufsize);
+ if (ret) {
+ leaf->value = ret;
+ return ret;
+ } else if (errno == 0) {
+ leaf->value = MISSING;
+ break;
+ } else if (errno == ERANGE) {
+ void *next = varena_grow(varena, ptr, &bufsize);
+ if (!next) {
+ break;
+ }
+ ptr = next;
+ } else {
+ break;
+ }
+ }
+
+ varena_free(varena, ptr, bufsize);
+ return NULL;
+}
+
+/**
+ * An arena-allocated struct passwd.
+ */
+struct bfs_passwd {
+ struct passwd pwd;
+ char buf[];
+};
+
+struct bfs_users {
+ /** bfs_passwd arena. */
+ struct varena varena;
+ /** A map from usernames to entries. */
+ struct trie by_name;
+ /** A map from UIDs to entries. */
+ struct trie by_uid;
+};
+
+struct bfs_users *bfs_users_new(void) {
+ struct bfs_users *users = ALLOC(struct bfs_users);
+ if (!users) {
+ return NULL;
+ }
+
+ VARENA_INIT(&users->varena, struct bfs_passwd, buf);
+ trie_init(&users->by_name);
+ trie_init(&users->by_uid);
+ return users;
+}
+
+/** bfs_getent() callback for getpwnam_r(). */
+static void *bfs_getpwnam_impl(const void *key, void *ptr, size_t bufsize) {
+ struct bfs_passwd *storage = ptr;
+
+ struct passwd *ret = NULL;
+ errno = getpwnam_r(key, &storage->pwd, storage->buf, bufsize, &ret);
+ return ret;
+}
+
+const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name) {
+ struct trie_leaf *leaf = trie_insert_str(&users->by_name, name);
+ if (!leaf) {
+ return NULL;
+ }
+
+ return bfs_getent(bfs_getpwnam_impl, name, leaf, &users->varena);
+}
+
+/** bfs_getent() callback for getpwuid_r(). */
+static void *bfs_getpwuid_impl(const void *key, void *ptr, size_t bufsize) {
+ const uid_t *uid = key;
+ struct bfs_passwd *storage = ptr;
+
+ struct passwd *ret = NULL;
+ errno = getpwuid_r(*uid, &storage->pwd, storage->buf, bufsize, &ret);
+ return ret;
+}
+
+const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid) {
+ struct trie_leaf *leaf = trie_insert_mem(&users->by_uid, &uid, sizeof(uid));
+ if (!leaf) {
+ return NULL;
+ }
+
+ return bfs_getent(bfs_getpwuid_impl, &uid, leaf, &users->varena);
+}
+
+void bfs_users_flush(struct bfs_users *users) {
+ trie_clear(&users->by_uid);
+ trie_clear(&users->by_name);
+ varena_clear(&users->varena);
+}
+
+void bfs_users_free(struct bfs_users *users) {
+ if (users) {
+ trie_destroy(&users->by_uid);
+ trie_destroy(&users->by_name);
+ varena_destroy(&users->varena);
+ free(users);
+ }
+}
+
+/**
+ * An arena-allocated struct group.
+ */
+struct bfs_group {
+ struct group grp;
+ char buf[];
+};
+
+struct bfs_groups {
+ /** bfs_group arena. */
+ struct varena varena;
+ /** A map from group names to entries. */
+ struct trie by_name;
+ /** A map from GIDs to entries. */
+ struct trie by_gid;
+};
+
+struct bfs_groups *bfs_groups_new(void) {
+ struct bfs_groups *groups = ALLOC(struct bfs_groups);
+ if (!groups) {
+ return NULL;
+ }
+
+ VARENA_INIT(&groups->varena, struct bfs_group, buf);
+ trie_init(&groups->by_name);
+ trie_init(&groups->by_gid);
+ return groups;
+}
+
+/** bfs_getent() callback for getgrnam_r(). */
+static void *bfs_getgrnam_impl(const void *key, void *ptr, size_t bufsize) {
+ struct bfs_group *storage = ptr;
+
+ struct group *ret = NULL;
+ errno = getgrnam_r(key, &storage->grp, storage->buf, bufsize, &ret);
+ return ret;
+}
+
+const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name) {
+ struct trie_leaf *leaf = trie_insert_str(&groups->by_name, name);
+ if (!leaf) {
+ return NULL;
+ }
+
+ return bfs_getent(bfs_getgrnam_impl, name, leaf, &groups->varena);
+}
+
+/** bfs_getent() callback for getgrgid_r(). */
+static void *bfs_getgrgid_impl(const void *key, void *ptr, size_t bufsize) {
+ const gid_t *gid = key;
+ struct bfs_group *storage = ptr;
+
+ struct group *ret = NULL;
+ errno = getgrgid_r(*gid, &storage->grp, storage->buf, bufsize, &ret);
+ return ret;
+}
+
+const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid) {
+ struct trie_leaf *leaf = trie_insert_mem(&groups->by_gid, &gid, sizeof(gid));
+ if (!leaf) {
+ return NULL;
+ }
+
+ return bfs_getent(bfs_getgrgid_impl, &gid, leaf, &groups->varena);
+}
+
+void bfs_groups_flush(struct bfs_groups *groups) {
+ trie_clear(&groups->by_gid);
+ trie_clear(&groups->by_name);
+ varena_clear(&groups->varena);
+}
+
+void bfs_groups_free(struct bfs_groups *groups) {
+ if (groups) {
+ trie_destroy(&groups->by_gid);
+ trie_destroy(&groups->by_name);
+ varena_destroy(&groups->varena);
+ free(groups);
+ }
+}
diff --git a/src/pwcache.h b/src/pwcache.h
new file mode 100644
index 0000000..d7c602d
--- /dev/null
+++ b/src/pwcache.h
@@ -0,0 +1,124 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A caching wrapper for /etc/{passwd,group}.
+ */
+
+#ifndef BFS_PWCACHE_H
+#define BFS_PWCACHE_H
+
+#include <grp.h>
+#include <pwd.h>
+
+/**
+ * A user cache.
+ */
+struct bfs_users;
+
+/**
+ * Create a user cache.
+ *
+ * @return
+ * A new user cache, or NULL on failure.
+ */
+struct bfs_users *bfs_users_new(void);
+
+/**
+ * Get a user entry by name.
+ *
+ * @users
+ * The user cache.
+ * @name
+ * The username to look up.
+ * @return
+ * The matching user, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
+ */
+const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name);
+
+/**
+ * Get a user entry by ID.
+ *
+ * @users
+ * The user cache.
+ * @uid
+ * The ID to look up.
+ * @return
+ * The matching user, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
+ */
+const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid);
+
+/**
+ * Flush a user cache.
+ *
+ * @users
+ * The cache to flush.
+ */
+void bfs_users_flush(struct bfs_users *users);
+
+/**
+ * Free a user cache.
+ *
+ * @users
+ * The user cache to free.
+ */
+void bfs_users_free(struct bfs_users *users);
+
+/**
+ * A group cache.
+ */
+struct bfs_groups;
+
+/**
+ * Create a group cache.
+ *
+ * @return
+ * A new group cache, or NULL on failure.
+ */
+struct bfs_groups *bfs_groups_new(void);
+
+/**
+ * Get a group entry by name.
+ *
+ * @groups
+ * The group cache.
+ * @name
+ * The group name to look up.
+ * @return
+ * The matching group, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
+ */
+const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name);
+
+/**
+ * Get a group entry by ID.
+ *
+ * @groups
+ * The group cache.
+ * @uid
+ * The ID to look up.
+ * @return
+ * The matching group, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
+ */
+const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid);
+
+/**
+ * Flush a group cache.
+ *
+ * @groups
+ * The cache to flush.
+ */
+void bfs_groups_flush(struct bfs_groups *groups);
+
+/**
+ * Free a group cache.
+ *
+ * @groups
+ * The group cache to free.
+ */
+void bfs_groups_free(struct bfs_groups *groups);
+
+#endif // BFS_PWCACHE_H
diff --git a/src/sanity.h b/src/sanity.h
new file mode 100644
index 0000000..be77eef
--- /dev/null
+++ b/src/sanity.h
@@ -0,0 +1,94 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Sanitizer interface.
+ */
+
+#ifndef BFS_SANITY_H
+#define BFS_SANITY_H
+
+#include <stddef.h>
+
+// Call macro(ptr, size) or macro(ptr, sizeof(*ptr))
+#define SANITIZE_CALL(...) \
+ SANITIZE_CALL_(__VA_ARGS__, )
+
+#define SANITIZE_CALL_(macro, ptr, ...) \
+ SANITIZE_CALL__(macro, ptr, __VA_ARGS__ sizeof(*(ptr)), )
+
+#define SANITIZE_CALL__(macro, ptr, size, ...) \
+ macro(ptr, size)
+
+#if __SANITIZE_ADDRESS__
+# include <sanitizer/asan_interface.h>
+
+/**
+ * sanitize_alloc(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as allocated.
+ */
+#define sanitize_alloc(...) SANITIZE_CALL(__asan_unpoison_memory_region, __VA_ARGS__)
+
+/**
+ * sanitize_free(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as free.
+ */
+#define sanitize_free(...) SANITIZE_CALL(__asan_poison_memory_region, __VA_ARGS__)
+
+/**
+ * Adjust the size of an allocated region, for things like dynamic arrays.
+ *
+ * @ptr
+ * The memory region.
+ * @old
+ * The previous usable size of the region.
+ * @new
+ * The new usable size of the region.
+ * @cap
+ * The total allocated capacity of the region.
+ */
+static inline void sanitize_resize(const void *ptr, size_t old, size_t new, size_t cap) {
+ const char *beg = ptr;
+ __sanitizer_annotate_contiguous_container(beg, beg + cap, beg + old, beg + new);
+}
+
+#else
+# define sanitize_alloc(...) ((void)0)
+# define sanitize_free(...) ((void)0)
+# define sanitize_resize(ptr, old, new, cap) ((void)0)
+#endif
+
+#if __SANITIZE_MEMORY__
+# include <sanitizer/msan_interface.h>
+
+/**
+ * sanitize_init(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as initialized.
+ */
+#define sanitize_init(...) SANITIZE_CALL(__msan_unpoison, __VA_ARGS__)
+
+/**
+ * sanitize_uninit(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as uninitialized.
+ */
+#define sanitize_uninit(...) SANITIZE_CALL(__msan_allocated_memory, __VA_ARGS__)
+
+#else
+# define sanitize_init(...) ((void)0)
+# define sanitize_uninit(...) ((void)0)
+#endif
+
+/**
+ * Initialize a variable, unless sanitizers would detect uninitialized uses.
+ */
+#if __SANITIZE_MEMORY__
+# define uninit(value)
+#else
+# define uninit(value) = value
+#endif
+
+#endif // BFS_SANITY_H
diff --git a/src/sighook.c b/src/sighook.c
new file mode 100644
index 0000000..a87bed5
--- /dev/null
+++ b/src/sighook.c
@@ -0,0 +1,692 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Dynamic (un)registration of signal handlers.
+ *
+ * Because signal handlers can interrupt any thread at an arbitrary point, they
+ * must be lock-free or risk deadlock. Therefore, we implement the global table
+ * of signal "hooks" with a simple read-copy-update (RCU) scheme. Readers get a
+ * reference-counted pointer (struct arc) to the table in a lock-free way, and
+ * release the reference count when finished.
+ *
+ * Updates are managed by struct rcu, which has two slots: one active and one
+ * inactive. Readers acquire a reference to the active slot. A single writer
+ * can safely update it by initializing the inactive slot, atomically swapping
+ * the slots, and waiting for the reference count of the newly inactive slot to
+ * drop to zero. Once it does, the old pointer can be safely freed.
+ */
+
+#include "sighook.h"
+
+#include "alloc.h"
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "thread.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#if __linux__
+# include <sys/syscall.h>
+#endif
+
+// NetBSD opens a file descriptor for each sem_init()
+#if defined(_POSIX_SEMAPHORES) && !__NetBSD__
+# define BFS_POSIX_SEMAPHORES _POSIX_SEMAPHORES
+#else
+# define BFS_POSIX_SEMAPHORES (-1)
+#endif
+
+#if BFS_POSIX_SEMAPHORES >= 0
+# include <semaphore.h>
+#endif
+
+/**
+ * An atomically reference-counted pointer.
+ */
+struct arc {
+ /** The current reference count (0 means empty). */
+ atomic size_t refs;
+ /** The reference itself. */
+ void *ptr;
+
+#if BFS_POSIX_SEMAPHORES >= 0
+ /** A semaphore for arc_wait(). */
+ sem_t sem;
+ /** sem_init() result. */
+ int sem_status;
+#endif
+};
+
+/** Initialize an arc. */
+static void arc_init(struct arc *arc) {
+ bfs_verify(atomic_is_lock_free(&arc->refs));
+
+ atomic_init(&arc->refs, 0);
+ arc->ptr = NULL;
+
+#if BFS_POSIX_SEMAPHORES >= 0
+ if (sysoption(SEMAPHORES) > 0) {
+ arc->sem_status = sem_init(&arc->sem, false, 0);
+ } else {
+ arc->sem_status = -1;
+ }
+#endif
+}
+
+/** Get the current refcount. */
+static size_t arc_refs(const struct arc *arc) {
+ return load(&arc->refs, relaxed);
+}
+
+/** Set the pointer in an empty arc. */
+static void arc_set(struct arc *arc, void *ptr) {
+ bfs_assert(arc_refs(arc) == 0);
+ bfs_assert(ptr);
+
+ arc->ptr = ptr;
+ store(&arc->refs, 1, release);
+}
+
+/** Acquire a reference. */
+static void *arc_get(struct arc *arc) {
+ size_t refs = arc_refs(arc);
+ do {
+ if (refs < 1) {
+ return NULL;
+ }
+ } while (!compare_exchange_weak(&arc->refs, &refs, refs + 1, acquire, relaxed));
+
+ return arc->ptr;
+}
+
+/** Release a reference. */
+static void arc_put(struct arc *arc) {
+ size_t refs = fetch_sub(&arc->refs, 1, release);
+
+ if (refs == 1) {
+#if BFS_POSIX_SEMAPHORES >= 0
+ if (arc->sem_status == 0 && sem_post(&arc->sem) != 0) {
+ abort();
+ }
+#endif
+ }
+}
+
+/** Wait on the semaphore. */
+static int arc_sem_wait(struct arc *arc) {
+#if BFS_POSIX_SEMAPHORES >= 0
+ if (arc->sem_status == 0) {
+ while (sem_wait(&arc->sem) != 0) {
+ bfs_everify(errno == EINTR, "sem_wait()");
+ }
+ return 0;
+ }
+#endif
+
+ return -1;
+}
+
+/** Wait for all references to be released. */
+static void *arc_wait(struct arc *arc) {
+ size_t refs = fetch_sub(&arc->refs, 1, relaxed);
+ bfs_assert(refs > 0);
+
+ --refs;
+ while (refs > 0) {
+ if (arc_sem_wait(arc) == 0) {
+ bfs_assert(arc_refs(arc) == 0);
+ // sem_wait() provides enough ordering, so we can skip the fence
+ goto done;
+ }
+
+ // Some platforms (like macOS) don't support unnamed semaphores,
+ // but we can always busy-wait
+ spin_loop();
+ refs = arc_refs(arc);
+ }
+
+ thread_fence(&arc->refs, acquire);
+
+done:;
+ void *ptr = arc->ptr;
+ arc->ptr = NULL;
+ return ptr;
+}
+
+/** Destroy an arc. */
+static void arc_destroy(struct arc *arc) {
+ bfs_assert(arc_refs(arc) == 0);
+
+#if BFS_POSIX_SEMAPHORES >= 0
+ if (arc->sem_status == 0) {
+ bfs_everify(sem_destroy(&arc->sem) == 0, "sem_destroy()");
+ }
+#endif
+}
+
+/**
+ * A simple read-copy-update memory reclamation scheme.
+ */
+struct rcu {
+ /** The currently active slot. */
+ atomic size_t active;
+ /** The two slots. */
+ struct arc slots[2];
+};
+
+/** Sentinel value for RCU, since arc uses NULL already. */
+static void *RCU_NULL = &RCU_NULL;
+
+/** Map NULL -> RCU_NULL. */
+static void *rcu_encode(void *ptr) {
+ return ptr ? ptr : RCU_NULL;
+}
+
+/** Map RCU_NULL -> NULL. */
+static void *rcu_decode(void *ptr) {
+ bfs_assert(ptr != NULL);
+ return ptr == RCU_NULL ? NULL : ptr;
+}
+
+/** Initialize an RCU block. */
+static void rcu_init(struct rcu *rcu, void *ptr) {
+ bfs_verify(atomic_is_lock_free(&rcu->active));
+
+ atomic_init(&rcu->active, 0);
+ arc_init(&rcu->slots[0]);
+ arc_init(&rcu->slots[1]);
+ arc_set(&rcu->slots[0], rcu_encode(ptr));
+}
+
+/** Get the active slot. */
+static struct arc *rcu_active(struct rcu *rcu) {
+ size_t i = load(&rcu->active, relaxed);
+ return &rcu->slots[i];
+}
+
+/** Destroy an RCU block. */
+static void rcu_destroy(struct rcu *rcu) {
+ arc_wait(rcu_active(rcu));
+ arc_destroy(&rcu->slots[1]);
+ arc_destroy(&rcu->slots[0]);
+}
+
+/** Read an RCU-protected pointer. */
+static void *rcu_read(struct rcu *rcu, struct arc **slot) {
+ while (true) {
+ *slot = rcu_active(rcu);
+ void *ptr = arc_get(*slot);
+ if (ptr) {
+ return rcu_decode(ptr);
+ }
+ // Otherwise, the other slot became active; retry
+ }
+}
+
+/** Get the RCU-protected pointer without acquiring a reference. */
+static void *rcu_peek(struct rcu *rcu) {
+ struct arc *arc = rcu_active(rcu);
+ return rcu_decode(arc->ptr);
+}
+
+/** Update an RCU-protected pointer, and return the old one. */
+static void *rcu_update(struct rcu *rcu, void *ptr) {
+ size_t i = load(&rcu->active, relaxed);
+ struct arc *prev = &rcu->slots[i];
+
+ size_t j = i ^ 1;
+ struct arc *next = &rcu->slots[j];
+
+ arc_set(next, rcu_encode(ptr));
+ store(&rcu->active, j, relaxed);
+ return rcu_decode(arc_wait(prev));
+}
+
+/**
+ * An RCU-protected linked list.
+ */
+struct rcu_list {
+ /** The first node in the list. */
+ struct rcu head;
+ /** &last->next */
+ struct rcu *tail;
+};
+
+/**
+ * An rcu_list node.
+ */
+struct rcu_node {
+ /** The RCU pointer to this node. */
+ struct rcu *self;
+ /** The next node in the list. */
+ struct rcu next;
+};
+
+/** Initialize an rcu_list. */
+static void rcu_list_init(struct rcu_list *list) {
+ rcu_init(&list->head, NULL);
+ list->tail = &list->head;
+}
+
+/** Append to an rcu_list. */
+static void rcu_list_append(struct rcu_list *list, struct rcu_node *node) {
+ node->self = list->tail;
+ list->tail = &node->next;
+ rcu_init(&node->next, NULL);
+ rcu_update(node->self, node);
+}
+
+/** Remove from an rcu_list. */
+static void rcu_list_remove(struct rcu_list *list, struct rcu_node *node) {
+ struct rcu_node *next = rcu_peek(&node->next);
+ rcu_update(node->self, next);
+ if (next) {
+ next->self = node->self;
+ } else {
+ list->tail = &list->head;
+ }
+ rcu_destroy(&node->next);
+}
+
+/**
+ * Iterate over an rcu_list.
+ *
+ * It is save to `break` out of this loop, but `return` or `goto` will lead to
+ * a missed arc_put().
+ */
+#define for_rcu(type, node, list) \
+ for_rcu_(type, node, (list), node##_slot_, node##_prev_, node##_done_)
+
+#define for_rcu_(type, node, list, slot, prev, done) \
+ for (struct arc *slot, *prev, **done = NULL; !done; arc_put(slot), done = &slot) \
+ for (type *node = rcu_read(&list->head, &slot); \
+ node; \
+ prev = slot, \
+ node = rcu_read(&((struct rcu_node *)node)->next, &slot), \
+ arc_put(prev))
+
+struct sighook {
+ /** The RCU list node (must be the first field). */
+ struct rcu_node node;
+
+ /** The signal being hooked, or 0 for atsigexit(). */
+ int sig;
+ /** Signal hook flags. */
+ enum sigflags flags;
+ /** The function to call. */
+ sighook_fn *fn;
+ /** An argument to pass to the function. */
+ void *arg;
+ /** Flag for SH_ONESHOT. */
+ atomic bool armed;
+};
+
+/** The lists of signal hooks. */
+static struct rcu_list sighooks[64];
+
+/** Get the hook list for a particular signal. */
+static struct rcu_list *siglist(int sig) {
+ return &sighooks[sig % countof(sighooks)];
+}
+
+/** Mutex for initialization and RCU writer exclusion. */
+static pthread_mutex_t sigmutex = PTHREAD_MUTEX_INITIALIZER;
+
+/** Check if a signal was generated by userspace. */
+static bool is_user_generated(const siginfo_t *info) {
+ // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_03
+ //
+ // If si_code is SI_USER or SI_QUEUE, or any value less than or
+ // equal to 0, then the signal was generated by a process ...
+ int code = info->si_code;
+ return code == SI_USER || code == SI_QUEUE || code <= 0;
+}
+
+/** Check if a signal is caused by a fault. */
+static bool is_fault(const siginfo_t *info) {
+ int sig = info->si_signo;
+ if (sig == SIGBUS || sig == SIGFPE || sig == SIGILL || sig == SIGSEGV) {
+ return !is_user_generated(info);
+ } else {
+ return false;
+ }
+}
+
+// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/signal.h.html
+static const int FATAL_SIGNALS[] = {
+ SIGABRT,
+ SIGALRM,
+ SIGBUS,
+ SIGFPE,
+ SIGHUP,
+ SIGILL,
+ SIGINT,
+#ifdef SIGIO
+ SIGIO,
+#endif
+ SIGPIPE,
+#ifdef SIGPOLL
+ SIGPOLL,
+#endif
+#ifdef SIGPROF
+ SIGPROF,
+#endif
+#ifdef SIGPWR
+ SIGPWR,
+#endif
+ SIGQUIT,
+ SIGSEGV,
+#ifdef SIGSTKFLT
+ SIGSTKFLT,
+#endif
+#ifdef SIGSYS
+ SIGSYS,
+#endif
+ SIGTERM,
+ SIGTRAP,
+ SIGUSR1,
+ SIGUSR2,
+#ifdef SIGVTALRM
+ SIGVTALRM,
+#endif
+ SIGXCPU,
+ SIGXFSZ,
+};
+
+/** Check if a signal's default action is to terminate the process. */
+static bool is_fatal(int sig) {
+ for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) {
+ if (sig == FATAL_SIGNALS[i]) {
+ return true;
+ }
+ }
+
+#ifdef SIGRTMIN
+ // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03_01
+ //
+ // The default actions for the realtime signals in the range
+ // SIGRTMIN to SIGRTMAX shall be to terminate the process
+ // abnormally.
+ if (sig >= SIGRTMIN && sig <= SIGRTMAX) {
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+/** Reraise a fatal signal. */
+_noreturn
+static void reraise(siginfo_t *info) {
+ int sig = info->si_signo;
+
+ // Restore the default signal action
+ if (signal(sig, SIG_DFL) == SIG_ERR) {
+ goto fail;
+ }
+
+ // Unblock the signal, since we didn't set SA_NODEFER
+ sigset_t mask;
+ if (sigemptyset(&mask) != 0
+ || sigaddset(&mask, sig) != 0
+ || pthread_sigmask(SIG_UNBLOCK, &mask, NULL) != 0) {
+ goto fail;
+ }
+
+#if __linux__
+ // On Linux, try to re-raise the exact siginfo_t (since 3.9, a process can
+ // signal itself with any siginfo_t)
+ pid_t tid = syscall(SYS_gettid);
+ syscall(SYS_rt_tgsigqueueinfo, getpid(), tid, sig, info);
+#endif
+
+ raise(sig);
+fail:
+ abort();
+}
+
+/** Check whether we should run a hook. */
+static bool should_run(int sig, struct sighook *hook) {
+ if (hook->sig != sig && hook->sig != 0) {
+ return false;
+ }
+
+ if (hook->flags & SH_ONESHOT) {
+ if (!exchange(&hook->armed, false, relaxed)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/** Find any matching hooks and run them. */
+static enum sigflags run_hooks(struct rcu_list *list, int sig, siginfo_t *info) {
+ enum sigflags ret = 0;
+
+ for_rcu (struct sighook, hook, list) {
+ if (should_run(sig, hook)) {
+ hook->fn(sig, info, hook->arg);
+ ret |= hook->flags;
+ }
+ }
+
+ return ret;
+}
+
+/** Dispatches a signal to the registered handlers. */
+static void sigdispatch(int sig, siginfo_t *info, void *context) {
+ // If we get a fault (e.g. a "real" SIGSEGV, not something like
+ // kill(..., SIGSEGV)), don't try to run signal hooks, since we could be
+ // in an arbitrarily corrupted state.
+ //
+ // POSIX says that returning normally from a signal handler for a fault
+ // is undefined. But in practice, it's better to uninstall the handler
+ // and return, which will re-run the faulting instruction and cause us
+ // to die "correctly" (e.g. with a core dump pointing at the faulting
+ // instruction, not reraise()).
+ if (is_fault(info)) {
+ // On macOS, we cannot reliably distinguish between faults and
+ // asynchronous signals. For example, pkill -SEGV bfs will
+ // result in si_code == SEGV_ACCERR. So we always re-raise the
+ // signal, because just returning would cause us to ignore
+ // asynchronous SIG{BUS,ILL,SEGV}.
+#if !__APPLE__
+ if (signal(sig, SIG_DFL) != SIG_ERR) {
+ return;
+ }
+#endif
+ reraise(info);
+ }
+
+ // https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_04
+ //
+ // After returning from a signal-catching function, the value of
+ // errno is unspecified if the signal-catching function or any
+ // function it called assigned a value to errno and the signal-
+ // catching function did not save and restore the original value of
+ // errno.
+ int error = errno;
+
+ // Run the normal hooks
+ struct rcu_list *list = siglist(sig);
+ enum sigflags flags = run_hooks(list, sig, info);
+
+ // Run the atsigexit() hooks, if we're exiting
+ if (!(flags & SH_CONTINUE) && is_fatal(sig)) {
+ list = siglist(0);
+ run_hooks(list, sig, info);
+ reraise(info);
+ }
+
+ errno = error;
+}
+
+/** A saved signal handler, for sigreset() to restore. */
+struct sigsave {
+ struct rcu_node node;
+ int sig;
+ struct sigaction action;
+};
+
+/** The list of saved signal handlers. */
+static struct rcu_list saved;
+/** `saved` initialization status (since rcu_list_init() isn't atomic). */
+static atomic bool initialized = false;
+
+/** Make sure our signal handler is installed for a given signal. */
+static int siginit(int sig) {
+#ifdef SA_RESTART
+# define BFS_SA_RESTART SA_RESTART
+#else
+# define BFS_SA_RESTART 0
+#endif
+
+ static struct sigaction action = {
+ .sa_sigaction = sigdispatch,
+ .sa_flags = BFS_SA_RESTART | SA_SIGINFO,
+ };
+
+ static sigset_t signals;
+
+ if (!load(&initialized, relaxed)) {
+ if (sigemptyset(&signals) != 0
+ || sigemptyset(&action.sa_mask) != 0) {
+ return -1;
+ }
+
+ for (size_t i = 0; i < countof(sighooks); ++i) {
+ rcu_list_init(&sighooks[i]);
+ }
+
+ rcu_list_init(&saved);
+ store(&initialized, true, release);
+ }
+
+ int installed = sigismember(&signals, sig);
+ if (installed < 0) {
+ return -1;
+ } else if (installed) {
+ return 0;
+ }
+
+ sigset_t updated = signals;
+ if (sigaddset(&updated, sig) != 0) {
+ return -1;
+ }
+
+ struct sigaction original;
+ if (sigaction(sig, NULL, &original) != 0) {
+ return -1;
+ }
+
+ struct sigsave *save = ALLOC(struct sigsave);
+ if (!save) {
+ return -1;
+ }
+
+ save->sig = sig;
+ save->action = original;
+ rcu_list_append(&saved, &save->node);
+
+ if (sigaction(sig, &action, NULL) != 0) {
+ rcu_list_remove(&saved, &save->node);
+ free(save);
+ return -1;
+ }
+
+ signals = updated;
+ return 0;
+}
+
+/** Shared sighook()/atsigexit() implementation. */
+static struct sighook *sighook_impl(int sig, sighook_fn *fn, void *arg, enum sigflags flags) {
+ struct sighook *hook = ALLOC(struct sighook);
+ if (!hook) {
+ return NULL;
+ }
+
+ hook->sig = sig;
+ hook->flags = flags;
+ hook->fn = fn;
+ hook->arg = arg;
+ atomic_init(&hook->armed, true);
+
+ struct rcu_list *list = siglist(sig);
+ rcu_list_append(list, &hook->node);
+ return hook;
+}
+
+struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags) {
+ bfs_assert(sig > 0);
+
+ mutex_lock(&sigmutex);
+
+ struct sighook *ret = NULL;
+ if (siginit(sig) == 0) {
+ ret = sighook_impl(sig, fn, arg, flags);
+ }
+
+ mutex_unlock(&sigmutex);
+ return ret;
+}
+
+struct sighook *atsigexit(sighook_fn *fn, void *arg) {
+ mutex_lock(&sigmutex);
+
+ for (size_t i = 0; i < countof(FATAL_SIGNALS); ++i) {
+ // Ignore errors; atsigexit() is best-effort anyway and things
+ // like sanitizer runtimes or valgrind may reserve signals for
+ // their own use
+ siginit(FATAL_SIGNALS[i]);
+ }
+
+#ifdef SIGRTMIN
+ for (int i = SIGRTMIN; i <= SIGRTMAX; ++i) {
+ siginit(i);
+ }
+#endif
+
+ struct sighook *ret = sighook_impl(0, fn, arg, 0);
+ mutex_unlock(&sigmutex);
+ return ret;
+}
+
+void sigunhook(struct sighook *hook) {
+ if (!hook) {
+ return;
+ }
+
+ mutex_lock(&sigmutex);
+
+ struct rcu_list *list = siglist(hook->sig);
+ rcu_list_remove(list, &hook->node);
+
+ mutex_unlock(&sigmutex);
+
+ free(hook);
+}
+
+int sigreset(void) {
+ if (!load(&initialized, acquire)) {
+ return 0;
+ }
+
+ int ret = 0;
+
+ for_rcu (struct sigsave, save, &saved) {
+ if (sigaction(save->sig, &save->action, NULL) != 0) {
+ ret = -1;
+ break;
+ }
+ }
+
+ return ret;
+}
diff --git a/src/sighook.h b/src/sighook.h
new file mode 100644
index 0000000..7149229
--- /dev/null
+++ b/src/sighook.h
@@ -0,0 +1,83 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Signal hooks.
+ */
+
+#ifndef BFS_SIGHOOK_H
+#define BFS_SIGHOOK_H
+
+#include <signal.h>
+
+/**
+ * A dynamic signal hook.
+ */
+struct sighook;
+
+/**
+ * Signal hook flags.
+ */
+enum sigflags {
+ /** Suppress the default action for this signal. */
+ SH_CONTINUE = 1 << 0,
+ /** Only run this hook once. */
+ SH_ONESHOT = 1 << 1,
+};
+
+/**
+ * A signal hook callback. Hooks are executed from a signal handler, so must
+ * only call async-signal-safe functions.
+ *
+ * @sig
+ * The signal number.
+ * @info
+ * Additional information about the signal.
+ * @arg
+ * An arbitrary pointer passed to the hook.
+ */
+typedef void sighook_fn(int sig, siginfo_t *info, void *arg);
+
+/**
+ * Install a hook for a signal.
+ *
+ * @sig
+ * The signal to hook.
+ * @fn
+ * The function to call.
+ * @arg
+ * An argument passed to the function.
+ * @flags
+ * Flags for the new hook.
+ * @return
+ * The installed hook, or NULL on failure.
+ */
+struct sighook *sighook(int sig, sighook_fn *fn, void *arg, enum sigflags flags);
+
+/**
+ * On a best-effort basis, invoke the given hook just before the program is
+ * abnormally terminated by a signal.
+ *
+ * @fn
+ * The function to call.
+ * @arg
+ * An argument passed to the function.
+ * @return
+ * The installed hook, or NULL on failure.
+ */
+struct sighook *atsigexit(sighook_fn *fn, void *arg);
+
+/**
+ * Remove a signal hook.
+ */
+void sigunhook(struct sighook *hook);
+
+/**
+ * Restore all signal handlers to their original dispositions (e.g. after fork()).
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int sigreset(void);
+
+#endif // BFS_SIGHOOK_H
diff --git a/src/stat.c b/src/stat.c
new file mode 100644
index 0000000..1fcfde3
--- /dev/null
+++ b/src/stat.c
@@ -0,0 +1,376 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "stat.h"
+
+#include "atomic.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#if BFS_USE_STATX && !BFS_HAS_STATX
+# include <linux/stat.h>
+# include <sys/syscall.h>
+# include <unistd.h>
+#endif
+
+const char *bfs_stat_field_name(enum bfs_stat_field field) {
+ switch (field) {
+ case BFS_STAT_MODE:
+ return "mode";
+ case BFS_STAT_DEV:
+ return "device number";
+ case BFS_STAT_INO:
+ return "inode nunmber";
+ case BFS_STAT_NLINK:
+ return "link count";
+ case BFS_STAT_GID:
+ return "group ID";
+ case BFS_STAT_UID:
+ return "user ID";
+ case BFS_STAT_SIZE:
+ return "size";
+ case BFS_STAT_BLOCKS:
+ return "block count";
+ case BFS_STAT_RDEV:
+ return "underlying device";
+ case BFS_STAT_ATTRS:
+ return "attributes";
+ case BFS_STAT_ATIME:
+ return "access time";
+ case BFS_STAT_BTIME:
+ return "birth time";
+ case BFS_STAT_CTIME:
+ return "change time";
+ case BFS_STAT_MTIME:
+ return "modification time";
+ case BFS_STAT_MNT_ID:
+ return "mount ID";
+ }
+
+ bfs_bug("Unrecognized stat field %d", (int)field);
+ return "???";
+}
+
+int bfs_fstatat_flags(enum bfs_stat_flags flags) {
+ int ret = 0;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ ret |= AT_SYMLINK_NOFOLLOW;
+ }
+
+#ifdef AT_NO_AUTOMOUNT
+ ret |= AT_NO_AUTOMOUNT;
+#endif
+
+ return ret;
+}
+
+void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) {
+ dest->mask = 0;
+
+ dest->mode = src->st_mode;
+ dest->mask |= BFS_STAT_MODE;
+
+ dest->dev = src->st_dev;
+ dest->mask |= BFS_STAT_DEV;
+
+ dest->ino = src->st_ino;
+ dest->mask |= BFS_STAT_INO;
+
+ dest->nlink = src->st_nlink;
+ dest->mask |= BFS_STAT_NLINK;
+
+ dest->gid = src->st_gid;
+ dest->mask |= BFS_STAT_GID;
+
+ dest->uid = src->st_uid;
+ dest->mask |= BFS_STAT_UID;
+
+ dest->size = src->st_size;
+ dest->mask |= BFS_STAT_SIZE;
+
+ dest->blocks = src->st_blocks;
+ dest->mask |= BFS_STAT_BLOCKS;
+
+ dest->rdev = src->st_rdev;
+ dest->mask |= BFS_STAT_RDEV;
+
+ // No mount IDs in regular stat(), so use the dev_t as an approximation
+ dest->mnt_id = dest->dev;
+ dest->mask |= BFS_STAT_MNT_ID;
+
+#if BFS_HAS_ST_FLAGS
+ dest->attrs = src->st_flags;
+ dest->mask |= BFS_STAT_ATTRS;
+#endif
+
+ dest->atime = ST_ATIM(*src);
+ dest->mask |= BFS_STAT_ATIME;
+
+ dest->ctime = ST_CTIM(*src);
+ dest->mask |= BFS_STAT_CTIME;
+
+ dest->mtime = ST_MTIM(*src);
+ dest->mask |= BFS_STAT_MTIME;
+
+#if BFS_HAS_ST_BIRTHTIM
+ dest->btime = src->st_birthtim;
+ dest->mask |= BFS_STAT_BTIME;
+#elif BFS_HAS___ST_BIRTHTIM
+ dest->btime = src->__st_birthtim;
+ dest->mask |= BFS_STAT_BTIME;
+#elif BFS_HAS_ST_BIRTHTIMESPEC
+ dest->btime = src->st_birthtimespec;
+ dest->mask |= BFS_STAT_BTIME;
+#endif
+}
+
+/**
+ * bfs_stat() implementation backed by stat().
+ */
+static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
+ struct stat statbuf;
+ int ret = fstatat(at_fd, at_path, &statbuf, at_flags);
+ if (ret == 0) {
+ bfs_stat_convert(buf, &statbuf);
+ }
+ return ret;
+}
+
+#if BFS_USE_STATX
+
+/**
+ * Wrapper for the statx() system call, which had no glibc wrapper prior to 2.28.
+ */
+static int bfs_statx(int at_fd, const char *at_path, int at_flags, unsigned int mask, struct statx *buf) {
+#if BFS_HAS_STATX
+ int ret = statx(at_fd, at_path, at_flags, mask, buf);
+#else
+ int ret = syscall(SYS_statx, at_fd, at_path, at_flags, mask, buf);
+#endif
+
+ if (ret == 0) {
+ // -fsanitize=memory doesn't know about statx()
+ sanitize_init(buf);
+ }
+
+ return ret;
+}
+
+int bfs_statx_flags(enum bfs_stat_flags flags) {
+ int ret = bfs_fstatat_flags(flags);
+
+ if (flags & BFS_STAT_NOSYNC) {
+ ret |= AT_STATX_DONT_SYNC;
+ }
+
+ return ret;
+}
+
+unsigned int bfs_statx_mask(void) {
+ unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
+#ifdef STATX_MNT_ID
+ mask |= STATX_MNT_ID;
+#endif
+#ifdef STATX_MNT_ID_UNIQUE
+ mask |= STATX_MNT_ID_UNIQUE;
+#endif
+ return mask;
+}
+
+int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) {
+ // Callers shouldn't have to check anything except the times
+ const unsigned int guaranteed = STATX_BASIC_STATS & ~(STATX_ATIME | STATX_CTIME | STATX_MTIME);
+ if ((src->stx_mask & guaranteed) != guaranteed) {
+ errno = ENOTSUP;
+ return -1;
+ }
+
+ dest->mask = 0;
+
+ dest->mode = src->stx_mode;
+ dest->mask |= BFS_STAT_MODE;
+
+ dest->dev = xmakedev(src->stx_dev_major, src->stx_dev_minor);
+ dest->mask |= BFS_STAT_DEV;
+
+ dest->ino = src->stx_ino;
+ dest->mask |= BFS_STAT_INO;
+
+ dest->nlink = src->stx_nlink;
+ dest->mask |= BFS_STAT_NLINK;
+
+ dest->gid = src->stx_gid;
+ dest->mask |= BFS_STAT_GID;
+
+ dest->uid = src->stx_uid;
+ dest->mask |= BFS_STAT_UID;
+
+ dest->size = src->stx_size;
+ dest->mask |= BFS_STAT_SIZE;
+
+ dest->blocks = src->stx_blocks;
+ dest->mask |= BFS_STAT_BLOCKS;
+
+ dest->rdev = xmakedev(src->stx_rdev_major, src->stx_rdev_minor);
+ dest->mask |= BFS_STAT_RDEV;
+
+ dest->attrs = src->stx_attributes;
+ dest->mask |= BFS_STAT_ATTRS;
+
+ dest->mnt_id = dest->dev;
+#ifdef STATX_MNT_ID
+ unsigned int mnt_mask = STATX_MNT_ID;
+# ifdef STATX_MNT_ID_UNIQUE
+ mnt_mask |= STATX_MNT_ID_UNIQUE;
+# endif
+ if (src->stx_mask & mnt_mask) {
+ dest->mnt_id = src->stx_mnt_id;
+ }
+#endif
+ dest->mask |= BFS_STAT_MNT_ID;
+
+ if (src->stx_mask & STATX_ATIME) {
+ dest->atime.tv_sec = src->stx_atime.tv_sec;
+ dest->atime.tv_nsec = src->stx_atime.tv_nsec;
+ dest->mask |= BFS_STAT_ATIME;
+ }
+
+ if (src->stx_mask & STATX_BTIME) {
+ dest->btime.tv_sec = src->stx_btime.tv_sec;
+ dest->btime.tv_nsec = src->stx_btime.tv_nsec;
+ dest->mask |= BFS_STAT_BTIME;
+ }
+
+ if (src->stx_mask & STATX_CTIME) {
+ dest->ctime.tv_sec = src->stx_ctime.tv_sec;
+ dest->ctime.tv_nsec = src->stx_ctime.tv_nsec;
+ dest->mask |= BFS_STAT_CTIME;
+ }
+
+ if (src->stx_mask & STATX_MTIME) {
+ dest->mtime.tv_sec = src->stx_mtime.tv_sec;
+ dest->mtime.tv_nsec = src->stx_mtime.tv_nsec;
+ dest->mask |= BFS_STAT_MTIME;
+ }
+
+ return 0;
+}
+
+/**
+ * bfs_stat() implementation backed by statx().
+ */
+static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
+ unsigned int mask = bfs_statx_mask();
+ struct statx xbuf;
+ int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf);
+ if (ret != 0) {
+ return ret;
+ }
+
+ return bfs_statx_convert(buf, &xbuf);
+}
+
+#endif // BFS_USE_STATX
+
+/**
+ * Calls the stat() implementation with explicit flags.
+ */
+static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
+#if BFS_USE_STATX
+ static atomic bool has_statx = true;
+
+ if (load(&has_statx, relaxed)) {
+ int ret = bfs_statx_impl(at_fd, at_path, at_flags, buf);
+ if (ret != 0 && errno_is_like(ENOSYS)) {
+ store(&has_statx, false, relaxed);
+ } else {
+ return ret;
+ }
+ }
+
+ at_flags &= ~AT_STATX_DONT_SYNC;
+#endif
+
+ return bfs_stat_impl(at_fd, at_path, at_flags, buf);
+}
+
+/**
+ * Implements the BFS_STAT_TRYFOLLOW retry logic.
+ */
+static int bfs_stat_tryfollow(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags bfs_flags, struct bfs_stat *buf) {
+ int ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf);
+
+ if (ret != 0
+ && (bfs_flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW
+ && errno_is_like(ENOENT))
+ {
+ at_flags |= AT_SYMLINK_NOFOLLOW;
+ ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf);
+ }
+
+ return ret;
+}
+
+int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf) {
+#if BFS_USE_STATX
+ int at_flags = bfs_statx_flags(flags);
+#else
+ int at_flags = bfs_fstatat_flags(flags);
+#endif
+
+ if (at_path) {
+ return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf);
+ }
+
+#if BFS_USE_STATX
+ // If we have statx(), use it with AT_EMPTY_PATH for its extra features
+ at_flags |= AT_EMPTY_PATH;
+ return bfs_stat_explicit(at_fd, "", at_flags, buf);
+#else
+ // Otherwise, just use fstat() rather than fstatat(at_fd, ""), to save
+ // the kernel the trouble of copying in the empty string
+ struct stat sb;
+ if (fstat(at_fd, &sb) != 0) {
+ return -1;
+ }
+
+ bfs_stat_convert(buf, &sb);
+ return 0;
+#endif
+}
+
+const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field) {
+ if (!(buf->mask & field)) {
+ errno = ENOTSUP;
+ return NULL;
+ }
+
+ switch (field) {
+ case BFS_STAT_ATIME:
+ return &buf->atime;
+ case BFS_STAT_BTIME:
+ return &buf->btime;
+ case BFS_STAT_CTIME:
+ return &buf->ctime;
+ case BFS_STAT_MTIME:
+ return &buf->mtime;
+ default:
+ bfs_bug("Invalid stat field for time");
+ errno = EINVAL;
+ return NULL;
+ }
+}
+
+void bfs_stat_id(const struct bfs_stat *buf, bfs_file_id *id) {
+ memcpy(*id, &buf->dev, sizeof(buf->dev));
+ memcpy(*id + sizeof(buf->dev), &buf->ino, sizeof(buf->ino));
+}
diff --git a/stat.h b/src/stat.h
index 55c75e9..c4a63d3 100644
--- a/stat.h
+++ b/src/stat.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A facade over the stat() API that unifies some details that diverge between
@@ -25,33 +12,52 @@
#ifndef BFS_STAT_H
#define BFS_STAT_H
-#include "util.h"
+#include "bfs.h"
+
+#include <stdint.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
+#if !BFS_HAS_STATX && BFS_HAS_STATX_SYSCALL
+# include <linux/stat.h>
+#endif
+
+#ifndef BFS_USE_STATX
+# define BFS_USE_STATX (BFS_HAS_STATX || BFS_HAS_STATX_SYSCALL)
+#endif
+
+#if __has_include(<sys/param.h>)
+# include <sys/param.h>
+#endif
+
+#ifdef DEV_BSIZE
+# define BFS_STAT_BLKSIZE DEV_BSIZE
+#elif defined(S_BLKSIZE)
+# define BFS_STAT_BLKSIZE S_BLKSIZE
+#else
+# define BFS_STAT_BLKSIZE 512
#endif
/**
* bfs_stat field bitmask.
*/
enum bfs_stat_field {
- BFS_STAT_DEV = 1 << 0,
- BFS_STAT_INO = 1 << 1,
- BFS_STAT_TYPE = 1 << 2,
- BFS_STAT_MODE = 1 << 3,
- BFS_STAT_NLINK = 1 << 4,
- BFS_STAT_GID = 1 << 5,
- BFS_STAT_UID = 1 << 6,
- BFS_STAT_SIZE = 1 << 7,
- BFS_STAT_BLOCKS = 1 << 8,
- BFS_STAT_RDEV = 1 << 9,
- BFS_STAT_ATTRS = 1 << 10,
- BFS_STAT_ATIME = 1 << 11,
- BFS_STAT_BTIME = 1 << 12,
- BFS_STAT_CTIME = 1 << 13,
- BFS_STAT_MTIME = 1 << 14,
+ BFS_STAT_MODE = 1 << 0,
+ BFS_STAT_DEV = 1 << 1,
+ BFS_STAT_INO = 1 << 2,
+ BFS_STAT_NLINK = 1 << 3,
+ BFS_STAT_GID = 1 << 4,
+ BFS_STAT_UID = 1 << 5,
+ BFS_STAT_SIZE = 1 << 6,
+ BFS_STAT_BLOCKS = 1 << 7,
+ BFS_STAT_RDEV = 1 << 8,
+ BFS_STAT_ATTRS = 1 << 9,
+ BFS_STAT_ATIME = 1 << 10,
+ BFS_STAT_BTIME = 1 << 11,
+ BFS_STAT_CTIME = 1 << 12,
+ BFS_STAT_MTIME = 1 << 13,
+ BFS_STAT_MNT_ID = 1 << 14,
};
/**
@@ -73,14 +79,6 @@ enum bfs_stat_flags {
BFS_STAT_NOSYNC = 1 << 2,
};
-#ifdef DEV_BSIZE
-# define BFS_STAT_BLKSIZE DEV_BSIZE
-#elif defined(S_BLKSIZE)
-# define BFS_STAT_BLKSIZE S_BLKSIZE
-#else
-# define BFS_STAT_BLKSIZE 512
-#endif
-
/**
* Facade over struct stat.
*/
@@ -88,12 +86,12 @@ struct bfs_stat {
/** Bitmask indicating filled fields. */
enum bfs_stat_field mask;
+ /** File type and access mode. */
+ mode_t mode;
/** Device ID containing the file. */
dev_t dev;
/** Inode number. */
ino_t ino;
- /** File type and access mode. */
- mode_t mode;
/** Number of hard links. */
nlink_t nlink;
/** Owner group ID. */
@@ -106,6 +104,8 @@ struct bfs_stat {
blkcnt_t blocks;
/** The device ID represented by this file. */
dev_t rdev;
+ /** The ID of the mount point containing this file. */
+ uint64_t mnt_id;
/** Attributes/flags set on the file. */
unsigned long long attrs;
@@ -123,14 +123,14 @@ struct bfs_stat {
/**
* Facade over fstatat().
*
- * @param at_fd
+ * @at_fd
* The base file descriptor for the lookup.
- * @param at_path
+ * @at_path
* The path to stat, relative to at_fd. Pass NULL to fstat() at_fd
* itself.
- * @param flags
+ * @flags
* Flags that affect the lookup.
- * @param[out] buf
+ * @buf[out]
* A place to store the stat buffer, if successful.
* @return
* 0 on success, -1 on error.
@@ -138,6 +138,33 @@ struct bfs_stat {
int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf);
/**
+ * Convert bfs_stat_flags to fstatat() flags.
+ */
+int bfs_fstatat_flags(enum bfs_stat_flags flags);
+
+/**
+ * Convert struct stat to struct bfs_stat.
+ */
+void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src);
+
+#if BFS_USE_STATX
+/**
+ * Convert bfs_stat_flags to statx() flags.
+ */
+int bfs_statx_flags(enum bfs_stat_flags flags);
+
+/**
+ * Get the default statx() mask.
+ */
+unsigned int bfs_statx_mask(void);
+
+/**
+ * Convert struct statx to struct bfs_stat.
+ */
+int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src);
+#endif
+
+/**
* Get a particular time field from a bfs_stat() buffer.
*/
const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field);
diff --git a/src/thread.c b/src/thread.c
new file mode 100644
index 0000000..b3604f8
--- /dev/null
+++ b/src/thread.c
@@ -0,0 +1,94 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "thread.h"
+
+#include "bfstd.h"
+#include "diag.h"
+
+#include <errno.h>
+#include <pthread.h>
+
+#if __has_include(<pthread_np.h>)
+# include <pthread_np.h>
+#endif
+
+#define THREAD_FALLIBLE(expr) \
+ do { \
+ int err = expr; \
+ if (err == 0) { \
+ return 0; \
+ } else { \
+ errno = err; \
+ return -1; \
+ } \
+ } while (0)
+
+#define THREAD_INFALLIBLE(...) \
+ THREAD_INFALLIBLE_(__VA_ARGS__, 0, )
+
+#define THREAD_INFALLIBLE_(expr, allowed, ...) \
+ int err = expr; \
+ bfs_verify(err == 0 || err == allowed, "%s: %s", #expr, xstrerror(err)); \
+ (void)0
+
+int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg) {
+ THREAD_FALLIBLE(pthread_create(thread, attr, fn, arg));
+}
+
+void thread_setname(pthread_t thread, const char *name) {
+#if BFS_HAS_PTHREAD_SETNAME_NP
+ pthread_setname_np(thread, name);
+#elif BFS_HAS_PTHREAD_SET_NAME_NP
+ pthread_set_name_np(thread, name);
+#endif
+}
+
+void thread_join(pthread_t thread, void **ret) {
+ THREAD_INFALLIBLE(pthread_join(thread, ret));
+}
+
+int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) {
+ THREAD_FALLIBLE(pthread_mutex_init(mutex, attr));
+}
+
+void mutex_lock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_lock(mutex));
+}
+
+bool mutex_trylock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_trylock(mutex), EBUSY);
+ return err == 0;
+}
+
+void mutex_unlock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_unlock(mutex));
+}
+
+void mutex_destroy(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_destroy(mutex));
+}
+
+int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) {
+ THREAD_FALLIBLE(pthread_cond_init(cond, attr));
+}
+
+void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_cond_wait(cond, mutex));
+}
+
+void cond_signal(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_signal(cond));
+}
+
+void cond_broadcast(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_broadcast(cond));
+}
+
+void cond_destroy(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_destroy(cond));
+}
+
+void invoke_once(pthread_once_t *once, once_fn *fn) {
+ THREAD_INFALLIBLE(pthread_once(once, fn));
+}
diff --git a/src/thread.h b/src/thread.h
new file mode 100644
index 0000000..3dd8422
--- /dev/null
+++ b/src/thread.h
@@ -0,0 +1,95 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Wrappers for POSIX threading APIs.
+ */
+
+#ifndef BFS_THREAD_H
+#define BFS_THREAD_H
+
+#include <pthread.h>
+
+/** Thread entry point type. */
+typedef void *thread_fn(void *arg);
+
+/**
+ * Wrapper for pthread_create().
+ *
+ * @return
+ * 0 on success, -1 on error.
+ */
+int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg);
+
+/**
+ * Set the name of a thread.
+ */
+void thread_setname(pthread_t thread, const char *name);
+
+/**
+ * Wrapper for pthread_join().
+ */
+void thread_join(pthread_t thread, void **ret);
+
+/**
+ * Wrapper for pthread_mutex_init().
+ */
+int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
+
+/**
+ * Wrapper for pthread_mutex_lock().
+ */
+void mutex_lock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_trylock().
+ *
+ * @return
+ * Whether the mutex was locked.
+ */
+bool mutex_trylock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_unlock().
+ */
+void mutex_unlock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_destroy().
+ */
+void mutex_destroy(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_cond_init().
+ */
+int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
+
+/**
+ * Wrapper for pthread_cond_wait().
+ */
+void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_cond_signal().
+ */
+void cond_signal(pthread_cond_t *cond);
+
+/**
+ * Wrapper for pthread_cond_broadcast().
+ */
+void cond_broadcast(pthread_cond_t *cond);
+
+/**
+ * Wrapper for pthread_cond_destroy().
+ */
+void cond_destroy(pthread_cond_t *cond);
+
+/** pthread_once() callback type. */
+typedef void once_fn(void);
+
+/**
+ * Wrapper for pthread_once().
+ */
+void invoke_once(pthread_once_t *once, once_fn *fn);
+
+#endif // BFS_THREAD_H
diff --git a/src/trie.c b/src/trie.c
new file mode 100644
index 0000000..6aac17f
--- /dev/null
+++ b/src/trie.c
@@ -0,0 +1,782 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * This is an implementation of a "qp trie," as documented at
+ * https://dotat.at/prog/qp/README.html
+ *
+ * An uncompressed trie over the dataset {AAAA, AADD, ABCD, DDAA, DDDD} would
+ * look like
+ *
+ * A A A A
+ * â—───→â—───→â—───→â—───→○
+ * │ │ │ D D
+ * │ │ └───→â—───→○
+ * │ │ B C D
+ * │ └───→â—───→â—───→○
+ * │ D D A A
+ * └───→â—───→â—───→â—───→○
+ * │ D D
+ * └───→â—───→○
+ *
+ * A compressed (PATRICIA) trie collapses internal nodes that have only a single
+ * child, like this:
+ *
+ * A A AA
+ * â—───→â—───→â—────→○
+ * │ │ │ DD
+ * │ │ └────→○
+ * │ │ BCD
+ * │ └─────→○
+ * │ DD AA
+ * └────→â—────→○
+ * │ DD
+ * └────→○
+ *
+ * The nodes can be compressed further by dropping the actual compressed
+ * sequences from the nodes, storing it only in the leaves. This is the
+ * technique applied in QP tries, and the crit-bit trees that inspired them
+ * (https://cr.yp.to/critbit.html). Only the index to test, and the values to
+ * branch on, need to be stored in each node.
+ *
+ * A A A
+ * 0───→1───→2───→AAAA
+ * │ │ │ D
+ * │ │ └───→AADD
+ * │ │ B
+ * │ └───→ABCD
+ * │ D A
+ * └───→2───→DDAA
+ * │ D
+ * └───→DDDD
+ *
+ * Nodes are represented very compactly. Rather than a dense array of children,
+ * a sparse array of only the non-NULL children directly follows the node in
+ * memory. A bitmap is used to track which children exist.
+ *
+ * ┌────────────â”
+ * │ [4] [3] [2][1][0] â†â”€ children
+ * │ ↓ ↓ ↓ ↓ ↓
+ * │ 14 10 6 3 0 â†â”€ sparse index
+ * │ ↓ ↓ ↓ ↓ ↓
+ * │ 0100010001001001 â†â”€ bitmap
+ * │
+ * │ To convert a sparse index to a dense index, mask off the bits above it, and
+ * │ count the remaining bits.
+ * │
+ * │ 10 â†â”€ sparse index
+ * │ ↓
+ * │ 0000001111111111 â†â”€ mask
+ * │ & 0100010001001001 â†â”€ bitmap
+ * │ ────────────────
+ * │ = 0000000001001001
+ * │ └──┼──┘
+ * │ [3] â†â”€ dense index
+ * └───────────────────┘
+ *
+ * This implementation tests a whole nibble (half byte/hex digit) at every
+ * branch, so the bitmap takes up 16 bits. The remainder of a machine word is
+ * used to hold the offset, which severely constrains its range on 32-bit
+ * platforms. As a workaround, we store relative instead of absolute offsets,
+ * and insert intermediate singleton "jump" nodes when necessary.
+ */
+
+#include "trie.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bit.h"
+#include "diag.h"
+#include "list.h"
+
+#include <stdint.h>
+#include <string.h>
+
+static_assert(CHAR_WIDTH == 8, "This trie implementation assumes 8-bit bytes.");
+
+#if __i386__ || __x86_64__
+# define _trie_clones _target_clones("popcnt", "default")
+#else
+# define _trie_clones
+#endif
+
+/** Number of bits for the sparse array bitmap, aka the range of a nibble. */
+#define BITMAP_WIDTH 16
+/** The number of remaining bits in a word, to hold the offset. */
+#define OFFSET_WIDTH (SIZE_WIDTH - BITMAP_WIDTH)
+/** The highest representable offset (only 64k on a 32-bit architecture). */
+#define OFFSET_MAX (((size_t)1 << OFFSET_WIDTH) - 1)
+
+/**
+ * An internal node of the trie.
+ */
+struct trie_node {
+ /**
+ * A bitmap that hold which indices exist in the sparse children array.
+ * Bit i will be set if a child exists at logical index i, and its index
+ * into the array will be popcount(bitmap & ((1 << i) - 1)).
+ */
+ size_t bitmap : BITMAP_WIDTH;
+
+ /**
+ * The offset into the key in nibbles. This is relative to the parent
+ * node, to support offsets larger than OFFSET_MAX.
+ */
+ size_t offset : OFFSET_WIDTH;
+
+ /**
+ * Flexible array of children. Each pointer uses the lowest bit as a
+ * tag to distinguish internal nodes from leaves. This is safe as long
+ * as all dynamic allocations are aligned to more than a single byte.
+ */
+ uintptr_t children[]; // _counted_by(count_ones(bitmap))
+};
+
+/** Check if an encoded pointer is to an internal node. */
+static bool trie_is_node(uintptr_t ptr) {
+ return ptr & 1;
+}
+
+/** Decode a pointer to an internal node. */
+static struct trie_node *trie_decode_node(uintptr_t ptr) {
+ bfs_assert(trie_is_node(ptr));
+ return (struct trie_node *)(ptr - 1);
+}
+
+/** Encode a pointer to an internal node. */
+static uintptr_t trie_encode_node(const struct trie_node *node) {
+ uintptr_t ptr = (uintptr_t)node + 1;
+ bfs_assert(trie_is_node(ptr));
+ return ptr;
+}
+
+/** Decode a pointer to a leaf. */
+static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) {
+ bfs_assert(!trie_is_node(ptr));
+ return (struct trie_leaf *)ptr;
+}
+
+/** Encode a pointer to a leaf. */
+static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) {
+ uintptr_t ptr = (uintptr_t)leaf;
+ bfs_assert(!trie_is_node(ptr));
+ return ptr;
+}
+
+void trie_init(struct trie *trie) {
+ trie->root = 0;
+ LIST_INIT(trie);
+ VARENA_INIT(&trie->nodes, struct trie_node, children);
+ VARENA_INIT(&trie->leaves, struct trie_leaf, key);
+}
+
+/** Extract the nibble at a certain offset from a byte sequence. */
+static unsigned char trie_key_nibble(const void *key, size_t length, size_t offset) {
+ const unsigned char *bytes = key;
+ size_t byte = offset / 2;
+ bfs_assert(byte < length);
+
+ // A branchless version of
+ // if (offset & 1) {
+ // return bytes[byte] & 0xF;
+ // } else {
+ // return bytes[byte] >> 4;
+ // }
+ unsigned int shift = 4 * ((offset + 1) % 2);
+ return (bytes[byte] >> shift) & 0xF;
+}
+
+/** Extract the nibble at a certain offset from a leaf. */
+static unsigned char trie_leaf_nibble(const struct trie_leaf *leaf, size_t offset) {
+ return trie_key_nibble(leaf->key, leaf->length, offset);
+}
+
+/** Get the number of children of an internal node. */
+_trie_clones
+static unsigned int trie_node_size(const struct trie_node *node) {
+ return count_ones((unsigned int)node->bitmap);
+}
+
+/**
+ * Finds a leaf in the trie that matches the key at every branch. If the key
+ * exists in the trie, the representative will match the searched key. But
+ * since only branch points are tested, it can be different from the key. In
+ * that case, the first mismatch between the key and the representative will be
+ * the depth at which to make a new branch to insert the key.
+ */
+_trie_clones
+static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) {
+ uintptr_t ptr = trie->root;
+
+ size_t offset = 0, limit = 2 * length;
+ while (trie_is_node(ptr)) {
+ struct trie_node *node = trie_decode_node(ptr);
+ offset += node->offset;
+
+ unsigned int index = 0;
+ if (offset < limit) {
+ unsigned char nibble = trie_key_nibble(key, length, offset);
+ unsigned int bit = 1U << nibble;
+ unsigned int map = node->bitmap;
+ unsigned int bits = map & (bit - 1);
+ unsigned int mask = -!!(map & bit);
+ // index = (map & bit) ? count_ones(bits) : 0;
+ index = count_ones(bits) & mask;
+ }
+ ptr = node->children[index];
+ }
+
+ return trie_decode_leaf(ptr);
+}
+
+struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) {
+ return trie_find_mem(trie, key, strlen(key) + 1);
+}
+
+_trie_clones
+static struct trie_leaf *trie_find_mem_impl(const struct trie *trie, const void *key, size_t length) {
+ struct trie_leaf *rep = trie_representative(trie, key, length);
+ if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) {
+ return rep;
+ } else {
+ return NULL;
+ }
+}
+
+struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) {
+ return trie_find_mem_impl(trie, key, length);
+}
+
+void *trie_get_str(const struct trie *trie, const char *key) {
+ const struct trie_leaf *leaf = trie_find_str(trie, key);
+ return leaf ? leaf->value : NULL;
+}
+
+void *trie_get_mem(const struct trie *trie, const void *key, size_t length) {
+ const struct trie_leaf *leaf = trie_find_mem(trie, key, length);
+ return leaf ? leaf->value : NULL;
+}
+
+_trie_clones
+static struct trie_leaf *trie_find_postfix_impl(const struct trie *trie, const char *key) {
+ size_t length = strlen(key);
+ struct trie_leaf *rep = trie_representative(trie, key, length + 1);
+ if (rep && rep->length >= length && memcmp(rep->key, key, length) == 0) {
+ return rep;
+ } else {
+ return NULL;
+ }
+}
+
+struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) {
+ return trie_find_postfix_impl(trie, key);
+}
+
+/**
+ * Find a leaf that may end at the current node.
+ */
+static struct trie_leaf *trie_terminal_leaf(const struct trie_node *node) {
+ // Finding a terminating NUL byte may take two nibbles
+ for (int i = 0; i < 2; ++i) {
+ if (!(node->bitmap & 1)) {
+ break;
+ }
+
+ uintptr_t ptr = node->children[0];
+ if (trie_is_node(ptr)) {
+ node = trie_decode_node(ptr);
+ } else {
+ return trie_decode_leaf(ptr);
+ }
+ }
+
+ return NULL;
+}
+
+/** Check if a leaf is a prefix of a search key. */
+static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *key, size_t length) {
+ if (leaf && leaf->length <= length) {
+ return memcmp(key + skip, leaf->key + skip, leaf->length - skip - 1) == 0;
+ } else {
+ return false;
+ }
+}
+
+_trie_clones
+static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) {
+ uintptr_t ptr = trie->root;
+ if (!ptr) {
+ return NULL;
+ }
+
+ struct trie_leaf *best = NULL;
+ size_t skip = 0;
+ size_t length = strlen(key) + 1;
+
+ size_t offset = 0, limit = 2 * length;
+ while (trie_is_node(ptr)) {
+ struct trie_node *node = trie_decode_node(ptr);
+ offset += node->offset;
+ if (offset >= limit) {
+ return best;
+ }
+
+ struct trie_leaf *leaf = trie_terminal_leaf(node);
+ if (trie_check_prefix(leaf, skip, key, length)) {
+ best = leaf;
+ skip = offset / 2;
+ }
+
+ unsigned char nibble = trie_key_nibble(key, length, offset);
+ unsigned int bit = 1U << nibble;
+ if (node->bitmap & bit) {
+ unsigned int index = count_ones(node->bitmap & (bit - 1));
+ ptr = node->children[index];
+ } else {
+ return best;
+ }
+ }
+
+ struct trie_leaf *leaf = trie_decode_leaf(ptr);
+ if (trie_check_prefix(leaf, skip, key, length)) {
+ best = leaf;
+ }
+
+ return best;
+}
+
+struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
+ return trie_find_prefix_impl(trie, key);
+}
+
+/** Create a new leaf, holding a copy of the given key. */
+static struct trie_leaf *trie_leaf_alloc(struct trie *trie, const void *key, size_t length) {
+ struct trie_leaf *leaf = varena_alloc(&trie->leaves, length);
+ if (!leaf) {
+ return NULL;
+ }
+
+ LIST_ITEM_INIT(leaf);
+ LIST_APPEND(trie, leaf);
+
+ leaf->value = NULL;
+ leaf->length = length;
+ memcpy(leaf->key, key, length);
+
+ return leaf;
+}
+
+/** Free a leaf. */
+static void trie_leaf_free(struct trie *trie, struct trie_leaf *leaf) {
+ LIST_REMOVE(trie, leaf);
+ varena_free(&trie->leaves, leaf, leaf->length);
+}
+
+/** Create a new node. */
+static struct trie_node *trie_node_alloc(struct trie *trie, size_t size) {
+ bfs_assert(has_single_bit(size));
+ return varena_alloc(&trie->nodes, size);
+}
+
+/** Reallocate a trie node. */
+static struct trie_node *trie_node_realloc(struct trie *trie, struct trie_node *node, size_t old_size, size_t new_size) {
+ bfs_assert(has_single_bit(old_size));
+ bfs_assert(has_single_bit(new_size));
+ return varena_realloc(&trie->nodes, node, old_size, new_size);
+}
+
+/** Free a node. */
+static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) {
+ bfs_assert(size == trie_node_size(node));
+ varena_free(&trie->nodes, node, size);
+}
+
+/** Find the offset of the first nibble that differs between two keys. */
+static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t length) {
+ if (!rep) {
+ return 0;
+ }
+
+ if (rep->length < length) {
+ length = rep->length;
+ }
+
+ const char *rep_bytes = rep->key;
+ const char *key_bytes = key;
+
+ size_t ret = 0, i = 0;
+
+#define CHUNK(n) CHUNK_(uint##n##_t, load8_beu##n)
+#define CHUNK_(type, load8) \
+ (length - i >= sizeof(type)) { \
+ type rep_chunk = load8(rep_bytes + i); \
+ type key_chunk = load8(key_bytes + i); \
+ type diff = rep_chunk ^ key_chunk; \
+ ret += leading_zeros(diff) / 4; \
+ if (diff) { \
+ return ret; \
+ } \
+ i += sizeof(type); \
+ }
+
+#if SIZE_WIDTH >= 64
+ while CHUNK(64);
+ if CHUNK(32);
+#else
+ while CHUNK(32);
+#endif
+ if CHUNK(16);
+ if CHUNK(8);
+
+#undef CHUNK_
+#undef CHUNK
+
+ return ret;
+}
+
+/**
+ * Insert a leaf into a node. The node must not have a child in that position
+ * already. Effectively takes a subtrie like this:
+ *
+ * ptr
+ * |
+ * v X
+ * *--->...
+ * | Z
+ * +--->...
+ *
+ * and transforms it to:
+ *
+ * ptr
+ * |
+ * v X
+ * *--->...
+ * | Y
+ * +--->leaf
+ * | Z
+ * +--->...
+ */
+_trie_clones
+static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, unsigned char nibble) {
+ struct trie_node *node = trie_decode_node(*ptr);
+ unsigned int size = trie_node_size(node);
+
+ // Double the capacity every power of two
+ if (has_single_bit(size)) {
+ node = trie_node_realloc(trie, node, size, 2 * size);
+ if (!node) {
+ trie_leaf_free(trie, leaf);
+ return NULL;
+ }
+ *ptr = trie_encode_node(node);
+ }
+
+ unsigned int bit = 1U << nibble;
+
+ // The child must not already be present
+ bfs_assert(!(node->bitmap & bit));
+ node->bitmap |= bit;
+
+ unsigned int target = count_ones(node->bitmap & (bit - 1));
+ for (size_t i = size; i > target; --i) {
+ node->children[i] = node->children[i - 1];
+ }
+ node->children[target] = trie_encode_leaf(leaf);
+ return leaf;
+}
+
+/**
+ * When the current offset exceeds OFFSET_MAX, insert "jump" nodes that bridge
+ * the gap. This function takes a subtrie like this:
+ *
+ * ptr
+ * |
+ * v
+ * *--->rep
+ *
+ * and changes it to:
+ *
+ * ptr ret
+ * | |
+ * v v
+ * *--->*--->rep
+ *
+ * so that a new key can be inserted like:
+ *
+ * ptr ret
+ * | |
+ * v v X
+ * *--->*--->rep
+ * | Y
+ * +--->key
+ */
+static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, size_t *offset) {
+ // We only ever need to jump to leaf nodes, since internal nodes are
+ // guaranteed to be within OFFSET_MAX anyway
+ struct trie_leaf *leaf = trie_decode_leaf(*ptr);
+
+ struct trie_node *node = trie_node_alloc(trie, 1);
+ if (!node) {
+ return NULL;
+ }
+
+ *offset += OFFSET_MAX;
+ node->offset = OFFSET_MAX;
+
+ unsigned char nibble = trie_leaf_nibble(leaf, *offset);
+ node->bitmap = 1 << nibble;
+
+ node->children[0] = *ptr;
+ *ptr = trie_encode_node(node);
+ return node->children;
+}
+
+/**
+ * Split a node in the trie. Changes a subtrie like this:
+ *
+ * ptr
+ * |
+ * v
+ * *...>--->rep
+ *
+ * into this:
+ *
+ * ptr
+ * |
+ * v X
+ * *--->*...>--->rep
+ * | Y
+ * +--->leaf
+ */
+static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, struct trie_leaf *rep, size_t offset, size_t mismatch) {
+ unsigned char key_nibble = trie_leaf_nibble(leaf, mismatch);
+ unsigned char rep_nibble = trie_leaf_nibble(rep, mismatch);
+ bfs_assert(key_nibble != rep_nibble);
+
+ struct trie_node *node = trie_node_alloc(trie, 2);
+ if (!node) {
+ trie_leaf_free(trie, leaf);
+ return NULL;
+ }
+
+ node->bitmap = (1 << key_nibble) | (1 << rep_nibble);
+
+ size_t delta = mismatch - offset;
+ if (trie_is_node(*ptr)) {
+ struct trie_node *child = trie_decode_node(*ptr);
+ child->offset -= delta;
+ }
+ node->offset = delta;
+
+ unsigned int key_index = key_nibble > rep_nibble;
+ node->children[key_index] = trie_encode_leaf(leaf);
+ node->children[key_index ^ 1] = *ptr;
+ *ptr = trie_encode_node(node);
+ return leaf;
+}
+
+struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) {
+ return trie_insert_mem(trie, key, strlen(key) + 1);
+}
+
+_trie_clones
+static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) {
+ struct trie_leaf *rep = trie_representative(trie, key, length);
+ size_t mismatch = trie_mismatch(rep, key, length);
+ size_t misbyte = mismatch / 2;
+ if (misbyte >= length) {
+ bfs_assert(misbyte == length);
+ return rep;
+ } else if (rep && misbyte >= rep->length) {
+ bfs_bug("trie keys must be prefix-free");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ struct trie_leaf *leaf = trie_leaf_alloc(trie, key, length);
+ if (!leaf) {
+ return NULL;
+ }
+
+ if (!rep) {
+ trie->root = trie_encode_leaf(leaf);
+ return leaf;
+ }
+
+ size_t offset = 0;
+ uintptr_t *ptr = &trie->root;
+ while (trie_is_node(*ptr)) {
+ struct trie_node *node = trie_decode_node(*ptr);
+ if (offset + node->offset > mismatch) {
+ break;
+ }
+ offset += node->offset;
+
+ unsigned char nibble = trie_leaf_nibble(leaf, offset);
+ unsigned int bit = 1U << nibble;
+ if (node->bitmap & bit) {
+ bfs_assert(offset < mismatch);
+ unsigned int index = count_ones(node->bitmap & (bit - 1));
+ ptr = &node->children[index];
+ } else {
+ bfs_assert(offset == mismatch);
+ return trie_node_insert(trie, ptr, leaf, nibble);
+ }
+ }
+
+ while (mismatch - offset > OFFSET_MAX) {
+ ptr = trie_jump(trie, ptr, &offset);
+ if (!ptr) {
+ trie_leaf_free(trie, leaf);
+ return NULL;
+ }
+ }
+
+ return trie_split(trie, ptr, leaf, rep, offset, mismatch);
+}
+
+struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) {
+ return trie_insert_mem_impl(trie, key, length);
+}
+
+int trie_set_str(struct trie *trie, const char *key, const void *value) {
+ struct trie_leaf *leaf = trie_insert_str(trie, key);
+ if (leaf) {
+ leaf->value = (void *)value;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value) {
+ struct trie_leaf *leaf = trie_insert_mem(trie, key, length);
+ if (leaf) {
+ leaf->value = (void *)value;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+/** Free a chain of singleton nodes. */
+static void trie_free_singletons(struct trie *trie, uintptr_t ptr) {
+ while (trie_is_node(ptr)) {
+ struct trie_node *node = trie_decode_node(ptr);
+
+ // Make sure the bitmap is a power of two, i.e. it has just one child
+ bfs_assert(has_single_bit((size_t)node->bitmap));
+
+ ptr = node->children[0];
+ trie_node_free(trie, node, 1);
+ }
+
+ trie_leaf_free(trie, trie_decode_leaf(ptr));
+}
+
+/**
+ * Try to collapse a two-child node like:
+ *
+ * parent child
+ * | |
+ * v v
+ * *----->*----->*----->leaf
+ * |
+ * +----->other
+ *
+ * into
+ *
+ * parent
+ * |
+ * v
+ * other
+ */
+static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) {
+ uintptr_t other = parent_node->children[child_index ^ 1];
+ if (trie_is_node(other)) {
+ struct trie_node *other_node = trie_decode_node(other);
+ if (other_node->offset + parent_node->offset <= OFFSET_MAX) {
+ other_node->offset += parent_node->offset;
+ } else {
+ return -1;
+ }
+ }
+
+ *parent = other;
+ trie_node_free(trie, parent_node, 2);
+ return 0;
+}
+
+_trie_clones
+static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) {
+ uintptr_t *child = &trie->root;
+ uintptr_t *parent = NULL;
+ unsigned int child_bit = 0, child_index = 0;
+ size_t offset = 0;
+ while (trie_is_node(*child)) {
+ struct trie_node *node = trie_decode_node(*child);
+ offset += node->offset;
+
+ unsigned char nibble = trie_leaf_nibble(leaf, offset);
+ unsigned int bit = 1U << nibble;
+ unsigned int bitmap = node->bitmap;
+ bfs_assert(bitmap & bit);
+ unsigned int index = count_ones(bitmap & (bit - 1));
+
+ // Advance the parent pointer, unless this node had only one child
+ if (!has_single_bit(bitmap)) {
+ parent = child;
+ child_bit = bit;
+ child_index = index;
+ }
+
+ child = &node->children[index];
+ }
+
+ bfs_assert(trie_decode_leaf(*child) == leaf);
+
+ if (!parent) {
+ trie_free_singletons(trie, trie->root);
+ trie->root = 0;
+ return;
+ }
+
+ struct trie_node *node = trie_decode_node(*parent);
+ trie_free_singletons(trie, node->children[child_index]);
+
+ unsigned int parent_size = trie_node_size(node);
+ bfs_assert(parent_size > 1);
+ if (parent_size == 2 && trie_collapse_node(trie, parent, node, child_index) == 0) {
+ return;
+ }
+
+ for (size_t i = child_index; i + 1 < parent_size; ++i) {
+ node->children[i] = node->children[i + 1];
+ }
+ node->bitmap &= ~child_bit;
+ --parent_size;
+
+ if (has_single_bit(parent_size)) {
+ node = trie_node_realloc(trie, node, 2 * parent_size, parent_size);
+ if (node) {
+ *parent = trie_encode_node(node);
+ }
+ }
+}
+
+void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
+ trie_remove_impl(trie, leaf);
+}
+
+void trie_clear(struct trie *trie) {
+ trie->root = 0;
+ LIST_INIT(trie);
+
+ varena_clear(&trie->leaves);
+ varena_clear(&trie->nodes);
+}
+
+void trie_destroy(struct trie *trie) {
+ varena_destroy(&trie->leaves);
+ varena_destroy(&trie->nodes);
+}
diff --git a/trie.h b/src/trie.h
index 2d29ac7..19bd81d 100644
--- a/trie.h
+++ b/src/trie.h
@@ -1,50 +1,41 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#ifndef BFS_TRIE_H
#define BFS_TRIE_H
+#include "alloc.h"
+#include "list.h"
+
#include <stddef.h>
#include <stdint.h>
/**
- * A trie that holds a set of fixed- or variable-length strings.
- */
-struct trie {
- uintptr_t root;
-};
-
-/**
* A leaf of a trie.
*/
struct trie_leaf {
- /**
- * An arbitrary value associated with this leaf.
- */
+ /** Linked list of leaves, in insertion order. */
+ struct trie_leaf *prev, *next;
+ /** An arbitrary value associated with this leaf. */
void *value;
-
- /**
- * The length of the key in bytes.
- */
+ /** The length of the key in bytes. */
size_t length;
+ /** The key itself, stored inline. */
+ char key[] _counted_by(length);
+};
- /**
- * The key itself, stored inline.
- */
- char key[];
+/**
+ * A trie that holds a set of fixed- or variable-length strings.
+ */
+struct trie {
+ /** Pointer to the root node/leaf. */
+ uintptr_t root;
+ /** Linked list of leaves. */
+ struct trie_leaf *head, *tail;
+ /** Node allocator. */
+ struct varena nodes;
+ /** Leaf allocator. */
+ struct varena leaves;
};
/**
@@ -53,47 +44,63 @@ struct trie_leaf {
void trie_init(struct trie *trie);
/**
- * Get the first (lexicographically earliest) leaf in the trie.
+ * Find the leaf for a string key.
*
- * @param trie
+ * @trie
* The trie to search.
+ * @key
+ * The key to look up.
* @return
- * The first leaf, or NULL if the trie is empty.
+ * The found leaf, or NULL if the key is not present.
*/
-struct trie_leaf *trie_first_leaf(const struct trie *trie);
+struct trie_leaf *trie_find_str(const struct trie *trie, const char *key);
/**
- * Find the leaf for a string key.
+ * Find the leaf for a fixed-size key.
*
- * @param trie
+ * @trie
* The trie to search.
- * @param key
+ * @key
* The key to look up.
+ * @length
+ * The length of the key in bytes.
* @return
* The found leaf, or NULL if the key is not present.
*/
-struct trie_leaf *trie_find_str(const struct trie *trie, const char *key);
+struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length);
/**
- * Find the leaf for a fixed-size key.
+ * Get the value associated with a string key.
*
- * @param trie
+ * @trie
* The trie to search.
- * @param key
+ * @key
* The key to look up.
- * @param length
+ * @return
+ * The found value, or NULL if the key is not present.
+ */
+void *trie_get_str(const struct trie *trie, const char *key);
+
+/**
+ * Get the value associated with a fixed-size key.
+ *
+ * @trie
+ * The trie to search.
+ * @key
+ * The key to look up.
+ * @length
* The length of the key in bytes.
* @return
- * The found leaf, or NULL if the key is not present.
+ * The found value, or NULL if the key is not present.
*/
-struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length);
+void *trie_get_mem(const struct trie *trie, const void *key, size_t length);
/**
* Find the shortest leaf that starts with a given key.
*
- * @param trie
+ * @trie
* The trie to search.
- * @param key
+ * @key
* The key to look up.
* @return
* A leaf that starts with the given key, or NULL.
@@ -103,9 +110,9 @@ struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key);
/**
* Find the leaf that is the longest prefix of the given key.
*
- * @param trie
+ * @trie
* The trie to search.
- * @param key
+ * @key
* The key to look up.
* @return
* The longest prefix match for the given key, or NULL.
@@ -115,9 +122,9 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key);
/**
* Insert a string key into the trie.
*
- * @param trie
+ * @trie
* The trie to modify.
- * @param key
+ * @key
* The key to insert.
* @return
* The inserted leaf, or NULL on failure.
@@ -127,11 +134,11 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key);
/**
* Insert a fixed-size key into the trie.
*
- * @param trie
+ * @trie
* The trie to modify.
- * @param key
+ * @key
* The key to insert.
- * @param length
+ * @length
* The length of the key in bytes.
* @return
* The inserted leaf, or NULL on failure.
@@ -139,18 +146,59 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key);
struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length);
/**
+ * Set the value for a string key.
+ *
+ * @trie
+ * The trie to modify.
+ * @key
+ * The key to insert.
+ * @value
+ * The value to set.
+ * @return
+ * 0 on success, -1 on error.
+ */
+int trie_set_str(struct trie *trie, const char *key, const void *value);
+
+/**
+ * Set the value for a fixed-size key.
+ *
+ * @trie
+ * The trie to modify.
+ * @key
+ * The key to insert.
+ * @length
+ * The length of the key in bytes.
+ * @value
+ * The value to set.
+ * @return
+ * 0 on success, -1 on error.
+ */
+int trie_set_mem(struct trie *trie, const void *key, size_t length, const void *value);
+
+/**
* Remove a leaf from a trie.
*
- * @param trie
+ * @trie
* The trie to modify.
- * @param leaf
+ * @leaf
* The leaf to remove.
*/
void trie_remove(struct trie *trie, struct trie_leaf *leaf);
/**
+ * Remove all leaves from a trie.
+ */
+void trie_clear(struct trie *trie);
+
+/**
* Destroy a trie and its contents.
*/
void trie_destroy(struct trie *trie);
+/**
+ * Iterate over the leaves of a trie.
+ */
+#define for_trie(leaf, trie) \
+ for_list (struct trie_leaf, leaf, trie)
+
#endif // BFS_TRIE_H
diff --git a/typo.c b/src/typo.c
index 4012730..7b359c4 100644
--- a/typo.c
+++ b/src/typo.c
@@ -1,26 +1,15 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#include "typo.h"
+
#include <limits.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Assume QWERTY layout for now
-static const int key_coords[UCHAR_MAX][3] = {
+static const int8_t key_coords[UCHAR_MAX + 1][3] = {
['`'] = { 0, 0, 0},
['~'] = { 0, 0, 1},
['1'] = { 3, 0, 0},
@@ -125,7 +114,7 @@ static const int key_coords[UCHAR_MAX][3] = {
};
static int char_distance(char a, char b) {
- const int *ac = key_coords[(unsigned char)a], *bc = key_coords[(unsigned char)b];
+ const int8_t *ac = key_coords[(unsigned char)a], *bc = key_coords[(unsigned char)b];
int ret = 0;
for (int i = 0; i < 3; ++i) {
ret += abs(ac[i] - bc[i]);
diff --git a/src/typo.h b/src/typo.h
new file mode 100644
index 0000000..b0daaf1
--- /dev/null
+++ b/src/typo.h
@@ -0,0 +1,18 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#ifndef BFS_TYPO_H
+#define BFS_TYPO_H
+
+/**
+ * Find the "typo" distance between two strings.
+ *
+ * @actual
+ * The actual string typed by the user.
+ * @expected
+ * The expected valid string.
+ * @return The distance between the two strings.
+ */
+int typo_distance(const char *actual, const char *expected);
+
+#endif // BFS_TYPO_H
diff --git a/src/version.c b/src/version.c
new file mode 100644
index 0000000..7479a9f
--- /dev/null
+++ b/src/version.c
@@ -0,0 +1,32 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "bfs.h"
+
+const char bfs_version[] = {
+#include "version.i"
+};
+
+const char bfs_confflags[] = {
+#include "confflags.i"
+};
+
+const char bfs_cc[] = {
+#include "cc.i"
+};
+
+const char bfs_cppflags[] = {
+#include "cppflags.i"
+};
+
+const char bfs_cflags[] = {
+#include "cflags.i"
+};
+
+const char bfs_ldflags[] = {
+#include "ldflags.i"
+};
+
+const char bfs_ldlibs[] = {
+#include "ldlibs.i"
+};
diff --git a/src/xregex.c b/src/xregex.c
new file mode 100644
index 0000000..796544e
--- /dev/null
+++ b/src/xregex.c
@@ -0,0 +1,344 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "xregex.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+#include "thread.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if BFS_WITH_ONIGURUMA
+# include <langinfo.h>
+# include <oniguruma.h>
+#else
+# include <regex.h>
+#endif
+
+struct bfs_regex {
+#if BFS_WITH_ONIGURUMA
+ unsigned char *pattern;
+ OnigRegex impl;
+ int err;
+ OnigErrorInfo einfo;
+#else
+ regex_t impl;
+ int err;
+#endif
+};
+
+#if BFS_WITH_ONIGURUMA
+
+static int bfs_onig_status;
+static OnigEncoding bfs_onig_enc;
+
+static OnigSyntaxType bfs_onig_syntax_awk;
+static OnigSyntaxType bfs_onig_syntax_gnu_awk;
+static OnigSyntaxType bfs_onig_syntax_emacs;
+static OnigSyntaxType bfs_onig_syntax_egrep;
+static OnigSyntaxType bfs_onig_syntax_gnu_find;
+
+/** pthread_once() callback. */
+static void bfs_onig_once(void) {
+ // Fall back to ASCII by default
+ bfs_onig_enc = ONIG_ENCODING_ASCII;
+
+ // Oniguruma has no locale support, so try to guess the right encoding
+ // from the current locale.
+ const char *charmap = nl_langinfo(CODESET);
+ if (charmap) {
+#define BFS_MAP_ENCODING(name, value) \
+ do { \
+ if (strcmp(charmap, name) == 0) { \
+ bfs_onig_enc = value; \
+ } \
+ } while (0)
+#define BFS_MAP_ENCODING2(name1, name2, value) \
+ do { \
+ BFS_MAP_ENCODING(name1, value); \
+ BFS_MAP_ENCODING(name2, value); \
+ } while (0)
+
+ // These names were found with locale -m on Linux and FreeBSD
+#define BFS_MAP_ISO_8859(n) \
+ BFS_MAP_ENCODING2("ISO-8859-" #n, "ISO8859-" #n, ONIG_ENCODING_ISO_8859_ ## n)
+
+ BFS_MAP_ISO_8859(1);
+ BFS_MAP_ISO_8859(2);
+ BFS_MAP_ISO_8859(3);
+ BFS_MAP_ISO_8859(4);
+ BFS_MAP_ISO_8859(5);
+ BFS_MAP_ISO_8859(6);
+ BFS_MAP_ISO_8859(7);
+ BFS_MAP_ISO_8859(8);
+ BFS_MAP_ISO_8859(9);
+ BFS_MAP_ISO_8859(10);
+ BFS_MAP_ISO_8859(11);
+ // BFS_MAP_ISO_8859(12);
+ BFS_MAP_ISO_8859(13);
+ BFS_MAP_ISO_8859(14);
+ BFS_MAP_ISO_8859(15);
+ BFS_MAP_ISO_8859(16);
+
+ BFS_MAP_ENCODING("UTF-8", ONIG_ENCODING_UTF8);
+
+#define BFS_MAP_EUC(name) \
+ BFS_MAP_ENCODING2("EUC-" #name, "euc" #name, ONIG_ENCODING_EUC_ ## name)
+
+ BFS_MAP_EUC(JP);
+ BFS_MAP_EUC(TW);
+ BFS_MAP_EUC(KR);
+ BFS_MAP_EUC(CN);
+
+ BFS_MAP_ENCODING2("SHIFT_JIS", "SJIS", ONIG_ENCODING_SJIS);
+
+ // BFS_MAP_ENCODING("KOI-8", ONIG_ENCODING_KOI8);
+ BFS_MAP_ENCODING("KOI8-R", ONIG_ENCODING_KOI8_R);
+
+ BFS_MAP_ENCODING("CP1251", ONIG_ENCODING_CP1251);
+
+ BFS_MAP_ENCODING("GB18030", ONIG_ENCODING_BIG5);
+ }
+
+ bfs_onig_status = onig_initialize(&bfs_onig_enc, 1);
+ 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. */
+static int bfs_onig_initialize(OnigEncoding *enc) {
+ static pthread_once_t once = PTHREAD_ONCE_INIT;
+ invoke_once(&once, bfs_onig_once);
+
+ *enc = bfs_onig_enc;
+ return bfs_onig_status;
+}
+#endif
+
+int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags) {
+ struct bfs_regex *regex = *preg = ALLOC(struct bfs_regex);
+ if (!regex) {
+ return -1;
+ }
+
+#if BFS_WITH_ONIGURUMA
+ // onig_error_code_to_str() says
+ //
+ // don't call this after the pattern argument of onig_new() is freed
+ //
+ // so make a defensive copy.
+ regex->pattern = (unsigned char *)strdup(pattern);
+ if (!regex->pattern) {
+ goto fail;
+ }
+
+ regex->impl = NULL;
+ regex->err = ONIG_NORMAL;
+
+ OnigSyntaxType *syntax = NULL;
+ switch (type) {
+ case BFS_REGEX_POSIX_BASIC:
+ syntax = ONIG_SYNTAX_POSIX_BASIC;
+ break;
+ 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 = &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");
+
+ OnigOptionType options = syntax->options;
+ if (flags & BFS_REGEX_ICASE) {
+ options |= ONIG_OPTION_IGNORECASE;
+ }
+
+ OnigEncoding enc;
+ regex->err = bfs_onig_initialize(&enc);
+ if (regex->err != ONIG_NORMAL) {
+ return -1;
+ }
+
+ const unsigned char *end = regex->pattern + strlen(pattern);
+ regex->err = onig_new(&regex->impl, regex->pattern, end, options, enc, syntax, &regex->einfo);
+ if (regex->err != ONIG_NORMAL) {
+ return -1;
+ }
+#else
+ int cflags = 0;
+ switch (type) {
+ case BFS_REGEX_POSIX_BASIC:
+ break;
+ case BFS_REGEX_POSIX_EXTENDED:
+ cflags |= REG_EXTENDED;
+ break;
+ default:
+ errno = EINVAL;
+ goto fail;
+ }
+
+ if (flags & BFS_REGEX_ICASE) {
+ cflags |= REG_ICASE;
+ }
+
+ regex->err = regcomp(&regex->impl, pattern, cflags);
+ if (regex->err != 0) {
+ // https://github.com/google/sanitizers/issues/1496
+ sanitize_init(&regex->impl);
+ return -1;
+ }
+#endif
+
+ return 0;
+
+fail:
+ free(regex);
+ *preg = NULL;
+ return -1;
+}
+
+int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) {
+ size_t len = strlen(str);
+
+#if BFS_WITH_ONIGURUMA
+ const unsigned char *ustr = (const unsigned char *)str;
+ const unsigned char *end = ustr + len;
+
+ // The docs for onig_{match,search}() say
+ //
+ // Do not pass invalid byte string in the regex character encoding.
+ if (!onigenc_is_valid_mbc_string(onig_get_encoding(regex->impl), ustr, end)) {
+ return 0;
+ }
+
+ int ret;
+ if (flags & BFS_REGEX_ANCHOR) {
+ ret = onig_match(regex->impl, ustr, end, ustr, NULL, ONIG_OPTION_DEFAULT);
+ } else {
+ ret = onig_search(regex->impl, ustr, end, ustr, end, NULL, ONIG_OPTION_DEFAULT);
+ }
+
+ if (ret >= 0) {
+ if (flags & BFS_REGEX_ANCHOR) {
+ return (size_t)ret == len;
+ } else {
+ return 1;
+ }
+ } else if (ret == ONIG_MISMATCH) {
+ return 0;
+ } else {
+ regex->err = ret;
+ return -1;
+ }
+#else
+ regmatch_t match = {
+ .rm_so = 0,
+ .rm_eo = len,
+ };
+
+ int eflags = 0;
+#ifdef REG_STARTEND
+ eflags |= REG_STARTEND;
+#endif
+
+ int ret = regexec(&regex->impl, str, 1, &match, eflags);
+ if (ret == 0) {
+ if (flags & BFS_REGEX_ANCHOR) {
+ return match.rm_so == 0 && (size_t)match.rm_eo == len;
+ } else {
+ return 1;
+ }
+ } else if (ret == REG_NOMATCH) {
+ return 0;
+ } else {
+ regex->err = ret;
+ return -1;
+ }
+#endif
+}
+
+void bfs_regfree(struct bfs_regex *regex) {
+ if (regex) {
+#if BFS_WITH_ONIGURUMA
+ onig_free(regex->impl);
+ free(regex->pattern);
+#else
+ regfree(&regex->impl);
+#endif
+ free(regex);
+ }
+}
+
+char *bfs_regerror(const struct bfs_regex *regex) {
+ if (!regex) {
+ return strdup(xstrerror(ENOMEM));
+ }
+
+#if BFS_WITH_ONIGURUMA
+ unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN);
+ if (str) {
+ onig_error_code_to_str(str, regex->err, &regex->einfo);
+ }
+ return (char *)str;
+#else
+ size_t len = regerror(regex->err, &regex->impl, NULL, 0);
+ char *str = malloc(len);
+ if (str) {
+ regerror(regex->err, &regex->impl, str, len);
+ }
+ return str;
+#endif
+}
diff --git a/src/xregex.h b/src/xregex.h
new file mode 100644
index 0000000..c4504ee
--- /dev/null
+++ b/src/xregex.h
@@ -0,0 +1,87 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
+// SPDX-License-Identifier: 0BSD
+
+#ifndef BFS_XREGEX_H
+#define BFS_XREGEX_H
+
+/**
+ * A compiled regular expression.
+ */
+struct bfs_regex;
+
+/**
+ * Regex syntax flavors.
+ */
+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,
+};
+
+/**
+ * Regex compilation flags.
+ */
+enum bfs_regcomp_flags {
+ /** Treat the regex case-insensitively. */
+ BFS_REGEX_ICASE = 1 << 0,
+};
+
+/**
+ * Regex execution flags.
+ */
+enum bfs_regexec_flags {
+ /** Only treat matches of the entire string as successful. */
+ BFS_REGEX_ANCHOR = 1 << 0,
+};
+
+/**
+ * Wrapper for regcomp() that supports additional regex types.
+ *
+ * @preg[out]
+ * Will hold the compiled regex.
+ * @pattern
+ * The regular expression to compile.
+ * @type
+ * The regular expression syntax to use.
+ * @flags
+ * Regex compilation flags.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags);
+
+/**
+ * Wrapper for regexec().
+ *
+ * @regex
+ * The regular expression to execute.
+ * @str
+ * The string to match against.
+ * @flags
+ * Regex execution flags.
+ * @return
+ * 1 for a match, 0 for no match, -1 on failure.
+ */
+int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags);
+
+/**
+ * Free a compiled regex.
+ */
+void bfs_regfree(struct bfs_regex *regex);
+
+/**
+ * Get a human-readable regex error message.
+ *
+ * @regex
+ * The compiled regex.
+ * @return
+ * A human-readable description of the error, which should be free()'d.
+ */
+char *bfs_regerror(const struct bfs_regex *regex);
+
+#endif // BFS_XREGEX_H
diff --git a/src/xspawn.c b/src/xspawn.c
new file mode 100644
index 0000000..ee62c05
--- /dev/null
+++ b/src/xspawn.c
@@ -0,0 +1,778 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "xspawn.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "list.h"
+#include "sighook.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#if __has_include(<paths.h>)
+# include <paths.h>
+#endif
+
+#if BFS_POSIX_SPAWN >= 0
+# include <spawn.h>
+#endif
+
+/**
+ * Types of spawn actions.
+ */
+enum bfs_spawn_op {
+ BFS_SPAWN_OPEN,
+ BFS_SPAWN_CLOSE,
+ BFS_SPAWN_DUP2,
+ BFS_SPAWN_FCHDIR,
+ BFS_SPAWN_SETRLIMIT,
+};
+
+/**
+ * A spawn action.
+ */
+struct bfs_spawn_action {
+ /** The next action in the list. */
+ struct bfs_spawn_action *next;
+
+ /** This action's operation. */
+ enum bfs_spawn_op op;
+ /** The input fd (or -1). */
+ int in_fd;
+ /** The output fd (or -1). */
+ int out_fd;
+
+ /** Operation-specific args. */
+ union {
+ /** BFS_SPAWN_OPEN args. */
+ struct {
+ const char *path;
+ int flags;
+ mode_t mode;
+ };
+
+ /** BFS_SPAWN_SETRLIMIT args. */
+ struct {
+ int resource;
+ struct rlimit rlimit;
+ };
+ };
+};
+
+int bfs_spawn_init(struct bfs_spawn *ctx) {
+ ctx->flags = 0;
+ SLIST_INIT(ctx);
+
+#if BFS_POSIX_SPAWN >= 0
+ if (sysoption(SPAWN) > 0) {
+ ctx->flags |= BFS_SPAWN_USE_POSIX;
+
+ errno = posix_spawn_file_actions_init(&ctx->actions);
+ if (errno != 0) {
+ return -1;
+ }
+
+ errno = posix_spawnattr_init(&ctx->attr);
+ if (errno != 0) {
+ posix_spawn_file_actions_destroy(&ctx->actions);
+ return -1;
+ }
+ }
+#endif
+
+ return 0;
+}
+
+/**
+ * Clear the BFS_SPAWN_USE_POSIX flag and free the attributes.
+ */
+static void bfs_spawn_clear_posix(struct bfs_spawn *ctx) {
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ ctx->flags &= ~BFS_SPAWN_USE_POSIX;
+
+#if BFS_POSIX_SPAWN >= 0
+ posix_spawnattr_destroy(&ctx->attr);
+ posix_spawn_file_actions_destroy(&ctx->actions);
+#endif
+ }
+}
+
+int bfs_spawn_destroy(struct bfs_spawn *ctx) {
+ bfs_spawn_clear_posix(ctx);
+
+ for_slist (struct bfs_spawn_action, action, ctx) {
+ free(action);
+ }
+
+ return 0;
+}
+
+#if BFS_POSIX_SPAWN >= 0
+/** Set some posix_spawnattr flags. */
+_maybe_unused
+static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) {
+ short prev;
+ errno = posix_spawnattr_getflags(&ctx->attr, &prev);
+ if (errno != 0) {
+ return -1;
+ }
+
+ short next = prev | flags;
+ if (next != prev) {
+ errno = posix_spawnattr_setflags(&ctx->attr, next);
+ if (errno != 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+#endif
+
+/** Allocate a spawn action. */
+static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) {
+ struct bfs_spawn_action *action = ALLOC(struct bfs_spawn_action);
+ if (!action) {
+ return NULL;
+ }
+
+ SLIST_ITEM_INIT(action);
+ action->op = op;
+ action->in_fd = -1;
+ action->out_fd = -1;
+ return action;
+}
+
+int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_OPEN);
+ if (!action) {
+ return -1;
+ }
+
+#if BFS_POSIX_SPAWN >= 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_addopen(&ctx->actions, fd, path, flags, mode);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#endif
+
+ action->out_fd = fd;
+ action->path = path;
+ action->flags = flags;
+ action->mode = mode;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_CLOSE);
+ if (!action) {
+ return -1;
+ }
+
+#if BFS_POSIX_SPAWN >= 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_addclose(&ctx->actions, fd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#endif
+
+ action->out_fd = fd;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_DUP2);
+ if (!action) {
+ return -1;
+ }
+
+#if BFS_POSIX_SPAWN >= 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_adddup2(&ctx->actions, oldfd, newfd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#endif
+
+ action->in_fd = oldfd;
+ action->out_fd = newfd;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+/**
+ * https://www.austingroupbugs.net/view.php?id=1208#c4830 says:
+ *
+ * ... a search of the directories passed as the environment variable
+ * PATH ..., using the working directory of the child process after all
+ * file_actions have been performed.
+ *
+ * but macOS and NetBSD resolve the PATH *before* file_actions (because there
+ * posix_spawn() is its own syscall).
+ */
+#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__)
+
+/**
+ * NetBSD even resolves the executable before file actions with posix_spawn()!
+ */
+#define BFS_POSIX_SPAWN_AFTER_FCHDIR !__NetBSD__
+
+int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR);
+ if (!action) {
+ return -1;
+ }
+
+#if __APPLE__
+ // macOS has a bug that causes EBADF when an fchdir() action refers to a
+ // file opened by the file actions
+ for_slist (struct bfs_spawn_action, prev, ctx) {
+ if (fd == prev->out_fd) {
+ bfs_spawn_clear_posix(ctx);
+ break;
+ }
+ }
+#endif
+
+#if BFS_HAS_POSIX_SPAWN_ADDFCHDIR
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir
+#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np
+#endif
+
+#if BFS_POSIX_SPAWN >= 0 && defined(BFS_POSIX_SPAWN_ADDFCHDIR)
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#else
+ bfs_spawn_clear_posix(ctx);
+#endif
+
+ action->in_fd = fd;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_SETRLIMIT);
+ if (!action) {
+ goto fail;
+ }
+
+#ifdef POSIX_SPAWN_SETRLIMIT
+ if (bfs_spawn_addflags(ctx, POSIX_SPAWN_SETRLIMIT) != 0) {
+ goto fail;
+ }
+
+ errno = posix_spawnattr_setrlimit(&ctx->attr, resource, rl);
+ if (errno != 0) {
+ goto fail;
+ }
+#else
+ bfs_spawn_clear_posix(ctx);
+#endif
+
+ action->resource = resource;
+ action->rlimit = *rl;
+ SLIST_APPEND(ctx, action);
+ return 0;
+
+fail:
+ free(action);
+ return -1;
+}
+
+/**
+ * Context for resolving executables in the $PATH.
+ */
+struct bfs_resolver {
+ /** The executable to spawn. */
+ const char *exe;
+ /** The $PATH to resolve in. */
+ char *path;
+ /** A buffer to hold the resolved path. */
+ char *buf;
+ /** The size of the buffer. */
+ size_t len;
+ /** Whether the executable is already resolved. */
+ bool done;
+ /** Whether to free(path). */
+ bool free;
+};
+
+/** Free a $PATH resolution context. */
+static void bfs_resolve_free(struct bfs_resolver *res) {
+ if (res->free) {
+ free(res->path);
+ }
+ free(res->buf);
+}
+
+/** Get the next component in the $PATH. */
+static bool bfs_resolve_next(const char **path, const char **next, size_t *len) {
+ *path = *next;
+ if (!*path) {
+ return false;
+ }
+
+ *next = strchr(*path, ':');
+ if (*next) {
+ *len = *next - *path;
+ ++*next;
+ } else {
+ *len = strlen(*path);
+ }
+
+ if (*len == 0) {
+ // POSIX 8.3: "A zero-length prefix is a legacy feature that
+ // indicates the current working directory."
+ *path = ".";
+ *len = 1;
+ }
+
+ return true;
+}
+
+/** Finish resolving an executable, potentially from the child process. */
+static int bfs_resolve_late(struct bfs_resolver *res) {
+ if (res->done) {
+ return 0;
+ }
+
+ char *buf = res->buf;
+ char *end = buf + res->len;
+
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ char *cur = xstpencpy(buf, end, path, len);
+ cur = xstpecpy(cur, end, "/");
+ cur = xstpecpy(cur, end, res->exe);
+ if (cur == end) {
+ bfs_bug("PATH resolution buffer too small");
+ errno = ENOMEM;
+ return -1;
+ }
+
+ if (xfaccessat(AT_FDCWD, buf, X_OK) == 0) {
+ res->exe = buf;
+ res->done = true;
+ return 0;
+ }
+ }
+
+ errno = ENOENT;
+ return -1;
+}
+
+/** Check if we can skip path resolution entirely. */
+static bool bfs_can_skip_resolve(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (ctx && !(ctx->flags & BFS_SPAWN_USE_PATH)) {
+ return true;
+ }
+
+ if (strchr(res->exe, '/')) {
+ return true;
+ }
+
+ return false;
+}
+
+/** Check if any $PATH components are relative. */
+static bool bfs_resolve_relative(const struct bfs_resolver *res) {
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ if (path[0] != '/') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** Check if the actions include fchdir(). */
+static bool bfs_spawn_will_chdir(const struct bfs_spawn *ctx) {
+ if (ctx) {
+ for_slist (const struct bfs_spawn_action, action, ctx) {
+ if (action->op == BFS_SPAWN_FCHDIR) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/** Check if we can call xfaccessat() before file actions. */
+static bool bfs_can_access_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (res->exe[0] == '/') {
+ return true;
+ }
+
+ if (bfs_spawn_will_chdir(ctx)) {
+ return false;
+ }
+
+ return true;
+}
+
+/** Check if we can resolve the executable before file actions. */
+static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (!bfs_resolve_relative(res)) {
+ return true;
+ }
+
+ if (bfs_spawn_will_chdir(ctx)) {
+ return false;
+ }
+
+ return true;
+}
+
+/** Get the required path resolution buffer size. */
+static size_t bfs_resolve_capacity(const struct bfs_resolver *res) {
+ size_t max = 0;
+
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ if (len > max) {
+ max = len;
+ }
+ }
+
+ // path + "/" + exe + '\0'
+ return max + 1 + strlen(res->exe) + 1;
+}
+
+/** Begin resolving an executable, from the parent process. */
+static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const struct bfs_spawn *ctx) {
+ *res = (struct bfs_resolver) {
+ .exe = exe,
+ };
+
+ if (bfs_can_skip_resolve(res, ctx)) {
+ if (bfs_can_access_early(res, ctx)) {
+ // Do this check eagerly, even though posix_spawn()/execv() also
+ // would, because:
+ //
+ // - faccessat() is faster than fork()/clone() + execv()
+ // - posix_spawn() is not guaranteed to report ENOENT
+ if (xfaccessat(AT_FDCWD, exe, X_OK) != 0) {
+ return -1;
+ }
+ }
+
+ res->done = true;
+ return 0;
+ }
+
+ res->path = getenv("PATH");
+ if (!res->path) {
+#if defined(_CS_PATH)
+ res->path = xconfstr(_CS_PATH);
+ res->free = true;
+#elif defined(_PATH_DEFPATH)
+ res->path = _PATH_DEFPATH;
+#else
+ errno = ENOENT;
+#endif
+ }
+ if (!res->path) {
+ goto fail;
+ }
+
+ bool can_finish = bfs_can_resolve_early(res, ctx);
+
+#if BFS_POSIX_SPAWNP_AFTER_FCHDIR
+ bool use_posix = ctx && (ctx->flags & BFS_SPAWN_USE_POSIX);
+ if (!can_finish && use_posix) {
+ // posix_spawnp() will do the resolution, so don't bother
+ // allocating a buffer
+ return 0;
+ }
+#endif
+
+ res->len = bfs_resolve_capacity(res);
+ res->buf = malloc(res->len);
+ if (!res->buf) {
+ goto fail;
+ }
+
+ if (can_finish && bfs_resolve_late(res) != 0) {
+ goto fail;
+ }
+
+ return 0;
+
+fail:
+ bfs_resolve_free(res);
+ return -1;
+}
+
+#if BFS_POSIX_SPAWN >= 0
+
+/** bfs_spawn() implementation using posix_spawn(). */
+static pid_t bfs_posix_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ pid_t ret;
+
+ if (res->done) {
+ errno = posix_spawn(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp);
+ } else {
+ errno = posix_spawnp(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp);
+ }
+
+ if (errno != 0) {
+ return -1;
+ }
+
+ return ret;
+}
+
+/** Check if we can use posix_spawn(). */
+static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (!(ctx->flags & BFS_SPAWN_USE_POSIX)) {
+ return false;
+ }
+
+#if !BFS_POSIX_SPAWNP_AFTER_FCHDIR
+ if (!res->done) {
+ return false;
+ }
+#endif
+
+#if !BFS_POSIX_SPAWN_AFTER_FCHDIR
+ if (res->exe[0] != '/' && bfs_spawn_will_chdir(ctx)) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+#endif // BFS_POSIX_SPAWN >= 0
+
+/** Actually exec() the new process. */
+_noreturn
+static void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, const sigset_t *mask, int pipefd[2]) {
+ xclose(pipefd[0]);
+
+ for_slist (const struct bfs_spawn_action, action, ctx) {
+ int fd;
+
+ // Move the error-reporting pipe out of the way if necessary...
+ if (action->out_fd == pipefd[1]) {
+ fd = dup_cloexec(pipefd[1]);
+ if (fd < 0) {
+ goto fail;
+ }
+ xclose(pipefd[1]);
+ pipefd[1] = fd;
+ }
+
+ // ... and pretend the pipe doesn't exist
+ if (action->in_fd == pipefd[1]) {
+ errno = EBADF;
+ goto fail;
+ }
+
+ switch (action->op) {
+ case BFS_SPAWN_OPEN:
+ fd = open(action->path, action->flags, action->mode);
+ if (fd < 0) {
+ goto fail;
+ }
+ if (fd != action->out_fd) {
+ if (dup2(fd, action->out_fd) < 0) {
+ goto fail;
+ }
+ }
+ break;
+ case BFS_SPAWN_CLOSE:
+ if (close(action->out_fd) != 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_DUP2:
+ if (dup2(action->in_fd, action->out_fd) < 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_FCHDIR:
+ if (fchdir(action->in_fd) != 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_SETRLIMIT:
+ if (setrlimit(action->resource, &action->rlimit) != 0) {
+ goto fail;
+ }
+ break;
+ }
+ }
+
+ if (bfs_resolve_late(res) != 0) {
+ goto fail;
+ }
+
+ // Reset signal handlers to their original values before we unblock
+ // signals, so that handlers don't run in both the parent and the child
+ if (sigreset() != 0) {
+ goto fail;
+ }
+
+ // Restore the original signal mask for the child process
+ errno = pthread_sigmask(SIG_SETMASK, mask, NULL);
+ if (errno != 0) {
+ goto fail;
+ }
+
+ execve(res->exe, argv, envp);
+
+fail:;
+ int error = errno;
+
+ // In case of a write error, the parent will still see that we exited
+ // unsuccessfully, but won't know why
+ (void)xwrite(pipefd[1], &error, sizeof(error));
+
+ xclose(pipefd[1]);
+ _Exit(127);
+}
+
+/** bfs_spawn() implementation using fork()/exec(). */
+static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ // Use a pipe to report errors from the child
+ int pipefd[2];
+ if (pipe_cloexec(pipefd) != 0) {
+ return -1;
+ }
+
+ // Block signals before fork() so handlers don't run in the child
+ sigset_t new_mask;
+ if (sigfillset(&new_mask) != 0) {
+ goto fail;
+ }
+ sigset_t old_mask;
+ errno = pthread_sigmask(SIG_BLOCK, &new_mask, &old_mask);
+ if (errno != 0) {
+ goto fail;
+ }
+
+#if BFS_HAS__FORK
+ pid_t pid = _Fork();
+#else
+ pid_t pid = fork();
+#endif
+ if (pid == 0) {
+ // Child
+ bfs_spawn_exec(res, ctx, argv, envp, &old_mask, pipefd);
+ }
+
+ // Restore the original signal mask
+ errno = pthread_sigmask(SIG_SETMASK, &old_mask, NULL);
+ bfs_everify(errno == 0, "pthread_sigmask()");
+
+ if (pid < 0) {
+ // fork() failed
+ goto fail;
+ }
+
+ xclose(pipefd[1]);
+
+ int error;
+ ssize_t nbytes = xread(pipefd[0], &error, sizeof(error));
+ xclose(pipefd[0]);
+ if (nbytes == sizeof(error)) {
+ xwaitpid(pid, NULL, 0);
+ errno = error;
+ return -1;
+ }
+
+ return pid;
+
+fail:
+ close_quietly(pipefd[1]);
+ close_quietly(pipefd[0]);
+ return -1;
+}
+
+/** Call the right bfs_spawn() implementation. */
+static pid_t bfs_spawn_impl(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
+#if BFS_POSIX_SPAWN >= 0
+ if (bfs_use_posix_spawn(res, ctx)) {
+ return bfs_posix_spawn(res, ctx, argv, envp);
+ }
+#endif
+
+ return bfs_fork_spawn(res, ctx, argv, envp);
+}
+
+pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ // execvp()/posix_spawnp() are typically implemented with repeated
+ // execv() calls for each $PATH component until one succeeds. It's
+ // faster to resolve the full path ahead of time.
+ struct bfs_resolver res;
+ if (bfs_resolve_early(&res, exe, ctx) != 0) {
+ return -1;
+ }
+
+ extern char **environ;
+ if (!envp) {
+ envp = environ;
+ }
+
+ pid_t ret = bfs_spawn_impl(&res, ctx, argv, envp);
+ bfs_resolve_free(&res);
+ return ret;
+}
+
+char *bfs_spawn_resolve(const char *exe) {
+ struct bfs_resolver res;
+ if (bfs_resolve_early(&res, exe, NULL) != 0) {
+ return NULL;
+ }
+ if (bfs_resolve_late(&res) != 0) {
+ bfs_resolve_free(&res);
+ return NULL;
+ }
+
+ char *ret;
+ if (res.exe == res.buf) {
+ ret = res.buf;
+ res.buf = NULL;
+ } else {
+ ret = strdup(res.exe);
+ }
+
+ bfs_resolve_free(&res);
+ return ret;
+}
diff --git a/src/xspawn.h b/src/xspawn.h
new file mode 100644
index 0000000..3c74ccd
--- /dev/null
+++ b/src/xspawn.h
@@ -0,0 +1,139 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * A process-spawning library inspired by posix_spawn().
+ */
+
+#ifndef BFS_XSPAWN_H
+#define BFS_XSPAWN_H
+
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef _POSIX_SPAWN
+# define BFS_POSIX_SPAWN _POSIX_SPAWN
+#else
+# define BFS_POSIX_SPAWN (-1)
+#endif
+
+#if BFS_POSIX_SPAWN >= 0
+# include <spawn.h>
+#endif
+
+/**
+ * bfs_spawn() flags.
+ */
+enum bfs_spawn_flags {
+ /** Use the PATH variable to resolve the executable (like execvp()). */
+ BFS_SPAWN_USE_PATH = 1 << 0,
+ /** Whether posix_spawn() can be used. */
+ BFS_SPAWN_USE_POSIX = 1 << 1,
+};
+
+/**
+ * bfs_spawn() attributes, controlling the context of the new process.
+ */
+struct bfs_spawn {
+ /** Spawn flags. */
+ enum bfs_spawn_flags flags;
+
+ /** Linked list of actions. */
+ struct bfs_spawn_action *head;
+ struct bfs_spawn_action **tail;
+
+#if BFS_POSIX_SPAWN >= 0
+ /** posix_spawn() context, for when we can use it. */
+ posix_spawn_file_actions_t actions;
+ posix_spawnattr_t attr;
+#endif
+};
+
+/**
+ * Create a new bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_init(struct bfs_spawn *ctx);
+
+/**
+ * Destroy a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_destroy(struct bfs_spawn *ctx);
+
+/**
+ * Add an open() action to a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode);
+
+/**
+ * Add a close() action to a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd);
+
+/**
+ * Add a dup2() action to a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd);
+
+/**
+ * Add an fchdir() action to a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd);
+
+/**
+ * Apply setrlimit() to a bfs_spawn() context.
+ *
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl);
+
+/**
+ * Spawn a new process.
+ *
+ * @exe
+ * The executable to run.
+ * @ctx
+ * The context for the new process.
+ * @argv
+ * The arguments for the new process.
+ * @envp
+ * The environment variables for the new process (NULL for the current
+ * environment).
+ * @return
+ * The PID of the new process, or -1 on error.
+ */
+pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp);
+
+/**
+ * Look up an executable in the current PATH, as BFS_SPAWN_USE_PATH or execvp()
+ * would do.
+ *
+ * @exe
+ * The name of the binary to execute. Bare names without a '/' will be
+ * searched on the provided PATH.
+ * @return
+ * The full path to the executable, which should be free()'d, or NULL on
+ * failure.
+ */
+char *bfs_spawn_resolve(const char *exe);
+
+#endif // BFS_XSPAWN_H
diff --git a/src/xtime.c b/src/xtime.c
new file mode 100644
index 0000000..6b8a141
--- /dev/null
+++ b/src/xtime.c
@@ -0,0 +1,503 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "xtime.h"
+
+#include "alloc.h"
+#include "bfs.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+int xmktime(struct tm *tm, time_t *timep) {
+ time_t time = mktime(tm);
+
+ if (time == -1) {
+ int error = errno;
+
+ struct tm tmp;
+ if (!localtime_r(&time, &tmp)) {
+ bfs_ebug("localtime_r(-1)");
+ return -1;
+ }
+
+ if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday
+ || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) {
+ errno = error;
+ return -1;
+ }
+ }
+
+ *timep = time;
+ return 0;
+}
+
+// FreeBSD is missing an interceptor
+#if BFS_HAS_TIMEGM && !(__FreeBSD__ && __SANITIZE_MEMORY__)
+
+int xtimegm(struct tm *tm, time_t *timep) {
+ time_t time = timegm(tm);
+
+ if (time == -1) {
+ int error = errno;
+
+ struct tm tmp;
+ if (!gmtime_r(&time, &tmp)) {
+ bfs_ebug("gmtime_r(-1)");
+ return -1;
+ }
+
+ if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday
+ || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) {
+ errno = error;
+ return -1;
+ }
+ }
+
+ *timep = time;
+ return 0;
+}
+
+#else
+
+static int safe_add(int *value, int delta) {
+ if (*value >= 0) {
+ if (delta > INT_MAX - *value) {
+ return -1;
+ }
+ } else {
+ if (delta < INT_MIN - *value) {
+ return -1;
+ }
+ }
+
+ *value += delta;
+ return 0;
+}
+
+static int floor_div(int n, int d) {
+ int a = n < 0;
+ return (n + a) / d - a;
+}
+
+static int wrap(int *value, int max, int *next) {
+ int carry = floor_div(*value, max);
+ *value -= carry * max;
+ return safe_add(next, carry);
+}
+
+static int month_length(int year, int month) {
+ static const int month_lengths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+ int ret = month_lengths[month];
+ if (month == 1 && year % 4 == 0 && (year % 100 != 0 || (year + 300) % 400 == 0)) {
+ ++ret;
+ }
+ return ret;
+}
+
+int xtimegm(struct tm *tm, time_t *timep) {
+ struct tm copy = *tm;
+ copy.tm_isdst = 0;
+
+ if (wrap(&copy.tm_sec, 60, &copy.tm_min) != 0) {
+ goto overflow;
+ }
+ if (wrap(&copy.tm_min, 60, &copy.tm_hour) != 0) {
+ goto overflow;
+ }
+ if (wrap(&copy.tm_hour, 24, &copy.tm_mday) != 0) {
+ goto overflow;
+ }
+
+ // In order to wrap the days of the month, we first need to know what
+ // month it is
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
+ goto overflow;
+ }
+
+ if (copy.tm_mday < 1) {
+ do {
+ --copy.tm_mon;
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
+ goto overflow;
+ }
+
+ copy.tm_mday += month_length(copy.tm_year, copy.tm_mon);
+ } while (copy.tm_mday < 1);
+ } else {
+ while (true) {
+ int days = month_length(copy.tm_year, copy.tm_mon);
+ if (copy.tm_mday <= days) {
+ break;
+ }
+
+ copy.tm_mday -= days;
+ ++copy.tm_mon;
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
+ goto overflow;
+ }
+ }
+ }
+
+ copy.tm_yday = 0;
+ for (int i = 0; i < copy.tm_mon; ++i) {
+ copy.tm_yday += month_length(copy.tm_year, i);
+ }
+ copy.tm_yday += copy.tm_mday - 1;
+
+ int leap_days;
+ // Compute floor((year - 69)/4) - floor((year - 1)/100) + floor((year + 299)/400) without overflows
+ if (copy.tm_year >= 0) {
+ leap_days = floor_div(copy.tm_year - 69, 4) - floor_div(copy.tm_year - 1, 100) + floor_div(copy.tm_year - 101, 400) + 1;
+ } else {
+ leap_days = floor_div(copy.tm_year + 3, 4) - floor_div(copy.tm_year + 99, 100) + floor_div(copy.tm_year + 299, 400) - 17;
+ }
+
+ long long epoch_days = 365LL * (copy.tm_year - 70) + leap_days + copy.tm_yday;
+ copy.tm_wday = (epoch_days + 4) % 7;
+ if (copy.tm_wday < 0) {
+ copy.tm_wday += 7;
+ }
+
+ long long epoch_time = copy.tm_sec + 60 * (copy.tm_min + 60 * (copy.tm_hour + 24 * epoch_days));
+ time_t time = (time_t)epoch_time;
+ if ((long long)time != epoch_time) {
+ goto overflow;
+ }
+
+ *tm = copy;
+ *timep = time;
+ return 0;
+
+overflow:
+ errno = EOVERFLOW;
+ return -1;
+}
+
+#endif // !BFS_HAS_TIMEGM
+
+/** Parse a decimal digit. */
+static int xgetdigit(char c) {
+ int ret = c - '0';
+ if (ret < 0 || ret > 9) {
+ return -1;
+ } else {
+ return ret;
+ }
+}
+
+/** Parse some digits from a timestamp. */
+static int xgetpart(const char **str, size_t n, int *result) {
+ *result = 0;
+
+ for (size_t i = 0; i < n; ++i, ++*str) {
+ int dig = xgetdigit(**str);
+ if (dig < 0) {
+ return -1;
+ }
+ *result *= 10;
+ *result += dig;
+ }
+
+ return 0;
+}
+
+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,
+ };
+
+ int tz_hour = 0;
+ int tz_min = 0;
+ bool tz_negative = false;
+ bool local = true;
+
+ // YYYY
+ if (xgetpart(&str, 4, &tm.tm_year) != 0) {
+ goto invalid;
+ }
+ tm.tm_year -= 1900;
+
+ // MM
+ if (*str == '-') {
+ ++str;
+ }
+ if (xgetpart(&str, 2, &tm.tm_mon) != 0) {
+ goto invalid;
+ }
+ tm.tm_mon -= 1;
+
+ // DD
+ if (*str == '-') {
+ ++str;
+ }
+ if (xgetpart(&str, 2, &tm.tm_mday) != 0) {
+ goto invalid;
+ }
+
+ if (!*str) {
+ goto end;
+ } else if (*str == 'T' || *str == ' ') {
+ ++str;
+ }
+
+ // hh
+ if (xgetpart(&str, 2, &tm.tm_hour) != 0) {
+ goto invalid;
+ }
+
+ // mm
+ if (!*str) {
+ goto end;
+ } else if (*str == ':') {
+ ++str;
+ } else if (xgetdigit(*str) < 0) {
+ goto zone;
+ }
+ if (xgetpart(&str, 2, &tm.tm_min) != 0) {
+ goto invalid;
+ }
+
+ // ss
+ if (!*str) {
+ goto end;
+ } else if (*str == ':') {
+ ++str;
+ } else if (xgetdigit(*str) < 0) {
+ goto zone;
+ }
+ if (xgetpart(&str, 2, &tm.tm_sec) != 0) {
+ goto invalid;
+ }
+
+zone:
+ if (!*str) {
+ goto end;
+ } else if (*str == 'Z') {
+ local = false;
+ ++str;
+ } else if (*str == '+' || *str == '-') {
+ local = false;
+ tz_negative = *str == '-';
+ ++str;
+
+ // hh
+ if (xgetpart(&str, 2, &tz_hour) != 0) {
+ goto invalid;
+ }
+
+ // mm
+ if (!*str) {
+ goto end;
+ } else if (*str == ':') {
+ ++str;
+ }
+ if (xgetpart(&str, 2, &tz_min) != 0) {
+ goto invalid;
+ }
+ } else {
+ goto invalid;
+ }
+
+ if (*str) {
+ goto invalid;
+ }
+
+end:
+ if (local) {
+ if (xmktime(&tm, &result->tv_sec) != 0) {
+ goto error;
+ }
+ } else {
+ if (xtimegm(&tm, &result->tv_sec) != 0) {
+ goto error;
+ }
+
+ int offset = (tz_hour * 60 + tz_min) * 60;
+ if (tz_negative) {
+ result->tv_sec += offset;
+ } else {
+ result->tv_sec -= offset;
+ }
+ }
+
+done:
+ result->tv_nsec = 0;
+ return 0;
+
+invalid:
+ errno = EINVAL;
+error:
+ return -1;
+}
+
+/** One nanosecond. */
+static const long NS = 1000L * 1000 * 1000;
+
+void timespec_add(struct timespec *lhs, const struct timespec *rhs) {
+ lhs->tv_sec += rhs->tv_sec;
+ lhs->tv_nsec += rhs->tv_nsec;
+ if (lhs->tv_nsec >= NS) {
+ lhs->tv_nsec -= NS;
+ lhs->tv_sec += 1;
+ }
+}
+
+void timespec_sub(struct timespec *lhs, const struct timespec *rhs) {
+ lhs->tv_sec -= rhs->tv_sec;
+ lhs->tv_nsec -= rhs->tv_nsec;
+ if (lhs->tv_nsec < 0) {
+ lhs->tv_nsec += NS;
+ lhs->tv_sec -= 1;
+ }
+}
+
+int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs) {
+ if (lhs->tv_sec < rhs->tv_sec) {
+ return -1;
+ } else if (lhs->tv_sec > rhs->tv_sec) {
+ return 1;
+ }
+
+ if (lhs->tv_nsec < rhs->tv_nsec) {
+ return -1;
+ } else if (lhs->tv_nsec > rhs->tv_nsec) {
+ return 1;
+ }
+
+ return 0;
+}
+
+void timespec_min(struct timespec *dest, const struct timespec *src) {
+ if (timespec_cmp(src, dest) < 0) {
+ *dest = *src;
+ }
+}
+
+void timespec_max(struct timespec *dest, const struct timespec *src) {
+ if (timespec_cmp(src, dest) > 0) {
+ *dest = *src;
+ }
+}
+
+double timespec_ns(const struct timespec *ts) {
+ return 1.0e9 * ts->tv_sec + ts->tv_nsec;
+}
+
+#if defined(_POSIX_TIMERS) && BFS_HAS_TIMER_CREATE
+# define BFS_POSIX_TIMERS _POSIX_TIMERS
+#else
+# define BFS_POSIX_TIMERS (-1)
+#endif
+
+struct timer {
+#if BFS_POSIX_TIMERS >= 0
+ /** The POSIX timer. */
+ timer_t timer;
+#endif
+ /** Whether to use timer_create() or setitimer(). */
+ bool legacy;
+};
+
+struct timer *xtimer_start(const struct timespec *interval) {
+ struct timer *timer = ALLOC(struct timer);
+ if (!timer) {
+ return NULL;
+ }
+
+#if BFS_POSIX_TIMERS >= 0
+ if (sysoption(TIMERS)) {
+ clockid_t clock = CLOCK_REALTIME;
+
+#if defined(_POSIX_MONOTONIC_CLOCK) && _POSIX_MONOTONIC_CLOCK >= 0
+ if (sysoption(MONOTONIC_CLOCK) > 0) {
+ clock = CLOCK_MONOTONIC;
+ }
+#endif
+
+ if (timer_create(clock, NULL, &timer->timer) != 0) {
+ goto fail;
+ }
+
+ // https://github.com/llvm/llvm-project/issues/111847
+ sanitize_init(&timer->timer);
+
+ struct itimerspec spec = {
+ .it_value = *interval,
+ .it_interval = *interval,
+ };
+ if (timer_settime(timer->timer, 0, &spec, NULL) != 0) {
+ timer_delete(timer->timer);
+ goto fail;
+ }
+
+ timer->legacy = false;
+ return timer;
+ }
+#endif
+
+#if BFS_POSIX_TIMERS <= 0
+ struct timeval tv = {
+ .tv_sec = interval->tv_sec,
+ .tv_usec = (interval->tv_nsec + 999) / 1000,
+ };
+ struct itimerval ival = {
+ .it_value = tv,
+ .it_interval = tv,
+ };
+ if (setitimer(ITIMER_REAL, &ival, NULL) != 0) {
+ goto fail;
+ }
+
+ timer->legacy = true;
+ return timer;
+#endif
+
+fail:
+ free(timer);
+ return NULL;
+}
+
+void xtimer_stop(struct timer *timer) {
+ if (!timer) {
+ return;
+ }
+
+ if (timer->legacy) {
+#if BFS_POSIX_TIMERS <= 0
+ struct itimerval ival = {0};
+ int ret = setitimer(ITIMER_REAL, &ival, NULL);
+ bfs_everify(ret == 0, "setitimer()");
+#endif
+ } else {
+#if BFS_POSIX_TIMERS >= 0
+ int ret = timer_delete(timer->timer);
+ bfs_everify(ret == 0, "timer_delete()");
+#endif
+ }
+
+ free(timer);
+}
diff --git a/src/xtime.h b/src/xtime.h
new file mode 100644
index 0000000..b76fef2
--- /dev/null
+++ b/src/xtime.h
@@ -0,0 +1,108 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Date/time handling.
+ */
+
+#ifndef BFS_XTIME_H
+#define BFS_XTIME_H
+
+#include <time.h>
+
+/**
+ * mktime() wrapper that reports errors more reliably.
+ *
+ * @tm[in,out]
+ * The struct tm to convert and normalize.
+ * @timep[out]
+ * A pointer to the result.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xmktime(struct tm *tm, time_t *timep);
+
+/**
+ * A portable timegm(), the inverse of gmtime().
+ *
+ * @tm[in,out]
+ * The struct tm to convert and normalize.
+ * @timep[out]
+ * A pointer to the result.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xtimegm(struct tm *tm, time_t *timep);
+
+/**
+ * Parse an ISO 8601-style timestamp.
+ *
+ * @str
+ * The string to parse.
+ * @result[out]
+ * A pointer to the result.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xgetdate(const char *str, struct timespec *result);
+
+/**
+ * Add to a timespec.
+ */
+void timespec_add(struct timespec *lhs, const struct timespec *rhs);
+
+/**
+ * Subtract from a timespec.
+ */
+void timespec_sub(struct timespec *lhs, const struct timespec *rhs);
+
+/**
+ * Compare two timespecs.
+ *
+ * @return
+ * An integer with the sign of (*lhs - *rhs).
+ */
+int timespec_cmp(const struct timespec *lhs, const struct timespec *rhs);
+
+/**
+ * Update a minimum timespec.
+ */
+void timespec_min(struct timespec *dest, const struct timespec *src);
+
+/**
+ * Update a maximum timespec.
+ */
+void timespec_max(struct timespec *dest, const struct timespec *src);
+
+/**
+ * Convert a timespec to floating point.
+ *
+ * @return
+ * The value in nanoseconds.
+ */
+double timespec_ns(const struct timespec *ts);
+
+/**
+ * A timer.
+ */
+struct timer;
+
+/**
+ * Start a timer.
+ *
+ * @interval
+ * The regular interval at which to send SIGALRM.
+ * @return
+ * The new timer on success, otherwise NULL.
+ */
+struct timer *xtimer_start(const struct timespec *interval);
+
+/**
+ * Stop a timer.
+ *
+ * @timer
+ * The timer to stop.
+ */
+void xtimer_stop(struct timer *timer);
+
+#endif // BFS_XTIME_H
diff --git a/stat.c b/stat.c
deleted file mode 100644
index 6042ffb..0000000
--- a/stat.c
+++ /dev/null
@@ -1,375 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "stat.h"
-#include "util.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
-#endif
-
-#ifdef STATX_BASIC_STATS
-# define HAVE_STATX true
-#elif __linux__
-# include <linux/stat.h>
-# include <sys/syscall.h>
-# include <unistd.h>
-#endif
-
-#if HAVE_STATX || defined(__NR_statx)
-# define HAVE_BFS_STATX true
-#endif
-
-#if __APPLE__
-# define st_atim st_atimespec
-# define st_ctim st_ctimespec
-# define st_mtim st_mtimespec
-# define st_birthtim st_birthtimespec
-#endif
-
-const char *bfs_stat_field_name(enum bfs_stat_field field) {
- switch (field) {
- case BFS_STAT_DEV:
- return "device number";
- case BFS_STAT_INO:
- return "inode nunmber";
- case BFS_STAT_TYPE:
- return "type";
- case BFS_STAT_MODE:
- return "mode";
- case BFS_STAT_NLINK:
- return "link count";
- case BFS_STAT_GID:
- return "group ID";
- case BFS_STAT_UID:
- return "user ID";
- case BFS_STAT_SIZE:
- return "size";
- case BFS_STAT_BLOCKS:
- return "block count";
- case BFS_STAT_RDEV:
- return "underlying device";
- case BFS_STAT_ATTRS:
- return "attributes";
- case BFS_STAT_ATIME:
- return "access time";
- case BFS_STAT_BTIME:
- return "birth time";
- case BFS_STAT_CTIME:
- return "change time";
- case BFS_STAT_MTIME:
- return "modification time";
- }
-
- assert(!"Unrecognized stat field");
- return "???";
-}
-
-/**
- * Check if we should retry a failed stat() due to a potentially broken link.
- */
-static bool bfs_stat_retry(int ret, enum bfs_stat_flags flags) {
- return ret != 0
- && (flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW
- && is_nonexistence_error(errno);
-}
-
-/**
- * Convert a struct stat to a struct bfs_stat.
- */
-static void bfs_stat_convert(const struct stat *statbuf, struct bfs_stat *buf) {
- buf->mask = 0;
-
- buf->dev = statbuf->st_dev;
- buf->mask |= BFS_STAT_DEV;
-
- buf->ino = statbuf->st_ino;
- buf->mask |= BFS_STAT_INO;
-
- buf->mode = statbuf->st_mode;
- buf->mask |= BFS_STAT_TYPE | BFS_STAT_MODE;
-
- buf->nlink = statbuf->st_nlink;
- buf->mask |= BFS_STAT_NLINK;
-
- buf->gid = statbuf->st_gid;
- buf->mask |= BFS_STAT_GID;
-
- buf->uid = statbuf->st_uid;
- buf->mask |= BFS_STAT_UID;
-
- buf->size = statbuf->st_size;
- buf->mask |= BFS_STAT_SIZE;
-
- buf->blocks = statbuf->st_blocks;
- buf->mask |= BFS_STAT_BLOCKS;
-
- buf->rdev = statbuf->st_rdev;
- buf->mask |= BFS_STAT_RDEV;
-
-#if BSD
- buf->attrs = statbuf->st_flags;
- buf->mask |= BFS_STAT_ATTRS;
-#endif
-
- buf->atime = statbuf->st_atim;
- buf->mask |= BFS_STAT_ATIME;
-
- buf->ctime = statbuf->st_ctim;
- buf->mask |= BFS_STAT_CTIME;
-
- buf->mtime = statbuf->st_mtim;
- buf->mask |= BFS_STAT_MTIME;
-
-#if __APPLE__ || __FreeBSD__ || __NetBSD__
- buf->btime = statbuf->st_birthtim;
- buf->mask |= BFS_STAT_BTIME;
-#endif
-}
-
-/**
- * bfs_stat() implementation backed by stat().
- */
-static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) {
- struct stat statbuf;
- int ret = fstatat(at_fd, at_path, &statbuf, at_flags);
-
- if (bfs_stat_retry(ret, flags)) {
- at_flags |= AT_SYMLINK_NOFOLLOW;
- ret = fstatat(at_fd, at_path, &statbuf, at_flags);
- }
-
- if (ret == 0) {
- bfs_stat_convert(&statbuf, buf);
- }
-
- return ret;
-}
-
-#if HAVE_BFS_STATX
-
-/**
- * Wrapper for the statx() system call, which had no glibc wrapper prior to 2.28.
- */
-static int bfs_statx(int at_fd, const char *at_path, int at_flags, unsigned int mask, struct statx *buf) {
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // -fsanitize=memory doesn't know about statx(), so tell it the memory
- // got initialized
- memset(buf, 0, sizeof(*buf));
-#endif
-
-#if HAVE_STATX
- return statx(at_fd, at_path, at_flags, mask, buf);
-#else
- return syscall(__NR_statx, at_fd, at_path, at_flags, mask, buf);
-#endif
-}
-
-/**
- * bfs_stat() implementation backed by statx().
- */
-static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) {
- unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
- struct statx xbuf;
- int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf);
-
- if (bfs_stat_retry(ret, flags)) {
- at_flags |= AT_SYMLINK_NOFOLLOW;
- ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf);
- }
-
- if (ret != 0) {
- return ret;
- }
-
- // Callers shouldn't have to check anything except the times
- const unsigned int guaranteed = STATX_BASIC_STATS ^ (STATX_ATIME | STATX_CTIME | STATX_MTIME);
- if ((xbuf.stx_mask & guaranteed) != guaranteed) {
- errno = ENOTSUP;
- return -1;
- }
-
- buf->mask = 0;
-
- buf->dev = bfs_makedev(xbuf.stx_dev_major, xbuf.stx_dev_minor);
- buf->mask |= BFS_STAT_DEV;
-
- if (xbuf.stx_mask & STATX_INO) {
- buf->ino = xbuf.stx_ino;
- buf->mask |= BFS_STAT_INO;
- }
-
- buf->mode = xbuf.stx_mode;
- if (xbuf.stx_mask & STATX_TYPE) {
- buf->mask |= BFS_STAT_TYPE;
- }
- if (xbuf.stx_mask & STATX_MODE) {
- buf->mask |= BFS_STAT_MODE;
- }
-
- if (xbuf.stx_mask & STATX_NLINK) {
- buf->nlink = xbuf.stx_nlink;
- buf->mask |= BFS_STAT_NLINK;
- }
-
- if (xbuf.stx_mask & STATX_GID) {
- buf->gid = xbuf.stx_gid;
- buf->mask |= BFS_STAT_GID;
- }
-
- if (xbuf.stx_mask & STATX_UID) {
- buf->uid = xbuf.stx_uid;
- buf->mask |= BFS_STAT_UID;
- }
-
- if (xbuf.stx_mask & STATX_SIZE) {
- buf->size = xbuf.stx_size;
- buf->mask |= BFS_STAT_SIZE;
- }
-
- if (xbuf.stx_mask & STATX_BLOCKS) {
- buf->blocks = xbuf.stx_blocks;
- buf->mask |= BFS_STAT_BLOCKS;
- }
-
- buf->rdev = bfs_makedev(xbuf.stx_rdev_major, xbuf.stx_rdev_minor);
- buf->mask |= BFS_STAT_RDEV;
-
- buf->attrs = xbuf.stx_attributes;
- buf->mask |= BFS_STAT_ATTRS;
-
- if (xbuf.stx_mask & STATX_ATIME) {
- buf->atime.tv_sec = xbuf.stx_atime.tv_sec;
- buf->atime.tv_nsec = xbuf.stx_atime.tv_nsec;
- buf->mask |= BFS_STAT_ATIME;
- }
-
- if (xbuf.stx_mask & STATX_BTIME) {
- buf->btime.tv_sec = xbuf.stx_btime.tv_sec;
- buf->btime.tv_nsec = xbuf.stx_btime.tv_nsec;
- buf->mask |= BFS_STAT_BTIME;
- }
-
- if (xbuf.stx_mask & STATX_CTIME) {
- buf->ctime.tv_sec = xbuf.stx_ctime.tv_sec;
- buf->ctime.tv_nsec = xbuf.stx_ctime.tv_nsec;
- buf->mask |= BFS_STAT_CTIME;
- }
-
- if (xbuf.stx_mask & STATX_MTIME) {
- buf->mtime.tv_sec = xbuf.stx_mtime.tv_sec;
- buf->mtime.tv_nsec = xbuf.stx_mtime.tv_nsec;
- buf->mask |= BFS_STAT_MTIME;
- }
-
- return ret;
-}
-
-#endif // HAVE_BFS_STATX
-
-/**
- * Allows calling stat with custom at_flags.
- */
-static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags flags, struct bfs_stat *buf) {
-#if HAVE_BFS_STATX
- static bool has_statx = true;
-
- if (has_statx) {
- int ret = bfs_statx_impl(at_fd, at_path, at_flags, flags, buf);
- // EPERM is commonly returned in a seccomp() sandbox that does
- // not allow statx()
- if (ret != 0 && (errno == ENOSYS || errno == EPERM)) {
- has_statx = false;
- } else {
- return ret;
- }
- }
-#endif
-
- return bfs_stat_impl(at_fd, at_path, at_flags, flags, buf);
-}
-
-int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf) {
- int at_flags = 0;
- if (flags & BFS_STAT_NOFOLLOW) {
- at_flags |= AT_SYMLINK_NOFOLLOW;
- }
-
-#ifdef AT_STATX_DONT_SYNC
- if (flags & BFS_STAT_NOSYNC) {
- at_flags |= AT_STATX_DONT_SYNC;
- }
-#endif
-
- if (at_path) {
- return bfs_stat_explicit(at_fd, at_path, at_flags, flags, buf);
- }
-
-#ifdef AT_EMPTY_PATH
- static bool has_at_ep = true;
- if (has_at_ep) {
- at_flags |= AT_EMPTY_PATH;
- int ret = bfs_stat_explicit(at_fd, "", at_flags, flags, buf);
- if (ret != 0 && errno == EINVAL) {
- has_at_ep = false;
- } else {
- return ret;
- }
- }
-#endif
-
- struct stat statbuf;
- if (fstat(at_fd, &statbuf) == 0) {
- bfs_stat_convert(&statbuf, buf);
- return 0;
- } else {
- return -1;
- }
-}
-
-const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field) {
- if (!(buf->mask & field)) {
- errno = ENOTSUP;
- return NULL;
- }
-
- switch (field) {
- case BFS_STAT_ATIME:
- return &buf->atime;
- case BFS_STAT_BTIME:
- return &buf->btime;
- case BFS_STAT_CTIME:
- return &buf->ctime;
- case BFS_STAT_MTIME:
- return &buf->mtime;
- default:
- assert(!"Invalid stat field for time");
- errno = EINVAL;
- return NULL;
- }
-}
-
-void bfs_stat_id(const struct bfs_stat *buf, bfs_file_id *id) {
- memcpy(*id, &buf->dev, sizeof(buf->dev));
- memcpy(*id + sizeof(buf->dev), &buf->ino, sizeof(buf->ino));
-}
diff --git a/tests.sh b/tests.sh
deleted file mode 100755
index 0b2d931..0000000
--- a/tests.sh
+++ /dev/null
@@ -1,3132 +0,0 @@
-#!/usr/bin/env bash
-
-############################################################################
-# bfs #
-# Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
-
-set -e
-set -o physical
-umask 022
-
-export LC_ALL=C
-export TZ=UTC
-
-if [ -t 1 ]; then
- BLD="$(printf '\033[01m')"
- RED="$(printf '\033[01;31m')"
- GRN="$(printf '\033[01;32m')"
- YLW="$(printf '\033[01;33m')"
- BLU="$(printf '\033[01;34m')"
- MAG="$(printf '\033[01;35m')"
- CYN="$(printf '\033[01;36m')"
- RST="$(printf '\033[0m')"
-fi
-
-if command -v capsh &>/dev/null; then
- if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
- if [ -n "$BFS_TRIED_DROP" ]; then
- cat >&2 <<EOF
-${RED}error:${RST} Failed to drop capabilities.
-EOF
-
- exit 1
- fi
-
- cat >&2 <<EOF
-${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
-${BLD}cap_dac_read_search${RST}.
-
-EOF
-
- BFS_TRIED_DROP=y exec capsh \
- --drop=cap_dac_override,cap_dac_read_search \
- --caps=cap_dac_override,cap_dac_read_search-eip \
- -- "$0" "$@"
- fi
-elif [ "$EUID" -eq 0 ]; then
- UNLESS=
- if [ "$(uname)" = "Linux" ]; then
- UNLESS=" unless ${GRN}capsh${RST} is installed"
- fi
-
- cat >&2 <<EOF
-${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
-will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
-EOF
- exit 1
-fi
-
-function usage() {
- local pad=$(printf "%*s" ${#0} "")
- cat <<EOF
-Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLU}--sudo${RST}]
- $pad [${BLU}--noclean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}] [${BLU}--help${RST}]
- $pad [${BLD}test_*${RST} [${BLD}test_*${RST} ...]]
-
- ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
- Set the path to the bfs executable to test (default: ${MAG}./bfs${RST})
-
- ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
- Choose which test cases to run (default: ${BLU}--all${RST})
-
- ${BLU}--sudo${RST}
- Run tests that require root (not included in ${BLU}--all${RST})
-
- ${BLU}--noclean${RST}
- Keep the test directories around after the run
-
- ${BLU}--update${RST}
- Update the expected outputs for the test cases
-
- ${BLU}--verbose${RST}
- Log the commands that get executed
-
- ${BLU}--help${RST}
- This message
-
- ${BLD}test_*${RST}
- Select individual test cases to run
-EOF
-}
-
-function _realpath() {
- (
- cd "${1%/*}"
- echo "$PWD/${1##*/}"
- )
-}
-
-BFS="$(_realpath ./bfs)"
-TESTS="$(_realpath ./tests)"
-UNAME="$(uname)"
-
-DEFAULT=yes
-POSIX=
-BSD=
-GNU=
-ALL=
-SUDO=
-CLEAN=yes
-UPDATE=
-VERBOSE=
-EXPLICIT=
-
-enabled_tests=()
-
-for arg; do
- case "$arg" in
- --bfs=*)
- BFS="${arg#*=}"
- ;;
- --posix)
- DEFAULT=
- POSIX=yes
- ;;
- --bsd)
- DEFAULT=
- POSIX=yes
- BSD=yes
- ;;
- --gnu)
- DEFAULT=
- POSIX=yes
- GNU=yes
- ;;
- --all)
- DEFAULT=
- POSIX=yes
- BSD=yes
- GNU=yes
- ALL=yes
- ;;
- --sudo)
- DEFAULT=
- SUDO=yes
- ;;
- --noclean)
- CLEAN=
- ;;
- --update)
- UPDATE=yes
- ;;
- --verbose)
- VERBOSE=yes
- ;;
- --help)
- usage
- exit 0
- ;;
- test_*)
- EXPLICIT=yes
- enabled_tests+=("$arg")
- ;;
- *)
- printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
- usage >&2
- exit 1
- ;;
- esac
-done
-
-posix_tests=(
- # General parsing
- test_basic
-
- test_parens
- test_bang
- test_implicit_and
- test_a
- test_o
-
- test_weird_names
-
- test_incomplete
- test_missing_paren
- test_extra_paren
-
- # Flags
-
- test_H
- test_H_slash
- test_H_broken
- test_H_notdir
- test_H_loops
-
- test_L
- test_L_broken
- test_L_notdir
- test_L_loops
-
- test_flag_weird_names
- test_flag_comma
-
- # Primaries
-
- test_depth
- test_depth_slash
- test_depth_error
- test_L_depth
-
- test_exec
- test_exec_plus
- test_exec_plus_status
- test_exec_plus_semicolon
-
- test_group_name
- test_group_id
- test_group_nogroup
-
- test_links
- test_links_plus
- test_links_minus
-
- test_name
- test_name_root
- test_name_root_depth
- test_name_trailing_slash
-
- test_newer
- test_newer_link
-
- test_nogroup
- test_nogroup_ulimit
-
- test_nouser
- test_nouser_ulimit
-
- test_ok_stdin
- test_ok_plus_semicolon
-
- test_path
-
- test_perm_000
- test_perm_000_minus
- test_perm_222
- test_perm_222_minus
- test_perm_644
- test_perm_644_minus
- test_perm_symbolic
- test_perm_symbolic_minus
- test_perm_leading_plus_symbolic_minus
- test_permcopy
- test_perm_setid
- test_perm_sticky
-
- test_prune
- test_prune_or_print
- test_not_prune
-
- test_size
- test_size_plus
- test_size_bytes
-
- test_type_d
- test_type_f
- test_type_l
- test_H_type_l
- test_L_type_l
-
- test_user_name
- test_user_id
- test_user_nouser
-
- # Closed file descriptors
- test_closed_stdin
- test_closed_stdout
- test_closed_stderr
-
- # PATH_MAX handling
- test_deep
-
- # Optimizer tests
- test_or_purity
- test_double_negation
- test_de_morgan_not
- test_de_morgan_and
- test_de_morgan_or
- test_data_flow_group
- test_data_flow_user
- test_data_flow_type
- test_data_flow_and_swap
- test_data_flow_or_swap
-)
-
-bsd_tests=(
- # Flags
-
- test_E
-
- test_P
- test_P_slash
-
- test_X
-
- test_d_path
-
- test_f
-
- test_s
-
- test_double_dash
- test_flag_double_dash
-
- # Primaries
-
- test_acl
- test_L_acl
-
- test_anewer
- test_asince
-
- test_delete
- test_delete_many
-
- test_depth_maxdepth_1
- test_depth_maxdepth_2
- test_depth_mindepth_1
- test_depth_mindepth_2
-
- test_depth_n
- test_depth_n_plus
- test_depth_n_minus
- test_depth_depth_n
- test_depth_depth_n_plus
- test_depth_depth_n_minus
- test_depth_overflow
- test_data_flow_depth
-
- test_exec_substring
-
- test_execdir_pwd
- test_execdir_slash
- test_execdir_slash_pwd
- test_execdir_slashes
- test_execdir_ulimit
-
- test_exit
-
- test_flags
-
- test_follow
-
- test_gid_name
-
- test_ilname
- test_L_ilname
-
- test_iname
-
- test_inum
-
- test_ipath
-
- test_iregex
-
- test_lname
- test_L_lname
-
- test_ls
- test_L_ls
-
- test_maxdepth
-
- test_mindepth
-
- test_mnewer
- test_H_mnewer
-
- test_msince
-
- test_mtime_units
-
- test_name_slash
- test_name_slashes
-
- test_H_newer
-
- test_newerma
- test_newermt
- test_newermt_epoch_minus_one
-
- test_ok_stdin
- test_ok_closed_stdin
-
- test_okdir_stdin
- test_okdir_closed_stdin
-
- test_perm_000_plus
- test_perm_222_plus
- test_perm_644_plus
-
- test_printx
-
- test_quit
- test_quit_child
- test_quit_depth
- test_quit_depth_child
- test_quit_after_print
- test_quit_before_print
- test_quit_implicit_print
-
- test_rm
-
- test_regex
- test_regex_parens
-
- test_samefile
- test_samefile_symlink
- test_H_samefile_symlink
- test_L_samefile_symlink
- test_samefile_broken
- test_H_samefile_broken
- test_L_samefile_broken
- test_samefile_notdir
- test_H_samefile_notdir
- test_L_samefile_notdir
-
- test_size_T
- test_size_big
-
- test_uid_name
-
- # Optimizer tests
- test_data_flow_sparse
-)
-
-gnu_tests=(
- # General parsing
-
- test_not
- test_and
- test_or
- test_comma
- test_precedence
-
- test_follow_comma
-
- # Flags
-
- test_P
- test_P_slash
-
- test_L_loops_continue
-
- test_double_dash
- test_flag_double_dash
-
- # Primaries
-
- test_anewer
-
- test_path_d
-
- test_daystart
- test_daystart_twice
-
- test_delete
- test_delete_many
- test_L_delete
-
- test_depth_mindepth_1
- test_depth_mindepth_2
- test_depth_maxdepth_1
- test_depth_maxdepth_2
-
- test_empty
- test_empty_special
-
- test_exec_nothing
- test_exec_substring
-
- test_execdir
- test_execdir_substring
- test_execdir_plus_semicolon
- test_execdir_pwd
- test_execdir_slash
- test_execdir_slash_pwd
- test_execdir_slashes
- test_execdir_ulimit
-
- test_executable
-
- test_false
-
- test_files0_from_file
- test_files0_from_stdin
- test_files0_from_none
- test_files0_from_empty
- test_files0_from_nowhere
- test_files0_from_nothing
- test_files0_from_ok
-
- test_fls
-
- test_follow
-
- test_fprint
- test_fprint_duplicate
- test_fprint_error
-
- test_fprint0
-
- test_fprintf
-
- test_fstype
-
- test_gid
- test_gid_plus
- test_gid_plus_plus
- test_gid_minus
- test_gid_minus_plus
-
- test_ignore_readdir_race
- test_ignore_readdir_race_root
- test_ignore_readdir_race_notdir
-
- test_ilname
- test_L_ilname
-
- test_iname
-
- test_inum
-
- test_ipath
-
- test_iregex
-
- test_lname
- test_L_lname
-
- test_ls
- test_L_ls
-
- test_maxdepth
-
- test_mindepth
-
- test_name_slash
- test_name_slashes
-
- test_H_newer
-
- test_newerma
- test_newermt
- test_newermt_epoch_minus_one
-
- test_ok_closed_stdin
- test_ok_nothing
-
- test_okdir_closed_stdin
- test_okdir_plus_semicolon
-
- test_perm_000_slash
- test_perm_222_slash
- test_perm_644_slash
- test_perm_symbolic_slash
- test_perm_leading_plus_symbolic_slash
-
- test_print_error
-
- test_print0
-
- test_printf
- test_printf_empty
- test_printf_slash
- test_printf_slashes
- test_printf_trailing_slash
- test_printf_trailing_slashes
- test_printf_flags
- test_printf_types
- test_printf_escapes
- test_printf_times
- test_printf_leak
- test_printf_nul
- test_printf_Y_error
- test_printf_H
- test_printf_u_g_ulimit
- test_printf_l_nonlink
-
- test_quit
- test_quit_child
- test_quit_depth
- test_quit_depth_child
- test_quit_after_print
- test_quit_before_print
-
- test_readable
-
- test_regex
- test_regex_parens
- test_regex_error
-
- test_regextype_posix_basic
- test_regextype_posix_extended
-
- test_samefile
- test_samefile_symlink
- test_H_samefile_symlink
- test_L_samefile_symlink
- test_samefile_broken
- test_H_samefile_broken
- test_L_samefile_broken
- test_samefile_notdir
- test_H_samefile_notdir
- test_L_samefile_notdir
-
- test_size_big
-
- test_true
-
- test_uid
- test_uid_plus
- test_uid_plus_plus
- test_uid_minus
- test_uid_minus_plus
-
- test_writable
-
- test_xtype_l
- test_xtype_f
- test_L_xtype_l
- test_L_xtype_f
-
- # Optimizer tests
- test_and_purity
- test_not_reachability
- test_comma_reachability
- test_and_false_or_true
- test_comma_redundant_true
- test_comma_redundant_false
-)
-
-bfs_tests=(
- # General parsing
- test_path_flag_expr
- test_path_expr_flag
- test_flag_expr_path
- test_expr_flag_path
- test_expr_path_flag
-
- test_typo
-
- # Flags
-
- test_D_multi
- test_D_all
-
- test_O0
- test_O1
- test_O2
- test_O3
- test_Ofast
-
- test_S_bfs
- test_S_dfs
- test_S_ids
-
- # Special forms
-
- test_exclude_name
- test_exclude_depth
- test_exclude_mindepth
- test_exclude_print
- test_exclude_exclude
-
- # Primaries
-
- test_color
- test_color_L
- test_color_rs_lc_rc_ec
- test_color_escapes
- test_color_nul
- test_color_ln_target
- test_color_L_ln_target
- test_color_mh
- test_color_mh0
- test_color_or
- test_color_mi
- test_color_or_mi
- test_color_or_mi0
- test_color_or0_mi
- test_color_or0_mi0
- test_color_su_sg0
- test_color_su0_sg
- test_color_su0_sg0
- test_color_st_tw_ow0
- test_color_st_tw0_ow
- test_color_st_tw0_ow0
- test_color_st0_tw_ow
- test_color_st0_tw_ow0
- test_color_st0_tw0_ow
- test_color_st0_tw0_ow0
- test_color_ext
- test_color_ext0
- test_color_ext_override
- test_color_ext_underride
- test_color_missing_colon
- test_color_no_stat
- test_color_L_no_stat
- test_color_star
- test_color_ls
-
- test_execdir_plus
-
- test_fprint_duplicate_stdout
- test_fprint_error_stdout
- test_fprint_error_stderr
-
- test_help
-
- test_hidden
- test_hidden_root
-
- test_newerma_nonexistent
- test_newermt_invalid
- test_newermq
- test_newerqm
-
- test_nohidden
- test_nohidden_depth
-
- test_perm_symbolic_trailing_comma
- test_perm_symbolic_double_comma
- test_perm_symbolic_missing_action
- test_perm_leading_plus_symbolic
-
- test_printf_w
- test_printf_incomplete_escape
- test_printf_invalid_escape
- test_printf_incomplete_format
- test_printf_invalid_format
- test_printf_duplicate_flag
- test_printf_must_be_numeric
-
- test_type_multi
-
- test_unique
- test_unique_depth
- test_L_unique
- test_L_unique_loops
- test_L_unique_depth
-
- test_version
-
- test_xtype_multi
-
- # Optimizer tests
- test_data_flow_hidden
- test_xtype_reorder
- test_xtype_depth
-
- # PATH_MAX handling
- test_deep_strict
-
- # Error handling
- test_stderr_fails_silently
- test_stderr_fails_loudly
-)
-
-sudo_tests=(
- test_capable
- test_L_capable
-
- test_mount
- test_L_mount
- test_xdev
- test_L_xdev
-
- test_inum_mount
- test_inum_bind_mount
- test_type_bind_mount
- test_xtype_bind_mount
-)
-
-case "$UNAME" in
- Darwin|FreeBSD)
- bsd_tests+=(
- test_xattr
- test_L_xattr
-
- test_xattrname
- test_L_xattrname
- )
- ;;
- *)
- sudo_tests+=(
- test_xattr
- test_L_xattr
-
- test_xattrname
- test_L_xattrname
- )
- ;;
-esac
-
-if [ "$DEFAULT" ]; then
- POSIX=yes
- BSD=yes
- GNU=yes
- ALL=yes
-fi
-
-if [ ! "$EXPLICIT" ]; then
- [ "$POSIX" ] && enabled_tests+=("${posix_tests[@]}")
- [ "$BSD" ] && enabled_tests+=("${bsd_tests[@]}")
- [ "$GNU" ] && enabled_tests+=("${gnu_tests[@]}")
- [ "$ALL" ] && enabled_tests+=("${bfs_tests[@]}")
- [ "$SUDO" ] && enabled_tests+=("${sudo_tests[@]}")
-fi
-
-eval enabled_tests=($(printf '%q\n' "${enabled_tests[@]}" | sort -u))
-
-# The temporary directory that will hold our test data
-TMP="$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)"
-chown "$(id -u):$(id -g)" "$TMP"
-
-# Clean up temporary directories on exit
-function cleanup() {
- # Don't force rm to deal with long paths
- for dir in "$TMP"/deep/*/*; do
- if [ -d "$dir" ]; then
- (cd "$dir" && rm -rf *)
- fi
- done
-
- # In case a test left anything weird in scratch/
- if [ -e "$TMP"/scratch ]; then
- chmod -R +rX "$TMP"/scratch
- fi
-
- rm -rf "$TMP"
-}
-
-if [ "$CLEAN" ]; then
- trap cleanup EXIT
-else
- echo "Test files saved to $TMP"
-fi
-
-# Install a file, creating any parent directories
-function installp() {
- local target="${@: -1}"
- mkdir -p "${target%/*}"
- install "$@"
-}
-
-# Like a mythical touch -p
-function touchp() {
- for arg; do
- installp -m644 /dev/null "$arg"
- done
-}
-
-# Creates a simple file+directory structure for tests
-function make_basic() {
- touchp "$1/a"
- touchp "$1/b"
- touchp "$1/c/d"
- touchp "$1/e/f"
- mkdir -p "$1/g/h"
- mkdir -p "$1/i"
- touchp "$1/j/foo"
- touchp "$1/k/foo/bar"
- touchp "$1/l/foo/bar/baz"
- echo baz >"$1/l/foo/bar/baz"
-}
-make_basic "$TMP/basic"
-
-# Creates a file+directory structure with various permissions for tests
-function make_perms() {
- installp -m000 /dev/null "$1/0"
- installp -m444 /dev/null "$1/r"
- installp -m222 /dev/null "$1/w"
- installp -m644 /dev/null "$1/rw"
- installp -m555 /dev/null "$1/rx"
- installp -m311 /dev/null "$1/wx"
- installp -m755 /dev/null "$1/rwx"
-}
-make_perms "$TMP/perms"
-
-# Creates a file+directory structure with various symbolic and hard links
-function make_links() {
- touchp "$1/file"
- ln -s file "$1/symlink"
- ln "$1/file" "$1/hardlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- mkdir -p "$1/deeply/nested/dir"
- touchp "$1/deeply/nested/file"
- ln -s file "$1/deeply/nested/link"
- ln -s nowhere "$1/deeply/nested/broken"
- ln -s deeply/nested "$1/skip"
-}
-make_links "$TMP/links"
-
-# Creates a file+directory structure with symbolic link loops
-function make_loops() {
- touchp "$1/file"
- ln -s file "$1/symlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- ln -s loop "$1/loop"
- mkdir -p "$1/deeply/nested/dir"
- ln -s ../../deeply "$1/deeply/nested/loop"
- ln -s deeply/nested/loop/nested "$1/skip"
-}
-make_loops "$TMP/loops"
-
-# Creates a file+directory structure with varying timestamps
-function make_times() {
- mkdir -p "$1"
- touch -t 199112140000 "$1/a"
- touch -t 199112140001 "$1/b"
- touch -t 199112140002 "$1/c"
- ln -s a "$1/l"
- touch -h -t 199112140003 "$1/l"
- touch -t 199112140004 "$1"
-}
-make_times "$TMP/times"
-
-# Creates a file+directory structure with various weird file/directory names
-function make_weirdnames() {
- touchp "$1/-/a"
- touchp "$1/(/b"
- touchp "$1/(-/c"
- touchp "$1/!/d"
- touchp "$1/!-/e"
- touchp "$1/,/f"
- touchp "$1/)/g"
- touchp "$1/.../h"
- touchp "$1/\\/i"
- touchp "$1/ /j"
-}
-make_weirdnames "$TMP/weirdnames"
-
-# Creates a very deep directory structure for testing PATH_MAX handling
-function make_deep() {
- mkdir -p "$1"
-
- # $name will be 255 characters, aka _XOPEN_NAME_MAX
- local name="0123456789ABCDEF"
- name="${name}${name}${name}${name}"
- name="${name}${name}${name}${name}"
- name="${name:0:255}"
-
- for i in {0..9} A B C D E F; do
- (
- mkdir "$1/$i"
- cd "$1/$i"
-
- # 16 * 256 == 4096 == PATH_MAX
- for j in {1..16}; do
- mkdir "$name"
- cd "$name" 2>/dev/null
- done
-
- touch "$name"
- )
- done
-}
-make_deep "$TMP/deep"
-
-# Creates a directory structure with many different types, and therefore colors
-function make_rainbow() {
- touchp "$1/file.txt"
- touchp "$1/file.dat"
- touchp "$1/star".{gz,tar,tar.gz}
- ln -s file.txt "$1/link.txt"
- touchp "$1/mh1"
- ln "$1/mh1" "$1/mh2"
- mkfifo "$1/pipe"
- # TODO: block
- ln -s /dev/null "$1/chardev_link"
- ln -s nowhere "$1/broken"
- "$TESTS/mksock" "$1/socket"
- touchp "$1"/s{u,g,ug}id
- chmod u+s "$1"/su{,g}id
- chmod g+s "$1"/s{u,}gid
- mkdir "$1/ow" "$1"/sticky{,_ow}
- chmod o+w "$1"/*ow
- chmod +t "$1"/sticky*
- touchp "$1"/exec.sh
- chmod +x "$1"/exec.sh
-}
-make_rainbow "$TMP/rainbow"
-
-# Creates a scratch directory that tests can modify
-function make_scratch() {
- mkdir -p "$1"
-}
-make_scratch "$TMP/scratch"
-
-function bfs_sort() {
- awk -F/ '{ print NF - 1 " " $0 }' | sort -n | cut -d' ' -f2-
-}
-
-# Close stdin so bfs doesn't think we're interactive
-exec </dev/null
-
-if [ "$VERBOSE" ]; then
- # dup stdout for verbose logging even when redirected
- exec 3>&1
-fi
-
-function bfs_verbose() {
- if [ "$VERBOSE" ]; then
- if [ -t 3 ]; then
- printf "${GRN}%q${RST} " $BFS >&3
-
- local expr_started=
- for arg; do
- if [[ $arg == -[A-Z]* ]]; then
- printf "${CYN}%q${RST} " "$arg" >&3
- elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then
- expr_started=yes
- printf "${RED}%q${RST} " "$arg" >&3
- elif [[ $expr_started && $arg == [\),] ]]; then
- printf "${RED}%q${RST} " "$arg" >&3
- elif [[ $arg == -?* ]]; then
- expr_started=yes
- printf "${BLU}%q${RST} " "$arg" >&3
- elif [ "$expr_started" ]; then
- printf "${BLD}%q${RST} " "$arg" >&3
- else
- printf "${MAG}%q${RST} " "$arg" >&3
- fi
- done
- else
- printf '%q ' $BFS "$@" >&3
- fi
- printf '\n' >&3
- fi
-}
-
-function invoke_bfs() {
- bfs_verbose "$@"
- $BFS "$@"
-}
-
-# Silence stderr unless --verbose is set
-function quiet() {
- if [ "$VERBOSE" ]; then
- "$@"
- else
- "$@" 2>/dev/null
- fi
-}
-
-# Return value when bfs fails
-EX_BFS=10
-# Return value when a difference is detected
-EX_DIFF=20
-
-function bfs_diff() (
- bfs_verbose "$@"
-
- # Close the dup()'d stdout to make sure we have enough fd's for the process
- # substitution, even with low ulimit -n
- exec 3>&-
-
- local CALLER
- for CALLER in "${FUNCNAME[@]}"; do
- if [[ $CALLER == test_* ]]; then
- break
- fi
- done
-
- local EXPECTED="$TESTS/$CALLER.out"
- if [ "$UPDATE" ]; then
- local ACTUAL="$EXPECTED"
- else
- local ACTUAL="$TMP/$CALLER.out"
- fi
-
- $BFS "$@" | bfs_sort >"$ACTUAL"
- local STATUS="${PIPESTATUS[0]}"
-
- if [ ! "$UPDATE" ]; then
- diff -u "$EXPECTED" "$ACTUAL" || return $EX_DIFF
- fi
-
- if [ "$STATUS" -eq 0 ]; then
- return 0
- else
- return $EX_BFS
- fi
-)
-
-function closefrom() {
- if [ -d /proc/self/fd ]; then
- local fds=/proc/self/fd
- else
- local fds=/dev/fd
- fi
-
- for fd in "$fds"/*; do
- if [ ! -e "$fd" ]; then
- continue
- fi
-
- local fd="${fd##*/}"
- if [ "$fd" -ge "$1" ]; then
- eval "exec ${fd}<&-"
- fi
- done
-}
-
-function inum() {
- ls -id "$@" | awk '{ print $1 }'
-}
-
-
-cd "$TMP"
-set +e
-
-# Test cases
-
-function test_basic() {
- bfs_diff basic
-}
-
-function test_type_d() {
- bfs_diff basic -type d
-}
-
-function test_type_f() {
- bfs_diff basic -type f
-}
-
-function test_type_l() {
- bfs_diff links/skip -type l
-}
-
-function test_H_type_l() {
- bfs_diff -H links/skip -type l
-}
-
-function test_L_type_l() {
- bfs_diff -L links/skip -type l
-}
-
-function test_type_multi() {
- bfs_diff links -type f,d,c
-}
-
-function test_mindepth() {
- bfs_diff basic -mindepth 1
-}
-
-function test_maxdepth() {
- bfs_diff basic -maxdepth 1
-}
-
-function test_depth() {
- bfs_diff basic -depth
-}
-
-function test_depth_slash() {
- bfs_diff basic/ -depth
-}
-
-function test_depth_mindepth_1() {
- bfs_diff basic -mindepth 1 -depth
-}
-
-function test_depth_mindepth_2() {
- bfs_diff basic -mindepth 2 -depth
-}
-
-function test_depth_maxdepth_1() {
- bfs_diff basic -maxdepth 1 -depth
-}
-
-function test_depth_maxdepth_2() {
- bfs_diff basic -maxdepth 2 -depth
-}
-
-function test_depth_error() {
- rm -rf scratch/*
- touchp scratch/foo/bar
- chmod -r scratch/foo
-
- quiet bfs_diff scratch -depth
- local ret=$?
-
- chmod +r scratch/foo
- rm -rf scratch/*
-
- [ $ret -eq $EX_BFS ]
-}
-
-function test_name() {
- bfs_diff basic -name '*f*'
-}
-
-function test_name_root() {
- bfs_diff basic/a -name a
-}
-
-function test_name_root_depth() {
- bfs_diff basic/g -depth -name g
-}
-
-function test_name_trailing_slash() {
- bfs_diff basic/g/ -name g
-}
-
-function test_name_slash() {
- bfs_diff / -maxdepth 0 -name /
-}
-
-function test_name_slashes() {
- bfs_diff /// -maxdepth 0 -name /
-}
-
-function test_path() {
- bfs_diff basic -path 'basic/*f*'
-}
-
-function test_true() {
- bfs_diff basic -true
-}
-
-function test_false() {
- bfs_diff basic -false
-}
-
-function test_executable() {
- bfs_diff perms -executable
-}
-
-function test_readable() {
- bfs_diff perms -readable
-}
-
-function test_writable() {
- bfs_diff perms -writable
-}
-
-function test_empty() {
- bfs_diff basic -empty
-}
-
-function test_empty_special() {
- bfs_diff rainbow -empty
-}
-
-function test_gid() {
- bfs_diff basic -gid "$(id -g)"
-}
-
-function test_gid_plus() {
- if [ "$(id -g)" -ne 0 ]; then
- bfs_diff basic -gid +0
- fi
-}
-
-function test_gid_plus_plus() {
- if [ "$(id -g)" -ne 0 ]; then
- bfs_diff basic -gid ++0
- fi
-}
-
-function test_gid_minus() {
- bfs_diff basic -gid "-$(($(id -g) + 1))"
-}
-
-function test_gid_minus_plus() {
- bfs_diff basic -gid "-+$(($(id -g) + 1))"
-}
-
-function test_uid() {
- bfs_diff basic -uid "$(id -u)"
-}
-
-function test_uid_plus() {
- if [ "$(id -u)" -ne 0 ]; then
- bfs_diff basic -uid +0
- fi
-}
-
-function test_uid_plus_plus() {
- if [ "$(id -u)" -ne 0 ]; then
- bfs_diff basic -uid ++0
- fi
-}
-
-function test_uid_minus() {
- bfs_diff basic -uid "-$(($(id -u) + 1))"
-}
-
-function test_uid_minus_plus() {
- bfs_diff basic -uid "-+$(($(id -u) + 1))"
-}
-
-function test_newer() {
- bfs_diff times -newer times/a
-}
-
-function test_newer_link() {
- bfs_diff times -newer times/l
-}
-
-function test_anewer() {
- bfs_diff times -anewer times/a
-}
-
-function test_asince() {
- bfs_diff times -asince 1991-12-14T00:01
-}
-
-function test_links() {
- bfs_diff links -type f -links 2
-}
-
-function test_links_plus() {
- bfs_diff links -type f -links +1
-}
-
-function test_links_minus() {
- bfs_diff links -type f -links -2
-}
-
-function test_P() {
- bfs_diff -P links/deeply/nested/dir
-}
-
-function test_P_slash() {
- bfs_diff -P links/deeply/nested/dir/
-}
-
-function test_H() {
- bfs_diff -H links/deeply/nested/dir
-}
-
-function test_H_slash() {
- bfs_diff -H links/deeply/nested/dir/
-}
-
-function test_H_broken() {
- bfs_diff -H links/broken
-}
-
-function test_H_notdir() {
- bfs_diff -H links/notdir
-}
-
-function test_H_newer() {
- bfs_diff -H times -newer times/l
-}
-
-function test_H_loops() {
- bfs_diff -H loops/deeply/nested/loop
-}
-
-function test_L() {
- bfs_diff -L links
-}
-
-function test_L_broken() {
- bfs_diff -H links/broken
-}
-
-function test_L_notdir() {
- bfs_diff -H links/notdir
-}
-
-function test_L_loops() {
- # POSIX says it's okay to either stop or keep going on seeing a filesystem
- # loop, as long as a diagnostic is printed
- local errors="$(invoke_bfs -L loops 2>&1 >/dev/null)"
- [ -n "$errors" ]
-}
-
-function test_L_loops_continue() {
- quiet bfs_diff -L loops
- [ $? -eq $EX_BFS ]
-}
-
-function test_X() {
- quiet bfs_diff -X weirdnames
- [ $? -eq $EX_BFS ]
-}
-
-function test_follow() {
- bfs_diff links -follow
-}
-
-function test_L_depth() {
- bfs_diff -L links -depth
-}
-
-function test_samefile() {
- bfs_diff links -samefile links/file
-}
-
-function test_samefile_symlink() {
- bfs_diff links -samefile links/symlink
-}
-
-function test_H_samefile_symlink() {
- bfs_diff -H links -samefile links/symlink
-}
-
-function test_L_samefile_symlink() {
- bfs_diff -L links -samefile links/symlink
-}
-
-function test_samefile_broken() {
- bfs_diff links -samefile links/broken
-}
-
-function test_H_samefile_broken() {
- bfs_diff -H links -samefile links/broken
-}
-
-function test_L_samefile_broken() {
- bfs_diff -L links -samefile links/broken
-}
-
-function test_samefile_notdir() {
- bfs_diff links -samefile links/notdir
-}
-
-function test_H_samefile_notdir() {
- bfs_diff -H links -samefile links/notdir
-}
-
-function test_L_samefile_notdir() {
- bfs_diff -L links -samefile links/notdir
-}
-
-function test_xtype_l() {
- bfs_diff links -xtype l
-}
-
-function test_xtype_f() {
- bfs_diff links -xtype f
-}
-
-function test_L_xtype_l() {
- bfs_diff -L links -xtype l
-}
-
-function test_L_xtype_f() {
- bfs_diff -L links -xtype f
-}
-
-function test_xtype_multi() {
- bfs_diff links -xtype f,d,c
-}
-
-function test_xtype_reorder() {
- # Make sure -xtype is not reordered in front of anything -- if -xtype runs
- # before -links 100, it will report an ELOOP error
- bfs_diff loops -links 100 -xtype l
- invoke_bfs loops -links 100 -xtype l
-}
-
-function test_xtype_depth() {
- # Make sure -xtype is considered side-effecting for facts_when_impure
- ! quiet invoke_bfs loops -xtype l -depth 100
-}
-
-function test_iname() {
- bfs_diff basic -iname '*F*'
-}
-
-function test_ipath() {
- bfs_diff basic -ipath 'basic/*F*'
-}
-
-function test_lname() {
- bfs_diff links -lname '[aq]'
-}
-
-function test_ilname() {
- bfs_diff links -ilname '[AQ]'
-}
-
-function test_L_lname() {
- bfs_diff -L links -lname '[aq]'
-}
-
-function test_L_ilname() {
- bfs_diff -L links -ilname '[AQ]'
-}
-
-function test_user_name() {
- bfs_diff basic -user "$(id -un)"
-}
-
-function test_user_id() {
- bfs_diff basic -user "$(id -u)"
-}
-
-function test_user_nouser() {
- # Regression test: this was wrongly optimized to -false
- bfs_diff basic -user "$(id -u)" \! -nouser
-}
-
-function test_group_name() {
- bfs_diff basic -group "$(id -gn)"
-}
-
-function test_group_id() {
- bfs_diff basic -group "$(id -g)"
-}
-
-function test_group_nogroup() {
- # Regression test: this was wrongly optimized to -false
- bfs_diff basic -group "$(id -g)" \! -nogroup
-}
-
-function test_daystart() {
- bfs_diff basic -daystart -mtime 0
-}
-
-function test_daystart_twice() {
- bfs_diff basic -daystart -daystart -mtime 0
-}
-
-function test_newerma() {
- bfs_diff times -newerma times/a
-}
-
-function test_newermt() {
- bfs_diff times -newermt 1991-12-14T00:01
-}
-
-function test_newermt_epoch_minus_one() {
- bfs_diff times -newermt 1969-12-31T23:59:59Z
-}
-
-function test_newermt_invalid() {
- ! quiet invoke_bfs times -newermt not_a_date_time
-}
-
-function test_newerma_nonexistent() {
- ! quiet invoke_bfs times -newerma basic/nonexistent
-}
-
-function test_newermq() {
- ! quiet invoke_bfs times -newermq times/a
-}
-
-function test_newerqm() {
- ! quiet invoke_bfs times -newerqm times/a
-}
-
-function test_size() {
- bfs_diff basic -type f -size 0
-}
-
-function test_size_plus() {
- bfs_diff basic -type f -size +0
-}
-
-function test_size_bytes() {
- bfs_diff basic -type f -size +0c
-}
-
-function test_size_big() {
- bfs_diff basic -size 9223372036854775807
-}
-
-function test_exec() {
- bfs_diff basic -exec echo '{}' \;
-}
-
-function test_exec_nothing() {
- # Regression test: don't segfault on missing command
- ! quiet invoke_bfs basic -exec \;
-}
-
-function test_exec_plus() {
- bfs_diff basic -exec "$TESTS/sort-args.sh" '{}' +
-}
-
-function test_exec_plus_status() {
- # -exec ... {} + should always return true, but if the command fails, bfs
- # should exit with a non-zero status
- bfs_diff basic -exec false '{}' + -print
- ! invoke_bfs basic -exec false '{}' +
-}
-
-function test_exec_plus_semicolon() {
- # POSIX says:
- # Only a <plus-sign> that immediately follows an argument containing only the two characters "{}"
- # shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be
- # treated as special.
- bfs_diff basic -exec echo foo '{}' bar + baz \;
-}
-
-function test_exec_substring() {
- bfs_diff basic -exec echo '-{}-' ';'
-}
-
-function test_execdir() {
- bfs_diff basic -execdir echo '{}' ';'
-}
-
-function test_execdir_plus() {
- if [[ "$BFS" != *"-S dfs"* ]]; then
- bfs_diff basic -execdir "$TESTS/sort-args.sh" '{}' +
- fi
-}
-
-function test_execdir_substring() {
- bfs_diff basic -execdir echo '-{}-' ';'
-}
-
-function test_execdir_plus_semicolon() {
- bfs_diff basic -execdir echo foo '{}' bar + baz \;
-}
-
-function test_execdir_pwd() {
- local TMP_REAL="$(cd "$TMP" && pwd)"
- local OFFSET="$((${#TMP_REAL} + 2))"
- bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" ';'
-}
-
-function test_execdir_slash() {
- # Don't prepend ./ for absolute paths in -execdir
- bfs_diff / -maxdepth 0 -execdir echo '{}' ';'
-}
-
-function test_execdir_slash_pwd() {
- bfs_diff / -maxdepth 0 -execdir pwd ';'
-}
-
-function test_execdir_slashes() {
- bfs_diff /// -maxdepth 0 -execdir echo '{}' ';'
-}
-
-function test_execdir_ulimit() {
- rm -rf scratch/*
- mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
- mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
-
- closefrom 4
- ulimit -n 13
- bfs_diff scratch -execdir echo '{}' ';'
-}
-
-function test_weird_names() {
- cd weirdnames
- bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
-}
-
-function test_flag_weird_names() {
- cd weirdnames
- bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
-}
-
-function test_flag_comma() {
- # , is a filename until a non-flag is seen
- cd weirdnames
- bfs_diff -L ',' -print
-}
-
-function test_follow_comma() {
- # , is an operator after a non-flag is seen
- cd weirdnames
- bfs_diff -follow ',' -print
-}
-
-function test_fprint() {
- invoke_bfs basic -fprint scratch/test_fprint.out
- sort -o scratch/test_fprint.out scratch/test_fprint.out
-
- if [ "$UPDATE" ]; then
- cp {scratch,"$TESTS"}/test_fprint.out
- else
- diff -u {"$TESTS",scratch}/test_fprint.out
- fi
-}
-
-function test_fprint_duplicate() {
- touchp scratch/test_fprint_duplicate.out
- ln scratch/test_fprint_duplicate.out scratch/test_fprint_duplicate.hard
- ln -s test_fprint_duplicate.out scratch/test_fprint_duplicate.soft
-
- invoke_bfs basic -fprint scratch/test_fprint_duplicate.out -fprint scratch/test_fprint_duplicate.hard -fprint scratch/test_fprint_duplicate.soft
- sort -o scratch/test_fprint_duplicate.out scratch/test_fprint_duplicate.out
-
- if [ "$UPDATE" ]; then
- cp {scratch,"$TESTS"}/test_fprint_duplicate.out
- else
- diff -u {"$TESTS",scratch}/test_fprint_duplicate.out
- fi
-}
-
-function test_fprint_duplicate_stdout() {
- touchp scratch/test_fprint_duplicate_stdout.out
-
- invoke_bfs basic -fprint scratch/test_fprint_duplicate_stdout.out -print >scratch/test_fprint_duplicate_stdout.out
- sort -o scratch/test_fprint_duplicate_stdout.out{,}
-
- if [ "$UPDATE" ]; then
- cp {scratch,"$TESTS"}/test_fprint_duplicate_stdout.out
- else
- diff -u {"$TESTS",scratch}/test_fprint_duplicate_stdout.out
- fi
-}
-
-function test_double_dash() {
- cd basic
- bfs_diff -- . -type f
-}
-
-function test_flag_double_dash() {
- cd basic
- bfs_diff -L -- . -type f
-}
-
-function test_ignore_readdir_race() {
- rm -rf scratch/*
- touch scratch/{foo,bar}
-
- # -links 1 forces a stat() call, which will fail for the second file
- invoke_bfs scratch -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" '{}' ';'
-}
-
-function test_ignore_readdir_race_root() {
- # Make sure -ignore_readdir_race doesn't suppress ENOENT at the root
- ! quiet invoke_bfs basic/nonexistent -ignore_readdir_race
-}
-
-function test_ignore_readdir_race_notdir() {
- # Check -ignore_readdir_race handling when a directory is replaced with a file
- rm -rf scratch/*
- touchp scratch/foo/bar
-
- invoke_bfs scratch -mindepth 1 -ignore_readdir_race -execdir rm -r '{}' \; -execdir touch '{}' \;
-}
-
-function test_perm_000() {
- bfs_diff perms -perm 000
-}
-
-function test_perm_000_minus() {
- bfs_diff perms -perm -000
-}
-
-function test_perm_000_slash() {
- bfs_diff perms -perm /000
-}
-
-function test_perm_000_plus() {
- bfs_diff perms -perm +000
-}
-
-function test_perm_222() {
- bfs_diff perms -perm 222
-}
-
-function test_perm_222_minus() {
- bfs_diff perms -perm -222
-}
-
-function test_perm_222_slash() {
- bfs_diff perms -perm /222
-}
-
-function test_perm_222_plus() {
- bfs_diff perms -perm +222
-}
-
-function test_perm_644() {
- bfs_diff perms -perm 644
-}
-
-function test_perm_644_minus() {
- bfs_diff perms -perm -644
-}
-
-function test_perm_644_slash() {
- bfs_diff perms -perm /644
-}
-
-function test_perm_644_plus() {
- bfs_diff perms -perm +644
-}
-
-function test_perm_symbolic() {
- bfs_diff perms -perm a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_minus() {
- bfs_diff perms -perm -a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_slash() {
- bfs_diff perms -perm /a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_trailing_comma() {
- ! quiet invoke_bfs perms -perm a+r,
-}
-
-function test_perm_symbolic_double_comma() {
- ! quiet invoke_bfs perms -perm a+r,,u+w
-}
-
-function test_perm_symbolic_missing_action() {
- ! quiet invoke_bfs perms -perm a
-}
-
-function test_perm_leading_plus_symbolic() {
- bfs_diff perms -perm +rwx
-}
-
-function test_perm_leading_plus_symbolic_minus() {
- bfs_diff perms -perm -+rwx
-}
-
-function test_perm_leading_plus_symbolic_slash() {
- bfs_diff perms -perm /+rwx
-}
-
-function test_permcopy() {
- bfs_diff perms -perm u+rw,g+u-w,o=g
-}
-
-function test_perm_setid() {
- bfs_diff rainbow -perm -u+s -o -perm -g+s
-}
-
-function test_perm_sticky() {
- bfs_diff rainbow -perm -a+t
-}
-
-function test_prune() {
- bfs_diff basic -name foo -prune
-}
-
-function test_prune_or_print() {
- bfs_diff basic -name foo -prune -o -print
-}
-
-function test_not_prune() {
- bfs_diff basic \! \( -name foo -prune \)
-}
-
-function test_ok_nothing() {
- # Regression test: don't segfault on missing command
- ! quiet invoke_bfs basic -ok \;
-}
-
-function test_ok_stdin() {
- # -ok should *not* close stdin
- # See https://savannah.gnu.org/bugs/?24561
- yes | quiet bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash '{}' \;
-}
-
-function test_okdir_stdin() {
- # -okdir should *not* close stdin
- yes | quiet bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash '{}' \;
-}
-
-function test_ok_plus_semicolon() {
- yes | quiet bfs_diff basic -ok echo '{}' + \;
-}
-
-function test_okdir_plus_semicolon() {
- yes | quiet bfs_diff basic -okdir echo '{}' + \;
-}
-
-function test_delete() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
-
- # Don't try to delete '.'
- (cd scratch && invoke_bfs . -delete)
-
- bfs_diff scratch
-}
-
-function test_delete_many() {
- # Test for https://github.com/tavianator/bfs/issues/67
-
- rm -rf scratch/*
- mkdir scratch/foo
- touch scratch/foo/{1..256}
-
- invoke_bfs scratch/foo -delete
- bfs_diff scratch
-}
-
-function test_L_delete() {
- rm -rf scratch/*
- mkdir scratch/foo
- mkdir scratch/bar
- ln -s ../foo scratch/bar/baz
-
- # Don't try to rmdir() a symlink
- invoke_bfs -L scratch/bar -delete || return 1
-
- bfs_diff scratch
-}
-
-function test_rm() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
-
- (cd scratch && invoke_bfs . -rm)
-
- bfs_diff scratch
-}
-
-function test_regex() {
- bfs_diff basic -regex 'basic/./.'
-}
-
-function test_iregex() {
- bfs_diff basic -iregex 'basic/[A-Z]/[a-z]'
-}
-
-function test_regex_parens() {
- cd weirdnames
- bfs_diff . -regex '\./\((\)'
-}
-
-function test_regex_error() {
- ! quiet invoke_bfs basic -regex '['
-}
-
-function test_E() {
- cd weirdnames
- bfs_diff -E . -regex '\./(\()'
-}
-
-function test_regextype_posix_basic() {
- cd weirdnames
- bfs_diff -regextype posix-basic -regex '\./\((\)'
-}
-
-function test_regextype_posix_extended() {
- cd weirdnames
- bfs_diff -regextype posix-extended -regex '\./(\()'
-}
-
-function test_d_path() {
- bfs_diff -d basic
-}
-
-function test_path_d() {
- bfs_diff basic -d
-}
-
-function test_f() {
- cd weirdnames
- bfs_diff -f '-' -f '('
-}
-
-function test_s() {
- invoke_bfs -s weirdnames -maxdepth 1 >"$TMP/test_s.out"
-
- if [ "$UPDATE" ]; then
- cp {"$TMP","$TESTS"}/test_s.out
- else
- diff -u {"$TESTS","$TMP"}/test_s.out
- fi
-}
-
-function test_hidden() {
- bfs_diff weirdnames -hidden
-}
-
-function test_hidden_root() {
- cd weirdnames
- bfs_diff . ./. ... ./... .../.. -hidden
-}
-
-function test_nohidden() {
- bfs_diff weirdnames -nohidden
-}
-
-function test_nohidden_depth() {
- bfs_diff weirdnames -depth -nohidden
-}
-
-function test_depth_n() {
- bfs_diff basic -depth 2
-}
-
-function test_depth_n_plus() {
- bfs_diff basic -depth +2
-}
-
-function test_depth_n_minus() {
- bfs_diff basic -depth -2
-}
-
-function test_depth_depth_n() {
- bfs_diff basic -depth -depth 2
-}
-
-function test_depth_depth_n_plus() {
- bfs_diff basic -depth -depth +2
-}
-
-function test_depth_depth_n_minus() {
- bfs_diff basic -depth -depth -2
-}
-
-function test_depth_overflow() {
- bfs_diff basic -depth -4294967296
-}
-
-function test_gid_name() {
- bfs_diff basic -gid "$(id -gn)"
-}
-
-function test_uid_name() {
- bfs_diff basic -uid "$(id -un)"
-}
-
-function test_mnewer() {
- bfs_diff times -mnewer times/a
-}
-
-function test_H_mnewer() {
- bfs_diff -H times -mnewer times/l
-}
-
-function test_msince() {
- bfs_diff times -msince 1991-12-14T00:01
-}
-
-function test_mtime_units() {
- bfs_diff times -mtime +500w400d300h200m100s
-}
-
-function test_size_T() {
- bfs_diff basic -type f -size 1T
-}
-
-function test_quit() {
- bfs_diff basic/g -print -name g -quit
-}
-
-function test_quit_child() {
- bfs_diff basic/g -print -name h -quit
-}
-
-function test_quit_depth() {
- bfs_diff basic/g -depth -print -name g -quit
-}
-
-function test_quit_depth_child() {
- bfs_diff basic/g -depth -print -name h -quit
-}
-
-function test_quit_after_print() {
- bfs_diff basic basic -print -quit
-}
-
-function test_quit_before_print() {
- bfs_diff basic basic -quit -print
-}
-
-function test_quit_implicit_print() {
- bfs_diff basic -name basic -o -quit
-}
-
-function test_inum() {
- bfs_diff basic -inum "$(inum basic/k/foo/bar)"
-}
-
-function test_nogroup() {
- bfs_diff basic -nogroup
-}
-
-function test_nogroup_ulimit() {
- closefrom 4
- ulimit -n 16
- bfs_diff deep -nogroup
-}
-
-function test_nouser() {
- bfs_diff basic -nouser
-}
-
-function test_nouser_ulimit() {
- closefrom 4
- ulimit -n 16
- bfs_diff deep -nouser
-}
-
-function test_ls() {
- invoke_bfs rainbow -ls >scratch/test_ls.out
-}
-
-function test_L_ls() {
- invoke_bfs -L rainbow -ls >scratch/test_L_ls.out
-}
-
-function test_fls() {
- invoke_bfs rainbow -fls scratch/test_fls.out
-}
-
-function test_printf() {
- bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
-}
-
-function test_printf_empty() {
- bfs_diff basic -printf ''
-}
-
-function test_printf_slash() {
- bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n'
-}
-
-function test_printf_slashes() {
- bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n'
-}
-
-function test_printf_trailing_slash() {
- bfs_diff basic/ -printf '(%h)/(%f)\n'
-}
-
-function test_printf_trailing_slashes() {
- bfs_diff basic/// -printf '(%h)/(%f)\n'
-}
-
-function test_printf_flags() {
- bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n'
-}
-
-function test_printf_types() {
- bfs_diff loops -printf '(%p) (%l) %y %Y\n'
-}
-
-function test_printf_escapes() {
- bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo'
-}
-
-function test_printf_times() {
- bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %T@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n'
-}
-
-function test_printf_leak() {
- # Memory leak regression test
- bfs_diff basic -maxdepth 0 -printf '%p'
-}
-
-function test_printf_nul() {
- # NUL byte regression test
- local EXPECTED="$TESTS/${FUNCNAME[0]}.out"
- if [ "$UPDATE" ]; then
- local ACTUAL="$EXPECTED"
- else
- local ACTUAL="$TMP/${FUNCNAME[0]}.out"
- fi
-
- invoke_bfs basic -maxdepth 0 -printf '%h\0%f\n' >"$ACTUAL"
-
- if [ ! "$UPDATE" ]; then
- diff -u "$EXPECTED" "$ACTUAL"
- fi
-}
-
-function test_printf_w() {
- # Birth times may not be supported, so just check that %w/%W/%B can be parsed
- bfs_diff times -false -printf '%w %WY %BY\n'
-}
-
-function test_printf_Y_error() {
- rm -rf scratch/*
- mkdir scratch/foo
- chmod -x scratch/foo
- ln -s foo/bar scratch/bar
-
- quiet bfs_diff scratch -printf '(%p) (%l) %y %Y\n'
- local ret=$?
-
- chmod +x scratch/foo
- rm -rf scratch/*
-
- [ $ret -eq $EX_BFS ]
-}
-
-function test_printf_H() {
- bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n'
-}
-
-function test_printf_u_g_ulimit() {
- closefrom 4
- ulimit -n 16
- [ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
-}
-
-function test_printf_l_nonlink() {
- bfs_diff links -printf '| %26p -> %-26l |\n'
-}
-
-function test_printf_incomplete_escape() {
- ! quiet invoke_bfs basic -printf '\'
-}
-
-function test_printf_invalid_escape() {
- ! quiet invoke_bfs basic -printf '\!'
-}
-
-function test_printf_incomplete_format() {
- ! quiet invoke_bfs basic -printf '%'
-}
-
-function test_printf_invalid_format() {
- ! quiet invoke_bfs basic -printf '%!'
-}
-
-function test_printf_duplicate_flag() {
- ! quiet invoke_bfs basic -printf '%--p'
-}
-
-function test_printf_must_be_numeric() {
- ! quiet invoke_bfs basic -printf '%+p'
-}
-
-function test_fprintf() {
- invoke_bfs basic -fprintf scratch/test_fprintf.out '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
- sort -o scratch/test_fprintf.out scratch/test_fprintf.out
-
- if [ "$UPDATE" ]; then
- cp scratch/test_fprintf.out "$TESTS/test_fprintf.out"
- else
- diff -u "$TESTS/test_fprintf.out" scratch/test_fprintf.out
- fi
-}
-
-function test_fstype() {
- fstype="$(invoke_bfs basic -maxdepth 0 -printf '%F\n')"
- bfs_diff basic -fstype "$fstype"
-}
-
-function test_path_flag_expr() {
- bfs_diff links/skip -H -type l
-}
-
-function test_path_expr_flag() {
- bfs_diff links/skip -type l -H
-}
-
-function test_flag_expr_path() {
- bfs_diff -H -type l links/skip
-}
-
-function test_expr_flag_path() {
- bfs_diff -type l -H links/skip
-}
-
-function test_expr_path_flag() {
- bfs_diff -type l links/skip -H
-}
-
-function test_parens() {
- bfs_diff basic \( -name '*f*' \)
-}
-
-function test_bang() {
- bfs_diff basic \! -name foo
-}
-
-function test_not() {
- bfs_diff basic -not -name foo
-}
-
-function test_implicit_and() {
- bfs_diff basic -name foo -type d
-}
-
-function test_a() {
- bfs_diff basic -name foo -a -type d
-}
-
-function test_and() {
- bfs_diff basic -name foo -and -type d
-}
-
-function test_o() {
- bfs_diff basic -name foo -o -type d
-}
-
-function test_or() {
- bfs_diff basic -name foo -or -type d
-}
-
-function test_comma() {
- bfs_diff basic -name '*f*' -print , -print
-}
-
-function test_precedence() {
- bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print
-}
-
-function test_incomplete() {
- ! quiet invoke_bfs basic \(
-}
-
-function test_missing_paren() {
- ! quiet invoke_bfs basic \( -print
-}
-
-function test_extra_paren() {
- ! quiet invoke_bfs basic -print \)
-}
-
-function test_color() {
- LS_COLORS= bfs_diff rainbow -color
-}
-
-function test_color_L() {
- LS_COLORS= bfs_diff -L rainbow -color
-}
-
-function test_color_rs_lc_rc_ec() {
- LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color
-}
-
-function test_color_escapes() {
- LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color
-}
-
-function test_color_nul() {
- local EXPECTED="$TESTS/${FUNCNAME[0]}.out"
- if [ "$UPDATE" ]; then
- local ACTUAL="$EXPECTED"
- else
- local ACTUAL="$TMP/${FUNCNAME[0]}.out"
- fi
-
- LS_COLORS="ec=\33[m\0:" invoke_bfs rainbow -color -maxdepth 0 >"$ACTUAL"
-
- if [ ! "$UPDATE" ]; then
- diff -u "$EXPECTED" "$ACTUAL"
- fi
-}
-
-function test_color_ln_target() {
- LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_L_ln_target() {
- LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color
-}
-
-function test_color_mh() {
- LS_COLORS="mh=01:" bfs_diff rainbow -color
-}
-
-function test_color_mh0() {
- LS_COLORS="mh=00:" bfs_diff rainbow -color
-}
-
-function test_color_or() {
- LS_COLORS="or=01:" bfs_diff rainbow -color
-}
-
-function test_color_mi() {
- LS_COLORS="mi=01:" bfs_diff rainbow -color
-}
-
-function test_color_or_mi() {
- LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_or_mi0() {
- LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color
-}
-
-function test_color_or0_mi() {
- LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_or0_mi0() {
- LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color
-}
-
-function test_color_su_sg0() {
- LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color
-}
-
-function test_color_su0_sg() {
- LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color
-}
-
-function test_color_su0_sg0() {
- LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw_ow0() {
- LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw0_ow() {
- LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw0_ow0() {
- LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw_ow() {
- LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw_ow0() {
- LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw0_ow() {
- LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw0_ow0() {
- LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_ext() {
- LS_COLORS="*.txt=01:" bfs_diff rainbow -color
-}
-
-function test_color_ext0() {
- LS_COLORS="*.txt=00:" bfs_diff rainbow -color
-}
-
-function test_color_ext_override() {
- LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_ext_underride() {
- LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color
-}
-
-function test_color_missing_colon() {
- LS_COLORS="*.txt=01" bfs_diff rainbow -color
-}
-
-function test_color_no_stat() {
- LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color
-}
-
-function test_color_L_no_stat() {
- LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color
-}
-
-function test_color_star() {
- # Regression test: don't segfault on LS_COLORS="*"
- LS_COLORS="*" bfs_diff rainbow -color
-}
-
-function test_color_ls() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
- ln -s foo/bar/baz scratch/link
- ln -s foo/bar/nowhere scratch/broken
- ln -s foo/bar/nowhere/nothing scratch/nested
- ln -s foo/bar/baz/qux scratch/notdir
- ln -s scratch/foo/bar scratch/relative
- mkdir scratch/__bfs__
- ln -s /__bfs__/nowhere scratch/absolute
-
- local EXPECTED="$TESTS/${FUNCNAME[0]}.out"
- if [ "$UPDATE" ]; then
- local ACTUAL="$EXPECTED"
- else
- local ACTUAL="$TMP/${FUNCNAME[0]}.out"
- fi
-
- LS_COLORS="or=01;31:" invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \
- | sed 's/.* -> //' \
- | sort -o "$ACTUAL"
-
- if [ ! "$UPDATE" ]; then
- diff -u "$EXPECTED" "$ACTUAL"
- fi
-}
-
-function test_deep() {
- closefrom 4
-
- ulimit -n 16
- bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash '{}' \;
-}
-
-function test_deep_strict() {
- closefrom 4
-
- # Not even enough fds to keep the root open
- ulimit -n 7
- bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash '{}' \;
-}
-
-function test_exit() {
- invoke_bfs basic -name foo -exit 42
- if [ $? -ne 42 ]; then
- return 1
- fi
-
- invoke_bfs basic -name qux -exit 42
- if [ $? -ne 0 ]; then
- return 1
- fi
-
- bfs_diff basic/g -print -name g -exit
-}
-
-function test_printx() {
- bfs_diff weirdnames -printx
-}
-
-function test_and_purity() {
- # Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure
- bfs_diff basic -name nonexistent \( -print , -false \)
-}
-
-function test_or_purity() {
- # Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure
- bfs_diff basic -name '*' -o -print
-}
-
-function test_double_negation() {
- bfs_diff basic \! \! -name 'foo'
-}
-
-function test_not_reachability() {
- bfs_diff basic -print \! -quit -print
-}
-
-function test_comma_reachability() {
- bfs_diff basic -print -quit , -print
-}
-
-function test_de_morgan_not() {
- bfs_diff basic \! \( -name 'foo' -o \! -type f \)
-}
-
-function test_de_morgan_and() {
- bfs_diff basic \( \! -name 'foo' -a \! -type f \)
-}
-
-function test_de_morgan_or() {
- bfs_diff basic \( \! -name 'foo' -o \! -type f \)
-}
-
-function test_and_false_or_true() {
- # Test (-a lhs(always_true) -false) <==> (! lhs),
- # (-o lhs(always_false) -true) <==> (! lhs)
- bfs_diff basic -prune -false -o -true
-}
-
-function test_comma_redundant_true() {
- # Test (, lhs(always_true) -true) <==> lhs
- bfs_diff basic -prune , -true
-}
-
-function test_comma_redundant_false() {
- # Test (, lhs(always_false) -false) <==> lhs
- bfs_diff basic -print -not -prune , -false
-}
-
-function test_data_flow_depth() {
- bfs_diff basic -depth +1 -depth -4
-}
-
-function test_data_flow_group() {
- bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \)
-}
-
-function test_data_flow_user() {
- bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \)
-}
-
-function test_data_flow_hidden() {
- bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \)
-}
-
-function test_data_flow_sparse() {
- bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \)
-}
-
-function test_data_flow_type() {
- bfs_diff basic \! \( -type f -o \! -type f \)
-}
-
-function test_data_flow_and_swap() {
- bfs_diff basic \! -type f -a -type d
-}
-
-function test_data_flow_or_swap() {
- bfs_diff basic \! \( -type f -o \! -type d \)
-}
-
-function test_print_error() {
- if [ -e /dev/full ]; then
- ! quiet invoke_bfs basic -maxdepth 0 >/dev/full
- fi
-}
-
-function test_fprint_error() {
- if [ -e /dev/full ]; then
- ! quiet invoke_bfs basic -maxdepth 0 -fprint /dev/full
- fi
-}
-
-function test_fprint_error_stdout() {
- if [ -e /dev/full ]; then
- ! quiet invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full
- fi
-}
-
-function test_fprint_error_stderr() {
- if [ -e /dev/full ]; then
- ! invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full
- fi
-}
-
-function test_print0() {
- invoke_bfs basic/a basic/b -print0 >scratch/test_print0.out
-
- if [ "$UPDATE" ]; then
- cp scratch/test_print0.out "$TESTS/test_print0.out"
- else
- cmp -s scratch/test_print0.out "$TESTS/test_print0.out"
- fi
-}
-
-function test_fprint0() {
- invoke_bfs basic/a basic/b -fprint0 scratch/test_fprint0.out
-
- if [ "$UPDATE" ]; then
- cp scratch/test_fprint0.out "$TESTS/test_fprint0.out"
- else
- cmp -s scratch/test_fprint0.out "$TESTS/test_fprint0.out"
- fi
-}
-
-function test_closed_stdin() {
- bfs_diff basic <&-
-}
-
-function test_ok_closed_stdin() {
- quiet bfs_diff basic -ok echo \; <&-
-}
-
-function test_okdir_closed_stdin() {
- quiet bfs_diff basic -okdir echo {} \; <&-
-}
-
-function test_closed_stdout() {
- ! quiet invoke_bfs basic >&-
-}
-
-function test_closed_stderr() {
- ! invoke_bfs basic >&- 2>&-
-}
-
-function test_unique() {
- bfs_diff links/{file,symlink,hardlink} -unique
-}
-
-function test_unique_depth() {
- bfs_diff basic -unique -depth
-}
-
-function test_L_unique() {
- bfs_diff -L links/{file,symlink,hardlink} -unique
-}
-
-function test_L_unique_loops() {
- bfs_diff -L loops/deeply/nested -unique
-}
-
-function test_L_unique_depth() {
- bfs_diff -L loops/deeply/nested -unique -depth
-}
-
-function test_mount() {
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- touch scratch/foo/bar scratch/mnt/baz
-
- bfs_diff scratch -mount
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_L_mount() {
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- ln -s ../mnt scratch/foo/bar
- touch scratch/mnt/baz
- ln -s ../mnt/baz scratch/foo/qux
-
- bfs_diff -L scratch -mount
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_xdev() {
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- touch scratch/foo/bar scratch/mnt/baz
-
- bfs_diff scratch -xdev
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_L_xdev() {
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- ln -s ../mnt scratch/foo/bar
- touch scratch/mnt/baz
- ln -s ../mnt/baz scratch/foo/qux
-
- bfs_diff -L scratch -xdev
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_inum_mount() {
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
-
- bfs_diff scratch -inum "$(inum scratch/mnt)"
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_inum_bind_mount() {
- rm -rf scratch/*
- touch scratch/{foo,bar}
- sudo mount --bind scratch/{foo,bar}
-
- bfs_diff scratch -inum "$(inum scratch/bar)"
- local ret=$?
-
- sudo umount scratch/bar
- return $ret
-}
-
-function test_type_bind_mount() {
- rm -rf scratch/*
- touch scratch/{file,null}
- sudo mount --bind /dev/null scratch/null
-
- bfs_diff scratch -type c
- local ret=$?
-
- sudo umount scratch/null
- return $ret
-}
-
-function test_xtype_bind_mount() {
- rm -rf scratch/*
- touch scratch/{file,null}
- sudo mount --bind /dev/null scratch/null
- ln -s /dev/null scratch/link
-
- bfs_diff -L scratch -type c
- local ret=$?
-
- sudo umount scratch/null
- return $ret
-}
-
-function set_acl() {
- case "$UNAME" in
- Darwin)
- chmod +a "$(id -un) allow read,write" "$1"
- ;;
- FreeBSD)
- if [ "$(getconf ACL_NFS4 "$1")" -gt 0 ]; then
- setfacl -m "u:$(id -un):rw::allow" "$1"
- else
- setfacl -m "u:$(id -un):rw" "$1"
- fi
- ;;
- *)
- setfacl -m "u:$(id -un):rw" "$1"
- ;;
- esac
-}
-
-function test_acl() {
- rm -rf scratch/*
-
- quiet invoke_bfs scratch -quit -acl || return 0
-
- touch scratch/{normal,acl}
- set_acl scratch/acl || return 0
- ln -s acl scratch/link
-
- bfs_diff scratch -acl
-}
-
-function test_L_acl() {
- rm -rf scratch/*
-
- quiet invoke_bfs scratch -quit -acl || return 0
-
- touch scratch/{normal,acl}
- set_acl scratch/acl || return 0
- ln -s acl scratch/link
-
- bfs_diff -L scratch -acl
-}
-
-function test_capable() {
- rm -rf scratch/*
-
- if ! quiet invoke_bfs scratch -quit -capable; then
- return 0
- fi
-
- touch scratch/{normal,capable}
- sudo setcap all+ep scratch/capable
- ln -s capable scratch/link
-
- bfs_diff scratch -capable
-}
-
-function test_L_capable() {
- rm -rf scratch/*
-
- if ! quiet invoke_bfs scratch -quit -capable; then
- return 0
- fi
-
- touch scratch/{normal,capable}
- sudo setcap all+ep scratch/capable
- ln -s capable scratch/link
-
- bfs_diff -L scratch -capable
-}
-
-function make_xattrs() {
- rm -rf scratch/*
-
- touch scratch/{normal,xattr,xattr_2}
- ln -s xattr scratch/link
- ln -s normal scratch/xattr_link
-
- case "$UNAME" in
- Darwin)
- xattr -w bfs_test true scratch/xattr \
- && xattr -w bfs_test_2 true scratch/xattr_2 \
- && xattr -s -w bfs_test true scratch/xattr_link
- ;;
- FreeBSD)
- setextattr user bfs_test true scratch/xattr \
- && setextattr user bfs_test_2 true scratch/xattr_2 \
- && setextattr -h user bfs_test true scratch/xattr_link
- ;;
- *)
- # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
- # namespace, which is writable by root and readable by others
- sudo setfattr -n security.bfs_test scratch/xattr \
- && sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \
- && sudo setfattr -h -n security.bfs_test scratch/xattr_link
- ;;
- esac
-}
-
-function test_xattr() {
- quiet invoke_bfs scratch -quit -xattr || return 0
- make_xattrs || return 0
- bfs_diff scratch -xattr
-}
-
-function test_L_xattr() {
- quiet invoke_bfs scratch -quit -xattr || return 0
- make_xattrs || return 0
- bfs_diff -L scratch -xattr
-}
-
-function test_xattrname() {
- quiet invoke_bfs scratch -quit -xattr || return 0
- make_xattrs || return 0
-
- case "$UNAME" in
- Darwin|FreeBSD)
- bfs_diff scratch -xattrname bfs_test
- ;;
- *)
- bfs_diff scratch -xattrname security.bfs_test
- ;;
- esac
-}
-
-function test_L_xattrname() {
- quiet invoke_bfs scratch -quit -xattr || return 0
- make_xattrs || return 0
-
- case "$UNAME" in
- Darwin|FreeBSD)
- bfs_diff -L scratch -xattrname bfs_test
- ;;
- *)
- bfs_diff -L scratch -xattrname security.bfs_test
- ;;
- esac
-}
-
-function test_help() {
- invoke_bfs -help | grep -E '\{...?\}' && return 1
- invoke_bfs -D help | grep -E '\{...?\}' && return 1
- invoke_bfs -S help | grep -E '\{...?\}' && return 1
- invoke_bfs -regextype help | grep -E '\{...?\}' && return 1
-
- return 0
-}
-
-function test_version() {
- invoke_bfs -version >/dev/null
-}
-
-function test_typo() {
- invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null
-}
-
-function test_D_multi() {
- quiet bfs_diff -D opt,tree,unknown basic
-}
-
-function test_D_all() {
- quiet bfs_diff -D all basic
-}
-
-function test_O0() {
- bfs_diff -O0 basic -not \( -type f -not -type f \)
-}
-
-function test_O1() {
- bfs_diff -O1 basic -not \( -type f -not -type f \)
-}
-
-function test_O2() {
- bfs_diff -O2 basic -not \( -type f -not -type f \)
-}
-
-function test_O3() {
- bfs_diff -O3 basic -not \( -type f -not -type f \)
-}
-
-function test_Ofast() {
- bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \)
-}
-
-function test_S() {
- invoke_bfs -S "$1" -s basic >"$TMP/test_S_$1.out"
-
- if [ "$UPDATE" ]; then
- cp {"$TMP","$TESTS"}/"test_S_$1.out"
- else
- diff -u {"$TESTS","$TMP"}/"test_S_$1.out"
- fi
-}
-
-function test_S_bfs() {
- test_S bfs
-}
-
-function test_S_dfs() {
- test_S dfs
-}
-
-function test_S_ids() {
- test_S ids
-}
-
-function test_exclude_name() {
- bfs_diff basic -exclude -name foo
-}
-
-function test_exclude_depth() {
- bfs_diff basic -depth -exclude -name foo
-}
-
-function test_exclude_mindepth() {
- bfs_diff basic -mindepth 3 -exclude -name foo
-}
-
-function test_exclude_print() {
- ! quiet invoke_bfs basic -exclude -print
-}
-
-function test_exclude_exclude() {
- ! quiet invoke_bfs basic -exclude -exclude -name foo
-}
-
-function test_flags() {
- quiet invoke_bfs scratch -quit -flags offline || return 0
-
- rm -rf scratch/*
-
- touch scratch/{foo,bar}
- chflags offline scratch/bar || return 0
-
- bfs_diff scratch -flags -offline,nohidden
-}
-
-function test_files0_from_file() {
- cd weirdnames
- invoke_bfs -mindepth 1 -fprintf ../scratch/files0.in "%P\0"
- bfs_diff -files0-from ../scratch/files0.in
-}
-
-function test_files0_from_stdin() {
- cd weirdnames
- invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from -
-}
-
-function test_files0_from_none() {
- ! printf "" | quiet invoke_bfs -files0-from -
-}
-
-function test_files0_from_empty() {
- ! printf "\0" | quiet invoke_bfs -files0-from -
-}
-
-function test_files0_from_nowhere() {
- ! quiet invoke_bfs -files0-from
-}
-
-function test_files0_from_nothing() {
- ! quiet invoke_bfs -files0-from basic/nonexistent
-}
-
-function test_files0_from_ok() {
- ! printf "basic\0" | quiet invoke_bfs -files0-from - -ok echo {} \;
-}
-
-function test_stderr_fails_silently() {
- if [ -e /dev/full ]; then
- bfs_diff -D all basic 2>/dev/full
- fi
-}
-
-function test_stderr_fails_loudly() {
- if [ -e /dev/full ]; then
- ! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full
- fi
-}
-
-
-BOL=
-EOL='\n'
-
-function update_eol() {
- # Put the cursor at the last column, then write a space so the next
- # character will wrap
- EOL="\\033[${COLUMNS}G "
-}
-
-if [ -t 1 -a ! "$VERBOSE" ]; then
- BOL='\r\033[K'
-
- # Workaround for bash 4: checkwinsize is off by default. We can turn it on,
- # but we also have to explicitly trigger a foreground job to finish so that
- # it will update the window size before we use $COLUMNS
- shopt -s checkwinsize
- (:)
-
- update_eol
- trap update_eol WINCH
-fi
-
-passed=0
-failed=0
-
-for test in "${enabled_tests[@]}"; do
- printf "${BOL}${YLW}%s${RST}${EOL}" "$test"
-
- ("$test")
- status=$?
-
- if [ $status -eq 0 ]; then
- ((++passed))
- else
- ((++failed))
- printf "${BOL}${RED}%s failed!${RST}\n" "$test"
- fi
-done
-
-if [ $passed -gt 0 ]; then
- printf "${BOL}${GRN}tests passed: %d${RST}\n" "$passed"
-fi
-if [ $failed -gt 0 ]; then
- printf "${BOL}${RED}tests failed: %s${RST}\n" "$failed"
- exit 1
-fi
diff --git a/tests/alloc.c b/tests/alloc.c
new file mode 100644
index 0000000..4aae515
--- /dev/null
+++ b/tests/alloc.c
@@ -0,0 +1,78 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "alloc.h"
+#include "diag.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+struct flexible {
+ alignas(64) int foo[8];
+ int bar[];
+};
+
+/** Check varena_realloc() poisoning for a size combination. */
+static struct flexible *check_varena_realloc(struct varena *varena, struct flexible *flexy, size_t old_count, size_t new_count) {
+ flexy = varena_realloc(varena, flexy, old_count, new_count);
+ bfs_everify(flexy);
+
+ for (size_t i = 0; i < new_count; ++i) {
+ if (i < old_count) {
+ bfs_check(flexy->bar[i] == (int)i);
+ } else {
+ flexy->bar[i] = i;
+ }
+ }
+
+ return flexy;
+}
+
+void check_alloc(void) {
+ // Check aligned allocation
+ void *ptr;
+ bfs_everify((ptr = zalloc(64, 129)));
+ bfs_check((uintptr_t)ptr % 64 == 0);
+ bfs_echeck((ptr = xrealloc(ptr, 64, 129, 65)));
+ bfs_check((uintptr_t)ptr % 64 == 0);
+ free(ptr);
+
+ // Check sizeof_flex()
+ bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible));
+ bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0);
+
+ // volatile to suppress -Walloc-size-larger-than
+ volatile size_t too_many = SIZE_MAX / sizeof(int) + 1;
+ bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX));
+
+ // Make sure we detect allocation size overflows
+ bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+ bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+
+ // varena tests
+ struct varena varena;
+ VARENA_INIT(&varena, struct flexible, bar);
+
+ for (size_t i = 0; i < 256; ++i) {
+ bfs_everify(varena_alloc(&varena, i));
+ struct arena *arena = &varena.arenas[varena.narenas - 1];
+ bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i));
+ }
+
+ // Check varena_realloc() (un)poisoning
+ struct flexible *flexy = varena_alloc(&varena, 160);
+ bfs_everify(flexy);
+
+ flexy = check_varena_realloc(&varena, flexy, 0, 160);
+ flexy = check_varena_realloc(&varena, flexy, 160, 192);
+ flexy = check_varena_realloc(&varena, flexy, 192, 160);
+ flexy = check_varena_realloc(&varena, flexy, 160, 320);
+ flexy = check_varena_realloc(&varena, flexy, 320, 96);
+
+ varena_destroy(&varena);
+}
diff --git a/tests/test_S_dfs.out b/tests/bfs/D_all.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_S_dfs.out
+++ b/tests/bfs/D_all.out
diff --git a/tests/bfs/D_all.sh b/tests/bfs/D_all.sh
new file mode 100644
index 0000000..170698a
--- /dev/null
+++ b/tests/bfs/D_all.sh
@@ -0,0 +1 @@
+bfs_diff -D all basic
diff --git a/tests/bfs/D_incomplete.sh b/tests/bfs/D_incomplete.sh
new file mode 100644
index 0000000..30c522a
--- /dev/null
+++ b/tests/bfs/D_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -D
diff --git a/tests/test_fprint.out b/tests/bfs/D_multi.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_fprint.out
+++ b/tests/bfs/D_multi.out
diff --git a/tests/bfs/D_multi.sh b/tests/bfs/D_multi.sh
new file mode 100644
index 0000000..08a8ca6
--- /dev/null
+++ b/tests/bfs/D_multi.sh
@@ -0,0 +1 @@
+bfs_diff -D opt,tree,unknown basic
diff --git a/tests/test_type_f.out b/tests/bfs/D_opt.out
index 6218a0c..6218a0c 100644
--- a/tests/test_type_f.out
+++ b/tests/bfs/D_opt.out
diff --git a/tests/bfs/D_opt.sh b/tests/bfs/D_opt.sh
new file mode 100644
index 0000000..c14fe70
--- /dev/null
+++ b/tests/bfs/D_opt.sh
@@ -0,0 +1 @@
+bfs_diff -D opt -nohidden -not \( -type c -o -type d \) -links -5 -links -10 -not -hidden basic
diff --git a/tests/test_O1.out b/tests/bfs/D_unknown.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O1.out
+++ b/tests/bfs/D_unknown.out
@@ -2,18 +2,18 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/D_unknown.sh b/tests/bfs/D_unknown.sh
new file mode 100644
index 0000000..cac9bd9
--- /dev/null
+++ b/tests/bfs/D_unknown.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -D unknown basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/test_O2.out b/tests/bfs/Dmulti.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O2.out
+++ b/tests/bfs/Dmulti.out
@@ -2,18 +2,18 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/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/test_L.out b/tests/bfs/LD_stat.out
index e24f4f7..ec9e861 100644
--- a/tests/test_L.out
+++ b/tests/bfs/LD_stat.out
@@ -1,17 +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/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/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/test_L_depth.out b/tests/bfs/LDstat.out
index e24f4f7..ec9e861 100644
--- a/tests/test_L_depth.out
+++ b/tests/bfs/LDstat.out
@@ -1,17 +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/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/link
+links/symlink
diff --git a/tests/bfs/LDstat.sh b/tests/bfs/LDstat.sh
new file mode 100644
index 0000000..ec6df0b
--- /dev/null
+++ b/tests/bfs/LDstat.sh
@@ -0,0 +1 @@
+bfs_diff -LDstat links
diff --git a/tests/bfs/L_capable.out b/tests/bfs/L_capable.out
new file mode 100644
index 0000000..0810d4a
--- /dev/null
+++ b/tests/bfs/L_capable.out
@@ -0,0 +1,2 @@
+./capable
+./link
diff --git a/tests/bfs/L_capable.sh b/tests/bfs/L_capable.sh
new file mode 100644
index 0000000..97c404f
--- /dev/null
+++ b/tests/bfs/L_capable.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+bfs_diff -L . -capable
diff --git a/tests/test_L_loops_continue.out b/tests/bfs/L_noerror.out
index fbe0cac..a514555 100644
--- a/tests/test_L_loops_continue.out
+++ b/tests/bfs/L_noerror.out
@@ -1,11 +1,11 @@
loops
loops/broken
loops/deeply
+loops/deeply/nested
+loops/deeply/nested/dir
loops/file
loops/notdir
loops/skip
-loops/symlink
-loops/deeply/nested
loops/skip/dir
loops/skip/loop
-loops/deeply/nested/dir
+loops/symlink
diff --git a/tests/bfs/L_noerror.sh b/tests/bfs/L_noerror.sh
new file mode 100644
index 0000000..7db2a4d
--- /dev/null
+++ b/tests/bfs/L_noerror.sh
@@ -0,0 +1 @@
+bfs_diff -L loops -noerror
diff --git a/tests/test_L_unique.out b/tests/bfs/L_unique.out
index c94c48e..c94c48e 100644
--- a/tests/test_L_unique.out
+++ b/tests/bfs/L_unique.out
diff --git a/tests/bfs/L_unique.sh b/tests/bfs/L_unique.sh
new file mode 100644
index 0000000..c804526
--- /dev/null
+++ b/tests/bfs/L_unique.sh
@@ -0,0 +1 @@
+bfs_diff -L links/{file,symlink,hardlink} -unique
diff --git a/tests/test_L_unique_depth.out b/tests/bfs/L_unique_depth.out
index dad0a98..dad0a98 100644
--- a/tests/test_L_unique_depth.out
+++ b/tests/bfs/L_unique_depth.out
diff --git a/tests/bfs/L_unique_depth.sh b/tests/bfs/L_unique_depth.sh
new file mode 100644
index 0000000..fb9aca1
--- /dev/null
+++ b/tests/bfs/L_unique_depth.sh
@@ -0,0 +1 @@
+bfs_diff -L loops/deeply/nested -unique -depth
diff --git a/tests/test_L_unique_loops.out b/tests/bfs/L_unique_loops.out
index dad0a98..dad0a98 100644
--- a/tests/test_L_unique_loops.out
+++ b/tests/bfs/L_unique_loops.out
diff --git a/tests/bfs/L_unique_loops.sh b/tests/bfs/L_unique_loops.sh
new file mode 100644
index 0000000..2bdd94e
--- /dev/null
+++ b/tests/bfs/L_unique_loops.sh
@@ -0,0 +1 @@
+bfs_diff -L loops/deeply/nested -unique
diff --git a/tests/test_O3.out b/tests/bfs/O0.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O3.out
+++ b/tests/bfs/O0.out
@@ -2,18 +2,18 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/O0.sh b/tests/bfs/O0.sh
new file mode 100644
index 0000000..0f92d71
--- /dev/null
+++ b/tests/bfs/O0.sh
@@ -0,0 +1 @@
+bfs_diff -O0 basic -not \( -type f -not -type f \)
diff --git a/tests/test_Ofast.out b/tests/bfs/O1.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_Ofast.out
+++ b/tests/bfs/O1.out
@@ -2,18 +2,18 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/O1.sh b/tests/bfs/O1.sh
new file mode 100644
index 0000000..924b410
--- /dev/null
+++ b/tests/bfs/O1.sh
@@ -0,0 +1 @@
+bfs_diff -O1 basic -not \( -type f -not -type f \)
diff --git a/tests/bfs/O2.out b/tests/bfs/O2.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/O2.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/O2.sh b/tests/bfs/O2.sh
new file mode 100644
index 0000000..9382456
--- /dev/null
+++ b/tests/bfs/O2.sh
@@ -0,0 +1 @@
+bfs_diff -O2 basic -not \( -type f -not -type f \)
diff --git a/tests/bfs/O3.out b/tests/bfs/O3.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/O3.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/O3.sh b/tests/bfs/O3.sh
new file mode 100644
index 0000000..5bdf2bc
--- /dev/null
+++ b/tests/bfs/O3.sh
@@ -0,0 +1 @@
+bfs_diff -O3 basic -not \( -type f -not -type f \)
diff --git a/tests/bfs/O9.out b/tests/bfs/O9.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/O9.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/O9.sh b/tests/bfs/O9.sh
new file mode 100644
index 0000000..c12a7a3
--- /dev/null
+++ b/tests/bfs/O9.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -O9 basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/bfs/O_3.sh b/tests/bfs/O_3.sh
new file mode 100644
index 0000000..f159852
--- /dev/null
+++ b/tests/bfs/O_3.sh
@@ -0,0 +1 @@
+! invoke_bfs -O 3 basic
diff --git a/tests/bfs/Ofast.out b/tests/bfs/Ofast.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/Ofast.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/Ofast.sh b/tests/bfs/Ofast.sh
new file mode 100644
index 0000000..87c1d8d
--- /dev/null
+++ b/tests/bfs/Ofast.sh
@@ -0,0 +1 @@
+bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \)
diff --git a/tests/test_D_all.out b/tests/bfs/S_bfs.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_D_all.out
+++ b/tests/bfs/S_bfs.out
diff --git a/tests/bfs/S_bfs.sh b/tests/bfs/S_bfs.sh
new file mode 100644
index 0000000..76976de
--- /dev/null
+++ b/tests/bfs/S_bfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S bfs -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/S_dfs.out b/tests/bfs/S_dfs.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/S_dfs.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/S_dfs.sh b/tests/bfs/S_dfs.sh
new file mode 100644
index 0000000..7dd7a46
--- /dev/null
+++ b/tests/bfs/S_dfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S dfs -s basic >"$OUT"
+diff_output
diff --git a/tests/test_D_multi.out b/tests/bfs/S_ids.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_D_multi.out
+++ b/tests/bfs/S_ids.out
diff --git a/tests/bfs/S_ids.sh b/tests/bfs/S_ids.sh
new file mode 100644
index 0000000..3995cf0
--- /dev/null
+++ b/tests/bfs/S_ids.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S ids -s basic >"$OUT"
+diff_output
diff --git a/tests/test_O0.out b/tests/bfs/Sbfs.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_O0.out
+++ b/tests/bfs/Sbfs.out
diff --git a/tests/bfs/Sbfs.sh b/tests/bfs/Sbfs.sh
new file mode 100644
index 0000000..72d92c8
--- /dev/null
+++ b/tests/bfs/Sbfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -Sbfs -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/and_incomplete.sh b/tests/bfs/and_incomplete.sh
new file mode 100644
index 0000000..05abc2d
--- /dev/null
+++ b/tests/bfs/and_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -a
diff --git a/tests/bfs/capable.out b/tests/bfs/capable.out
new file mode 100644
index 0000000..ac7b5ce
--- /dev/null
+++ b/tests/bfs/capable.out
@@ -0,0 +1 @@
+./capable
diff --git a/tests/bfs/capable.sh b/tests/bfs/capable.sh
new file mode 100644
index 0000000..35bb0b4
--- /dev/null
+++ b/tests/bfs/capable.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+bfs_diff . -capable
diff --git a/tests/bfs/closed_stderr.sh b/tests/bfs/closed_stderr.sh
new file mode 100644
index 0000000..26abd85
--- /dev/null
+++ b/tests/bfs/closed_stderr.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stderr before we can
+(bash -c 'echo >&2' 2>&-) && skip
+
+! invoke_bfs basic >&- 2>&-
diff --git a/tests/bfs/closed_stdin.out b/tests/bfs/closed_stdin.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/closed_stdin.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/closed_stdin.sh b/tests/bfs/closed_stdin.sh
new file mode 100644
index 0000000..6932be8
--- /dev/null
+++ b/tests/bfs/closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic <&-
diff --git a/tests/bfs/closed_stdout.sh b/tests/bfs/closed_stdout.sh
new file mode 100644
index 0000000..5b6f7c3
--- /dev/null
+++ b/tests/bfs/closed_stdout.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stdout before we can
+(bash -c echo >&-) && skip
+
+! invoke_bfs basic >&-
diff --git a/tests/test_color.out b/tests/bfs/color.out
index 77fc8a8..a439814 100644
--- a/tests/test_color.out
+++ b/tests/bfs/color.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color.sh b/tests/bfs/color.sh
new file mode 100644
index 0000000..23f05a3
--- /dev/null
+++ b/tests/bfs/color.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -color
diff --git a/tests/test_color_L.out b/tests/bfs/color_L.out
index b60dd4a..85923db 100644
--- a/tests/test_color_L.out
+++ b/tests/bfs/color_L.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/chardev_link
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_L.sh b/tests/bfs/color_L.sh
new file mode 100644
index 0000000..823db62
--- /dev/null
+++ b/tests/bfs/color_L.sh
@@ -0,0 +1 @@
+bfs_diff -L rainbow -color
diff --git a/tests/test_color_ln_target.out b/tests/bfs/color_L_ln_target.out
index cd4ec5e..23fe8d7 100644
--- a/tests/test_color_ln_target.out
+++ b/tests/bfs/color_L_ln_target.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_L_ln_target.sh b/tests/bfs/color_L_ln_target.sh
new file mode 100644
index 0000000..cc5991d
--- /dev/null
+++ b/tests/bfs/color_L_ln_target.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color
diff --git a/tests/test_color_L_no_stat.out b/tests/bfs/color_L_no_stat.out
index c0bb1be..72e0319 100644
--- a/tests/test_color_L_no_stat.out
+++ b/tests/bfs/color_L_no_stat.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/chardev_link
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
rainbow/socket
rainbow/broken
rainbow/file.txt
@@ -10,11 +9,19 @@
rainbow/pipe
rainbow/exec.sh
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_L_no_stat.sh b/tests/bfs/color_L_no_stat.sh
new file mode 100644
index 0000000..0a2caf0
--- /dev/null
+++ b/tests/bfs/color_L_no_stat.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color
diff --git a/tests/test_color_ext_override.out b/tests/bfs/color_auto.out
index 1377b65..a439814 100644
--- a/tests/test_color_ext_override.out
+++ b/tests/bfs/color_auto.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/star.tar
-rainbow/star.gz
-rainbow/star.tar.gz
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -16,5 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_auto.sh b/tests/bfs/color_auto.sh
new file mode 100644
index 0000000..7e875cc
--- /dev/null
+++ b/tests/bfs/color_auto.sh
@@ -0,0 +1,4 @@
+unset NO_COLOR
+bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/color_ca.out b/tests/bfs/color_ca.out
new file mode 100644
index 0000000..bf74202
--- /dev/null
+++ b/tests/bfs/color_ca.out
@@ -0,0 +1,4 @@
+.
+./link
+./capable
+./normal
diff --git a/tests/bfs/color_ca.sh b/tests/bfs/color_ca.sh
new file mode 100644
index 0000000..3aaaaf1
--- /dev/null
+++ b/tests/bfs/color_ca.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+LS_COLORS="ca=30;41:" bfs_diff . -color
diff --git a/tests/test_color_ext_underride.out b/tests/bfs/color_ca_incapable.out
index 787248a..a439814 100644
--- a/tests/test_color_ext_underride.out
+++ b/tests/bfs/color_ca_incapable.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
-rainbow/star.tar.gz
rainbow/exec.sh
-rainbow/star.tar
-rainbow/star.gz
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -16,5 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ca_incapable.sh b/tests/bfs/color_ca_incapable.sh
new file mode 100644
index 0000000..f46a127
--- /dev/null
+++ b/tests/bfs/color_ca_incapable.sh
@@ -0,0 +1 @@
+LS_COLORS="ca=30;41:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_cd0_no.out b/tests/bfs/color_cd0_no.out
new file mode 100644
index 0000000..37b3fbc
--- /dev/null
+++ b/tests/bfs/color_cd0_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/chardev_link
diff --git a/tests/bfs/color_cd0_no.sh b/tests/bfs/color_cd0_no.sh
new file mode 100644
index 0000000..325a782
--- /dev/null
+++ b/tests/bfs/color_cd0_no.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:cd=0:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_deep.out b/tests/bfs/color_deep.out
new file mode 100644
index 0000000..fb990d5
--- /dev/null
+++ b/tests/bfs/color_deep.out
@@ -0,0 +1,16 @@
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/bfs/color_deep.sh b/tests/bfs/color_deep.sh
new file mode 100644
index 0000000..a83ee0e
--- /dev/null
+++ b/tests/bfs/color_deep.sh
@@ -0,0 +1,7 @@
+name="0123456789ABCDEF"
+name="${name}${name}${name}${name}"
+name="${name}${name}${name}${name}"
+name="${name:0:255}"
+export LS_COLORS="*${name}=01:"
+
+bfs_diff deep -color -type f -printf '%f\n'
diff --git a/tests/test_color_escapes.out b/tests/bfs/color_escapes.out
index b71e138..0bf9fbb 100644
--- a/tests/test_color_escapes.out
+++ b/tests/bfs/color_escapes.out
@@ -1,3 +1,5 @@
+:$'rainbow/\e[1m'
+:$'rainbow/\e[1m/'$'\e[0m'
:rainbow
:rainbow/:exec.sh
:rainbow/:socket
@@ -13,8 +15,13 @@
:rainbow/:sticky
:rainbow/file.dat
:rainbow/file.txt
+:rainbow/lower.gz
+:rainbow/lower.tar
+:rainbow/lower.tar.gz
+:rainbow/lu.tar.GZ
:rainbow/mh1
:rainbow/mh2
-:rainbow/star.gz
-:rainbow/star.tar
-:rainbow/star.tar.gz
+:rainbow/ul.TAR.gz
+:rainbow/upper.GZ
+:rainbow/upper.TAR
+:rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_escapes.sh b/tests/bfs/color_escapes.sh
new file mode 100644
index 0000000..eb5817f
--- /dev/null
+++ b/tests/bfs/color_escapes.sh
@@ -0,0 +1 @@
+LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color
diff --git a/tests/test_color_missing_colon.out b/tests/bfs/color_ext.out
index cf26e73..218100f 100644
--- a/tests/test_color_missing_colon.out
+++ b/tests/bfs/color_ext.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ext.sh b/tests/bfs/color_ext.sh
new file mode 100644
index 0000000..c9f6d46
--- /dev/null
+++ b/tests/bfs/color_ext.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=01:" bfs_diff rainbow -color
diff --git a/tests/test_color_ext0.out b/tests/bfs/color_ext0.out
index e764a6b..d2a7fd5 100644
--- a/tests/test_color_ext0.out
+++ b/tests/bfs/color_ext0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/file.txt
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ext0.sh b/tests/bfs/color_ext0.sh
new file mode 100644
index 0000000..371a9c5
--- /dev/null
+++ b/tests/bfs/color_ext0.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_case.out b/tests/bfs/color_ext_case.out
new file mode 100644
index 0000000..93dc8f6
--- /dev/null
+++ b/tests/bfs/color_ext_case.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/exec.sh
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/ul.TAR.gz
+rainbow/lu.tar.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/file.txt
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh
new file mode 100644
index 0000000..4c14610
--- /dev/null
+++ b/tests/bfs/color_ext_case.sh
@@ -0,0 +1,6 @@
+# *.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32 -- case sensitive
+# *.tAr=01;33:*.TaR=01;33 -- case-insensitive
+# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive
+# *.txt=35:*TXT=36 -- case-insensitive
+export LS_COLORS="*.gz=01;30:*.gz=01;31:*.GZ=01;30:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36"
+bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_case_flipflop.out b/tests/bfs/color_ext_case_flipflop.out
new file mode 100644
index 0000000..f4cc53c
--- /dev/null
+++ b/tests/bfs/color_ext_case_flipflop.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.TAR.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/mh1
+rainbow/mh2
+rainbow/upper.GZ
+rainbow/upper.TAR
diff --git a/tests/bfs/color_ext_case_flipflop.sh b/tests/bfs/color_ext_case_flipflop.sh
new file mode 100644
index 0000000..4d6f615
--- /dev/null
+++ b/tests/bfs/color_ext_case_flipflop.sh
@@ -0,0 +1 @@
+LS_COLORS="*.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33:*.tar.gz=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_case_nul.out b/tests/bfs/color_ext_case_nul.out
new file mode 100644
index 0000000..8ccd9a7
--- /dev/null
+++ b/tests/bfs/color_ext_case_nul.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.tar
+rainbow/mh1
+rainbow/mh2
+rainbow/upper.TAR
diff --git a/tests/bfs/color_ext_case_nul.sh b/tests/bfs/color_ext_case_nul.sh
new file mode 100644
index 0000000..68fea1c
--- /dev/null
+++ b/tests/bfs/color_ext_case_nul.sh
@@ -0,0 +1,5 @@
+# Regression test: embedded NUL bytes in an extension caused an assertion
+# failure in the trie implementation
+
+export LS_COLORS='*.gz=01;31:*\0.GZ=01;32:'
+bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_case_priority.out b/tests/bfs/color_ext_case_priority.out
new file mode 100644
index 0000000..4a6c9a0
--- /dev/null
+++ b/tests/bfs/color_ext_case_priority.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.tar.gz
+rainbow/exec.sh
+rainbow/upper.TAR.GZ
+rainbow/lower.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.tar
+rainbow/mh1
+rainbow/mh2
+rainbow/upper.TAR
diff --git a/tests/bfs/color_ext_case_priority.sh b/tests/bfs/color_ext_case_priority.sh
new file mode 100644
index 0000000..f178c56
--- /dev/null
+++ b/tests/bfs/color_ext_case_priority.sh
@@ -0,0 +1 @@
+LS_COLORS="*.gz=01;33:*.tar.gz=01;31:*.TAR.GZ=01;32:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_override.out b/tests/bfs/color_ext_override.out
new file mode 100644
index 0000000..0acfcbc
--- /dev/null
+++ b/tests/bfs/color_ext_override.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh
new file mode 100644
index 0000000..9f818c9
--- /dev/null
+++ b/tests/bfs/color_ext_override.sh
@@ -0,0 +1 @@
+LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;30:*.gz=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_underride.out b/tests/bfs/color_ext_underride.out
new file mode 100644
index 0000000..5c98341
--- /dev/null
+++ b/tests/bfs/color_ext_underride.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.TAR.GZ
+rainbow/exec.sh
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/lower.gz
+rainbow/upper.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_underride.sh b/tests/bfs/color_ext_underride.sh
new file mode 100644
index 0000000..fb12e01
--- /dev/null
+++ b/tests/bfs/color_ext_underride.sh
@@ -0,0 +1 @@
+LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color
diff --git a/tests/test_color_mh0.out b/tests/bfs/color_fi0_no.out
index 77fc8a8..a439814 100644
--- a/tests/test_color_mh0.out
+++ b/tests/bfs/color_fi0_no.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_fi0_no.sh b/tests/bfs/color_fi0_no.sh
new file mode 100644
index 0000000..f947d64
--- /dev/null
+++ b/tests/bfs/color_fi0_no.sh
@@ -0,0 +1 @@
+LS_COLORS="fi=0:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_fi_no.out b/tests/bfs/color_fi_no.out
new file mode 100644
index 0000000..1c1ad8e
--- /dev/null
+++ b/tests/bfs/color_fi_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
diff --git a/tests/bfs/color_fi_no.sh b/tests/bfs/color_fi_no.sh
new file mode 100644
index 0000000..c2b4ec7
--- /dev/null
+++ b/tests/bfs/color_fi_no.sh
@@ -0,0 +1 @@
+LS_COLORS="fi=01;91:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/test_color_L_ln_target.out b/tests/bfs/color_ln_target.out
index cd4ec5e..23fe8d7 100644
--- a/tests/test_color_L_ln_target.out
+++ b/tests/bfs/color_ln_target.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ln_target.sh b/tests/bfs/color_ln_target.sh
new file mode 100644
index 0000000..707d25e
--- /dev/null
+++ b/tests/bfs/color_ln_target.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ls.out b/tests/bfs/color_ls.out
new file mode 100644
index 0000000..cc64318
--- /dev/null
+++ b/tests/bfs/color_ls.out
@@ -0,0 +1,12 @@
+scratch/foo/bar
+scratch/foo/bar
+/__bfs__/nowhere
+/__bfs__/nowhere
+foo/bar/nowhere
+foo/bar/nowhere
+foo/bar/nowhere/nothing
+foo/bar/nowhere/nothing
+foo/bar/baz
+foo/bar/baz
+foo/bar/baz//qux
+foo/bar/baz//qux
diff --git a/tests/bfs/color_ls.sh b/tests/bfs/color_ls.sh
new file mode 100644
index 0000000..f1cc216
--- /dev/null
+++ b/tests/bfs/color_ls.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+"$XTOUCH" -p scratch/foo/bar/baz
+ln -s foo/bar/baz scratch/link
+ln -s foo/bar/nowhere scratch/broken
+ln -s foo/bar/nowhere/nothing scratch/nested
+ln -s foo/bar/baz//qux scratch/notdir
+ln -s scratch/foo/bar scratch/relative
+mkdir scratch/__bfs__
+ln -s /__bfs__/nowhere scratch/absolute
+
+export LS_COLORS="or=01;31:"
+invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \
+ | sed 's/.* -> //' \
+ | sort >"$OUT"
+diff_output
diff --git a/tests/test_color_mh.out b/tests/bfs/color_mh.out
index 757a6a1..c658082 100644
--- a/tests/test_color_mh.out
+++ b/tests/bfs/color_mh.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -15,6 +17,11 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mh.sh b/tests/bfs/color_mh.sh
new file mode 100644
index 0000000..aff1845
--- /dev/null
+++ b/tests/bfs/color_mh.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=01:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_mh0.out b/tests/bfs/color_mh0.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_mh0.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mh0.sh b/tests/bfs/color_mh0.sh
new file mode 100644
index 0000000..7de880d
--- /dev/null
+++ b/tests/bfs/color_mh0.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_mi.out b/tests/bfs/color_mi.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_mi.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mi.sh b/tests/bfs/color_mi.sh
new file mode 100644
index 0000000..06dd8c6
--- /dev/null
+++ b/tests/bfs/color_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="mi=01:" bfs_diff rainbow -color
diff --git a/tests/test_color_ext.out b/tests/bfs/color_missing_colon.out
index cf26e73..218100f 100644
--- a/tests/test_color_ext.out
+++ b/tests/bfs/color_missing_colon.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_missing_colon.sh b/tests/bfs/color_missing_colon.sh
new file mode 100644
index 0000000..afa3763
--- /dev/null
+++ b/tests/bfs/color_missing_colon.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=01" bfs_diff rainbow -color
diff --git a/tests/bfs/color_no.out b/tests/bfs/color_no.out
new file mode 100644
index 0000000..67e1eee
--- /dev/null
+++ b/tests/bfs/color_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
diff --git a/tests/bfs/color_no.sh b/tests/bfs/color_no.sh
new file mode 100644
index 0000000..b7527cb
--- /dev/null
+++ b/tests/bfs/color_no.sh
@@ -0,0 +1 @@
+LS_COLORS="no=01;92:" bfs_diff rainbow -color
diff --git a/tests/test_color_no_stat.out b/tests/bfs/color_no_stat.out
index 1fc5324..e3031b2 100644
--- a/tests/test_color_no_stat.out
+++ b/tests/bfs/color_no_stat.out
@@ -1,7 +1,6 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -10,11 +9,19 @@
rainbow/pipe
rainbow/exec.sh
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_no_stat.sh b/tests/bfs/color_no_stat.sh
new file mode 100644
index 0000000..0bc2520
--- /dev/null
+++ b/tests/bfs/color_no_stat.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color
diff --git a/tests/test_L_ilname.out b/tests/bfs/color_notdir_slash_error.out
index e69de29..e69de29 100644
--- a/tests/test_L_ilname.out
+++ b/tests/bfs/color_notdir_slash_error.out
diff --git a/tests/bfs/color_notdir_slash_error.sh b/tests/bfs/color_notdir_slash_error.sh
new file mode 100644
index 0000000..ca26d50
--- /dev/null
+++ b/tests/bfs/color_notdir_slash_error.sh
@@ -0,0 +1,2 @@
+# Regression test: infinite loop printing the error message for .../notdir/nowhere
+! bfs_diff -color links/notdir/nowhere
diff --git a/tests/bfs/color_nul.out b/tests/bfs/color_nul.out
new file mode 100644
index 0000000..8ccd9a7
--- /dev/null
+++ b/tests/bfs/color_nul.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.tar
+rainbow/mh1
+rainbow/mh2
+rainbow/upper.TAR
diff --git a/tests/bfs/color_nul.sh b/tests/bfs/color_nul.sh
new file mode 100644
index 0000000..cb662d6
--- /dev/null
+++ b/tests/bfs/color_nul.sh
@@ -0,0 +1,3 @@
+LS_COLORS="ec=\33[\0m:*.gz=\0\61;31:" invoke_bfs rainbow -color | tr '\0' '0' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/test_color_or.out b/tests/bfs/color_or.out
index 9e1fe5c..0bd2570 100644
--- a/tests/test_color_or.out
+++ b/tests/bfs/color_or.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or.sh b/tests/bfs/color_or.sh
new file mode 100644
index 0000000..bccb400
--- /dev/null
+++ b/tests/bfs/color_or.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_or0_mi.out b/tests/bfs/color_or0_mi.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_or0_mi.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or0_mi.sh b/tests/bfs/color_or0_mi.sh
new file mode 100644
index 0000000..a362cf1
--- /dev/null
+++ b/tests/bfs/color_or0_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_or0_mi0.out b/tests/bfs/color_or0_mi0.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_or0_mi0.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or0_mi0.sh b/tests/bfs/color_or0_mi0.sh
new file mode 100644
index 0000000..d7c00f6
--- /dev/null
+++ b/tests/bfs/color_or0_mi0.sh
@@ -0,0 +1 @@
+LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_or_mi.out b/tests/bfs/color_or_mi.out
index 5667f56..fb67e58 100644
--- a/tests/test_color_or_mi.out
+++ b/tests/bfs/color_or_mi.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or_mi.sh b/tests/bfs/color_or_mi.sh
new file mode 100644
index 0000000..467ce6b
--- /dev/null
+++ b/tests/bfs/color_or_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/test_color_or_mi0.out b/tests/bfs/color_or_mi0.out
index 5667f56..fb67e58 100644
--- a/tests/test_color_or_mi0.out
+++ b/tests/bfs/color_or_mi0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or_mi0.sh b/tests/bfs/color_or_mi0.sh
new file mode 100644
index 0000000..a9c36bf
--- /dev/null
+++ b/tests/bfs/color_or_mi0.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_rs_lc_rc_ec.out b/tests/bfs/color_rs_lc_rc_ec.out
index a71df24..077ef8d 100644
--- a/tests/test_color_rs_lc_rc_ec.out
+++ b/tests/bfs/color_rs_lc_rc_ec.out
@@ -1,4 +1,5 @@
-LC01;34RCrainbowEC
+LC01;34RC$'rainbow/\e[1m'EC
+LC01;34RC$'rainbow/\e[1m/'EC$'\e[0m'
LC01;34RCrainbow/ECLC01;32RCexec.shEC
LC01;34RCrainbow/ECLC01;35RCsocketEC
LC01;34RCrainbow/ECLC01;36RCbrokenEC
@@ -13,8 +14,14 @@ LC01;34RCrainbow/ECLC37;41RCsuidEC
LC01;34RCrainbow/ECLC37;44RCstickyEC
LC01;34RCrainbow/ECfile.dat
LC01;34RCrainbow/ECfile.txt
+LC01;34RCrainbow/EClower.gz
+LC01;34RCrainbow/EClower.tar
+LC01;34RCrainbow/EClower.tar.gz
+LC01;34RCrainbow/EClu.tar.GZ
LC01;34RCrainbow/ECmh1
LC01;34RCrainbow/ECmh2
-LC01;34RCrainbow/ECstar.gz
-LC01;34RCrainbow/ECstar.tar
-LC01;34RCrainbow/ECstar.tar.gz
+LC01;34RCrainbow/ECul.TAR.gz
+LC01;34RCrainbow/ECupper.GZ
+LC01;34RCrainbow/ECupper.TAR
+LC01;34RCrainbow/ECupper.TAR.GZ
+LC01;34RCrainbowEC
diff --git a/tests/bfs/color_rs_lc_rc_ec.sh b/tests/bfs/color_rs_lc_rc_ec.sh
new file mode 100644
index 0000000..467b2da
--- /dev/null
+++ b/tests/bfs/color_rs_lc_rc_ec.sh
@@ -0,0 +1 @@
+LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw0_ow.out b/tests/bfs/color_st0_tw0_ow.out
index 9a47ef2..a82762b 100644
--- a/tests/test_color_st0_tw0_ow.out
+++ b/tests/bfs/color_st0_tw0_ow.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/suid
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw0_ow.sh b/tests/bfs/color_st0_tw0_ow.sh
new file mode 100644
index 0000000..8e2b8e3
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_st0_tw0_ow0.out b/tests/bfs/color_st0_tw0_ow0.out
new file mode 100644
index 0000000..041f1d4
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow0.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sgid
+rainbow/pipe
+rainbow/sugid
+rainbow/suid
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_st0_tw0_ow0.sh b/tests/bfs/color_st0_tw0_ow0.sh
new file mode 100644
index 0000000..c5d5fe7
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw_ow.out b/tests/bfs/color_st0_tw_ow.out
index 42549a1..4dcb2f2 100644
--- a/tests/test_color_st0_tw_ow.out
+++ b/tests/bfs/color_st0_tw_ow.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw_ow.sh b/tests/bfs/color_st0_tw_ow.sh
new file mode 100644
index 0000000..8fd9605
--- /dev/null
+++ b/tests/bfs/color_st0_tw_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw_ow0.out b/tests/bfs/color_st0_tw_ow0.out
index 535b8ae..954ce9c 100644
--- a/tests/test_color_st0_tw_ow0.out
+++ b/tests/bfs/color_st0_tw_ow0.out
@@ -1,7 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +13,15 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw_ow0.sh b/tests/bfs/color_st0_tw_ow0.sh
new file mode 100644
index 0000000..68c63dc
--- /dev/null
+++ b/tests/bfs/color_st0_tw_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw0_ow.out b/tests/bfs/color_st_tw0_ow.out
index c9a86f4..a6e9a16 100644
--- a/tests/test_color_st_tw0_ow.out
+++ b/tests/bfs/color_st_tw0_ow.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_st_tw0_ow.sh b/tests/bfs/color_st_tw0_ow.sh
new file mode 100644
index 0000000..be16251
--- /dev/null
+++ b/tests/bfs/color_st_tw0_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw0_ow0.out b/tests/bfs/color_st_tw0_ow0.out
index 2d94f3a..756dafb 100644
--- a/tests/test_color_st_tw0_ow0.out
+++ b/tests/bfs/color_st_tw0_ow0.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
diff --git a/tests/bfs/color_st_tw0_ow0.sh b/tests/bfs/color_st_tw0_ow0.sh
new file mode 100644
index 0000000..f869e7c
--- /dev/null
+++ b/tests/bfs/color_st_tw0_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw_ow0.out b/tests/bfs/color_st_tw_ow0.out
index 317ef90..6e4a260 100644
--- a/tests/test_color_st_tw_ow0.out
+++ b/tests/bfs/color_st_tw_ow0.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
diff --git a/tests/bfs/color_st_tw_ow0.sh b/tests/bfs/color_st_tw_ow0.sh
new file mode 100644
index 0000000..99a17a6
--- /dev/null
+++ b/tests/bfs/color_st_tw_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_star.sh b/tests/bfs/color_star.sh
new file mode 100644
index 0000000..6d5312e
--- /dev/null
+++ b/tests/bfs/color_star.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on LS_COLORS="*"
+! LS_COLORS="*" invoke_bfs rainbow -color
diff --git a/tests/test_color_su0_sg.out b/tests/bfs/color_su0_sg.out
index 8b8c8b8..d13b6b6 100644
--- a/tests/test_color_su0_sg.out
+++ b/tests/bfs/color_su0_sg.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -12,9 +14,14 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su0_sg.sh b/tests/bfs/color_su0_sg.sh
new file mode 100644
index 0000000..f5f57b4
--- /dev/null
+++ b/tests/bfs/color_su0_sg.sh
@@ -0,0 +1 @@
+LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color
diff --git a/tests/test_color_su0_sg0.out b/tests/bfs/color_su0_sg0.out
index 0cd5f9a..77fba58 100644
--- a/tests/test_color_su0_sg0.out
+++ b/tests/bfs/color_su0_sg0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -10,11 +12,16 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su0_sg0.sh b/tests/bfs/color_su0_sg0.sh
new file mode 100644
index 0000000..0198383
--- /dev/null
+++ b/tests/bfs/color_su0_sg0.sh
@@ -0,0 +1 @@
+LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_su_sg0.out b/tests/bfs/color_su_sg0.out
index a9e8c5d..8fab046 100644
--- a/tests/test_color_su_sg0.out
+++ b/tests/bfs/color_su_sg0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -12,9 +14,14 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su_sg0.sh b/tests/bfs/color_su_sg0.sh
new file mode 100644
index 0000000..8dc6984
--- /dev/null
+++ b/tests/bfs/color_su_sg0.sh
@@ -0,0 +1 @@
+LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/comma_incomplete.sh b/tests/bfs/comma_incomplete.sh
new file mode 100644
index 0000000..bd60168
--- /dev/null
+++ b/tests/bfs/comma_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print ,
diff --git a/tests/bfs/data_flow_hidden.out b/tests/bfs/data_flow_hidden.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/data_flow_hidden.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/data_flow_hidden.sh b/tests/bfs/data_flow_hidden.sh
new file mode 100644
index 0000000..6afaab2
--- /dev/null
+++ b/tests/bfs/data_flow_hidden.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \)
diff --git a/tests/test_deep.out b/tests/bfs/deep_strict.out
index c385fce..c385fce 100644
--- a/tests/test_deep.out
+++ b/tests/bfs/deep_strict.out
diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh
new file mode 100644
index 0000000..22453c0
--- /dev/null
+++ b/tests/bfs/deep_strict.sh
@@ -0,0 +1,3 @@
+# Not even enough fds to keep the root open
+ulimit -n $((NOPENFD + 4))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/test_not_prune.out b/tests/bfs/exclude_depth.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_not_prune.out
+++ b/tests/bfs/exclude_depth.out
@@ -2,12 +2,12 @@ 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/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/bfs/exclude_depth.sh b/tests/bfs/exclude_depth.sh
new file mode 100644
index 0000000..437b4dd
--- /dev/null
+++ b/tests/bfs/exclude_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -exclude -name foo
diff --git a/tests/bfs/exclude_exclude.sh b/tests/bfs/exclude_exclude.sh
new file mode 100644
index 0000000..739342f
--- /dev/null
+++ b/tests/bfs/exclude_exclude.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -exclude -name foo
diff --git a/tests/test_L_lname.out b/tests/bfs/exclude_mindepth.out
index e69de29..e69de29 100644
--- a/tests/test_L_lname.out
+++ b/tests/bfs/exclude_mindepth.out
diff --git a/tests/bfs/exclude_mindepth.sh b/tests/bfs/exclude_mindepth.sh
new file mode 100644
index 0000000..c8f70f9
--- /dev/null
+++ b/tests/bfs/exclude_mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 3 -exclude -name foo
diff --git a/tests/test_prune_or_print.out b/tests/bfs/exclude_name.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_prune_or_print.out
+++ b/tests/bfs/exclude_name.out
@@ -2,12 +2,12 @@ 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/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/bfs/exclude_name.sh b/tests/bfs/exclude_name.sh
new file mode 100644
index 0000000..7cf9f33
--- /dev/null
+++ b/tests/bfs/exclude_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -exclude -name foo
diff --git a/tests/bfs/exclude_print.sh b/tests/bfs/exclude_print.sh
new file mode 100644
index 0000000..dc89e1d
--- /dev/null
+++ b/tests/bfs/exclude_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -print
diff --git a/tests/test_name_root.out b/tests/bfs/exec_flush_fprint.out
index 511198f..511198f 100644
--- a/tests/test_name_root.out
+++ b/tests/bfs/exec_flush_fprint.out
diff --git a/tests/bfs/exec_flush_fprint.sh b/tests/bfs/exec_flush_fprint.sh
new file mode 100644
index 0000000..a862773
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint.sh
@@ -0,0 +1,2 @@
+# Even non-stdstreams should be flushed
+bfs_diff basic/a -fprint "$OUT.f" -exec cat "$OUT.f" \;
diff --git a/tests/bfs/exec_flush_fprint_fail.sh b/tests/bfs/exec_flush_fprint_fail.sh
new file mode 100644
index 0000000..cd38e41
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint_fail.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic/a -fprint /dev/full -exec true \;
diff --git a/tests/test_execdir.out b/tests/bfs/execdir_path_relative_slash.out
index 62b31f6..62b31f6 100644
--- a/tests/test_execdir.out
+++ b/tests/bfs/execdir_path_relative_slash.out
diff --git a/tests/bfs/execdir_path_relative_slash.sh b/tests/bfs/execdir_path_relative_slash.sh
new file mode 100644
index 0000000..fb5a924
--- /dev/null
+++ b/tests/bfs/execdir_path_relative_slash.sh
@@ -0,0 +1 @@
+PATH="foo:$PATH" bfs_diff basic -execdir /bin/sh -c 'printf "%s\\n" "$@"' sh {} +
diff --git a/tests/test_execdir_plus.out b/tests/bfs/execdir_plus.out
index 5bbb758..8866a8f 100644
--- a/tests/test_execdir_plus.out
+++ b/tests/bfs/execdir_plus.out
@@ -1,3 +1,4 @@
+./a ./b ./c ./e ./g ./i ./j ./k ./l
./bar
./bar
./basic
@@ -8,4 +9,3 @@
./foo
./foo
./h
-./a ./b ./c ./e ./g ./i ./j ./k ./l
diff --git a/tests/bfs/execdir_plus.sh b/tests/bfs/execdir_plus.sh
new file mode 100644
index 0000000..6f24bdc
--- /dev/null
+++ b/tests/bfs/execdir_plus.sh
@@ -0,0 +1,4 @@
+tree=$(invoke_bfs -D tree 2>&1 -quit)
+[[ "$tree" == *"-S dfs"* ]] && skip
+
+bfs_diff -j1 basic -execdir "$TESTS/sort-args.sh" {} +
diff --git a/tests/bfs/execdir_plus_nonexistent.out b/tests/bfs/execdir_plus_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/execdir_plus_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/execdir_plus_nonexistent.sh b/tests/bfs/execdir_plus_nonexistent.sh
new file mode 100644
index 0000000..ed7ed56
--- /dev/null
+++ b/tests/bfs/execdir_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_H_type_l.out b/tests/bfs/expr_flag_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_H_type_l.out
+++ b/tests/bfs/expr_flag_path.out
diff --git a/tests/bfs/expr_flag_path.sh b/tests/bfs/expr_flag_path.sh
new file mode 100644
index 0000000..bb89d92
--- /dev/null
+++ b/tests/bfs/expr_flag_path.sh
@@ -0,0 +1 @@
+bfs_diff -type l -H links/skip
diff --git a/tests/test_expr_flag_path.out b/tests/bfs/expr_path_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_flag_path.out
+++ b/tests/bfs/expr_path_flag.out
diff --git a/tests/bfs/expr_path_flag.sh b/tests/bfs/expr_path_flag.sh
new file mode 100644
index 0000000..818e5d1
--- /dev/null
+++ b/tests/bfs/expr_path_flag.sh
@@ -0,0 +1 @@
+bfs_diff -type l links/skip -H
diff --git a/tests/bfs/files0_from_root.sh b/tests/bfs/files0_from_root.sh
new file mode 100644
index 0000000..6ba5f00
--- /dev/null
+++ b/tests/bfs/files0_from_root.sh
@@ -0,0 +1,2 @@
+printf 'basic\0' >"$TEST/input"
+! invoke_bfs basic -files0-from "$TEST/input"
diff --git a/tests/test_expr_path_flag.out b/tests/bfs/flag_expr_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_path_flag.out
+++ b/tests/bfs/flag_expr_path.out
diff --git a/tests/bfs/flag_expr_path.sh b/tests/bfs/flag_expr_path.sh
new file mode 100644
index 0000000..a414e10
--- /dev/null
+++ b/tests/bfs/flag_expr_path.sh
@@ -0,0 +1 @@
+bfs_diff -H -type l links/skip
diff --git a/tests/test_fprint_duplicate_stdout.out b/tests/bfs/fprint_duplicate_stdout.out
index 6c21751..6c21751 100644
--- a/tests/test_fprint_duplicate_stdout.out
+++ b/tests/bfs/fprint_duplicate_stdout.out
diff --git a/tests/bfs/fprint_duplicate_stdout.sh b/tests/bfs/fprint_duplicate_stdout.sh
new file mode 100644
index 0000000..4e95e30
--- /dev/null
+++ b/tests/bfs/fprint_duplicate_stdout.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT" -print >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/fprint_error_stderr.sh b/tests/bfs/fprint_error_stderr.sh
new file mode 100644
index 0000000..2cc4037
--- /dev/null
+++ b/tests/bfs/fprint_error_stderr.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full
diff --git a/tests/bfs/fprint_error_stdout.sh b/tests/bfs/fprint_error_stdout.sh
new file mode 100644
index 0000000..42a7b36
--- /dev/null
+++ b/tests/bfs/fprint_error_stdout.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full
diff --git a/tests/bfs/help.sh b/tests/bfs/help.sh
new file mode 100644
index 0000000..5029c7e
--- /dev/null
+++ b/tests/bfs/help.sh
@@ -0,0 +1,4 @@
+! invoke_bfs -help | grep -E '\{...?\}' || fail
+! invoke_bfs -D help | grep -E '\{...?\}' || fail
+! invoke_bfs -S help | grep -E '\{...?\}' || fail
+! invoke_bfs -regextype help | grep -E '\{...?\}' || fail
diff --git a/tests/test_hidden.out b/tests/bfs/hidden.out
index e65ede9..e65ede9 100644
--- a/tests/test_hidden.out
+++ b/tests/bfs/hidden.out
diff --git a/tests/bfs/hidden.sh b/tests/bfs/hidden.sh
new file mode 100644
index 0000000..b0413c5
--- /dev/null
+++ b/tests/bfs/hidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -hidden
diff --git a/tests/test_hidden_root.out b/tests/bfs/hidden_root.out
index 199ae5f..8c1371b 100644
--- a/tests/test_hidden_root.out
+++ b/tests/bfs/hidden_root.out
@@ -1,5 +1,5 @@
...
+.../../...
./...
./...
-.../../...
././...
diff --git a/tests/bfs/hidden_root.sh b/tests/bfs/hidden_root.sh
new file mode 100644
index 0000000..905c5b5
--- /dev/null
+++ b/tests/bfs/hidden_root.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . ./. ... ./... .../.. -hidden
diff --git a/tests/bfs/high_byte.sh b/tests/bfs/high_byte.sh
new file mode 100644
index 0000000..c76199f
--- /dev/null
+++ b/tests/bfs/high_byte.sh
@@ -0,0 +1 @@
+! invoke_bfs -$'\xFF'
diff --git a/tests/bfs/j0.sh b/tests/bfs/j0.sh
new file mode 100644
index 0000000..97a7c5c
--- /dev/null
+++ b/tests/bfs/j0.sh
@@ -0,0 +1 @@
+! invoke_bfs -j0 basic
diff --git a/tests/bfs/j1.out b/tests/bfs/j1.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/j1.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/j1.sh b/tests/bfs/j1.sh
new file mode 100644
index 0000000..972ac1b
--- /dev/null
+++ b/tests/bfs/j1.sh
@@ -0,0 +1 @@
+bfs_diff -j1 basic
diff --git a/tests/bfs/j64.out b/tests/bfs/j64.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/j64.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/j64.sh b/tests/bfs/j64.sh
new file mode 100644
index 0000000..c56788f
--- /dev/null
+++ b/tests/bfs/j64.sh
@@ -0,0 +1 @@
+bfs_diff -j64 basic
diff --git a/tests/bfs/j_negative.sh b/tests/bfs/j_negative.sh
new file mode 100644
index 0000000..809c98c
--- /dev/null
+++ b/tests/bfs/j_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs -j-1 basic
diff --git a/tests/bfs/limit.out b/tests/bfs/limit.out
new file mode 100644
index 0000000..ea94276
--- /dev/null
+++ b/tests/bfs/limit.out
@@ -0,0 +1,4 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
diff --git a/tests/bfs/limit.sh b/tests/bfs/limit.sh
new file mode 100644
index 0000000..84b605f
--- /dev/null
+++ b/tests/bfs/limit.sh
@@ -0,0 +1 @@
+bfs_diff -s basic -type f -print -limit 4
diff --git a/tests/bfs/limit_0.sh b/tests/bfs/limit_0.sh
new file mode 100644
index 0000000..3ce26de
--- /dev/null
+++ b/tests/bfs/limit_0.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit 0
diff --git a/tests/bfs/limit_implicit_print.sh b/tests/bfs/limit_implicit_print.sh
new file mode 100644
index 0000000..cdb059d
--- /dev/null
+++ b/tests/bfs/limit_implicit_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -type f -limit 1
diff --git a/tests/bfs/limit_incomplete.sh b/tests/bfs/limit_incomplete.sh
new file mode 100644
index 0000000..2d1e842
--- /dev/null
+++ b/tests/bfs/limit_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit
diff --git a/tests/bfs/limit_one.sh b/tests/bfs/limit_one.sh
new file mode 100644
index 0000000..3f8181c
--- /dev/null
+++ b/tests/bfs/limit_one.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit one
diff --git a/tests/bfs/links_empty.sh b/tests/bfs/links_empty.sh
new file mode 100644
index 0000000..42cf6e5
--- /dev/null
+++ b/tests/bfs/links_empty.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ''
diff --git a/tests/bfs/links_invalid.sh b/tests/bfs/links_invalid.sh
new file mode 100644
index 0000000..4d139c9
--- /dev/null
+++ b/tests/bfs/links_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ASDF
diff --git a/tests/bfs/links_leading_space.sh b/tests/bfs/links_leading_space.sh
new file mode 100644
index 0000000..15957af
--- /dev/null
+++ b/tests/bfs/links_leading_space.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ' 1'
diff --git a/tests/bfs/links_negative.sh b/tests/bfs/links_negative.sh
new file mode 100644
index 0000000..e664b99
--- /dev/null
+++ b/tests/bfs/links_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links +-1
diff --git a/tests/bfs/links_noarg.sh b/tests/bfs/links_noarg.sh
new file mode 100644
index 0000000..5c948dc
--- /dev/null
+++ b/tests/bfs/links_noarg.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links
diff --git a/tests/bfs/newerma_nonexistent.sh b/tests/bfs/newerma_nonexistent.sh
new file mode 100644
index 0000000..cdedb4a
--- /dev/null
+++ b/tests/bfs/newerma_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerma basic/nonexistent
diff --git a/tests/bfs/newermq.sh b/tests/bfs/newermq.sh
new file mode 100644
index 0000000..2f705dc
--- /dev/null
+++ b/tests/bfs/newermq.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermq times/a
diff --git a/tests/bfs/newermt_invalid.sh b/tests/bfs/newermt_invalid.sh
new file mode 100644
index 0000000..98efece
--- /dev/null
+++ b/tests/bfs/newermt_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermt not_a_date_time
diff --git a/tests/bfs/newerqm.sh b/tests/bfs/newerqm.sh
new file mode 100644
index 0000000..c0cff98
--- /dev/null
+++ b/tests/bfs/newerqm.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerqm times/a
diff --git a/tests/bfs/nocolor.out b/tests/bfs/nocolor.out
new file mode 100644
index 0000000..d51d24d
--- /dev/null
+++ b/tests/bfs/nocolor.out
@@ -0,0 +1,27 @@
+rainbow
+rainbow/
+rainbow//
+rainbow/broken
+rainbow/chardev_link
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/pipe
+rainbow/sgid
+rainbow/socket
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor.sh b/tests/bfs/nocolor.sh
new file mode 100644
index 0000000..8dace0b
--- /dev/null
+++ b/tests/bfs/nocolor.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -nocolor
diff --git a/tests/bfs/nocolor_env.out b/tests/bfs/nocolor_env.out
new file mode 100644
index 0000000..d51d24d
--- /dev/null
+++ b/tests/bfs/nocolor_env.out
@@ -0,0 +1,27 @@
+rainbow
+rainbow/
+rainbow//
+rainbow/broken
+rainbow/chardev_link
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/pipe
+rainbow/sgid
+rainbow/socket
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor_env.sh b/tests/bfs/nocolor_env.sh
new file mode 100644
index 0000000..d1c2afb
--- /dev/null
+++ b/tests/bfs/nocolor_env.sh
@@ -0,0 +1,3 @@
+NO_COLOR=1 bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/nocolor_env_empty.out b/tests/bfs/nocolor_env_empty.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/nocolor_env_empty.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor_env_empty.sh b/tests/bfs/nocolor_env_empty.sh
new file mode 100644
index 0000000..1edfb1d
--- /dev/null
+++ b/tests/bfs/nocolor_env_empty.sh
@@ -0,0 +1,3 @@
+NO_COLOR= bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/noerror.out b/tests/bfs/noerror.out
new file mode 100644
index 0000000..c4f8ce4
--- /dev/null
+++ b/tests/bfs/noerror.out
@@ -0,0 +1,4 @@
+inaccessible
+inaccessible/dir
+inaccessible/file
+inaccessible/link
diff --git a/tests/bfs/noerror.sh b/tests/bfs/noerror.sh
new file mode 100644
index 0000000..e334f8f
--- /dev/null
+++ b/tests/bfs/noerror.sh
@@ -0,0 +1 @@
+bfs_diff inaccessible -noerror
diff --git a/tests/bfs/noerror_nowarn.sh b/tests/bfs/noerror_nowarn.sh
new file mode 100644
index 0000000..26e7e68
--- /dev/null
+++ b/tests/bfs/noerror_nowarn.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs inaccessible -noerror -nowarn 2>&1 >/dev/null)
+[ -z "$stderr" ]
diff --git a/tests/bfs/noerror_warn.sh b/tests/bfs/noerror_warn.sh
new file mode 100644
index 0000000..ec85f4c
--- /dev/null
+++ b/tests/bfs/noerror_warn.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs inaccessible -noerror -warn 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/test_nohidden.out b/tests/bfs/nohidden.out
index d2a9690..84e6bd2 100644
--- a/tests/test_nohidden.out
+++ b/tests/bfs/nohidden.out
@@ -1,19 +1,29 @@
+
+/n
weirdnames
+weirdnames/
+weirdnames/
weirdnames/
+weirdnames/ /j
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/\
-weirdnames/ /j
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
weirdnames/\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bfs/nohidden.sh b/tests/bfs/nohidden.sh
new file mode 100644
index 0000000..e3a3e4a
--- /dev/null
+++ b/tests/bfs/nohidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -nohidden
diff --git a/tests/test_nohidden_depth.out b/tests/bfs/nohidden_depth.out
index d2a9690..84e6bd2 100644
--- a/tests/test_nohidden_depth.out
+++ b/tests/bfs/nohidden_depth.out
@@ -1,19 +1,29 @@
+
+/n
weirdnames
+weirdnames/
+weirdnames/
weirdnames/
+weirdnames/ /j
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/\
-weirdnames/ /j
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
weirdnames/\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bfs/nohidden_depth.sh b/tests/bfs/nohidden_depth.sh
new file mode 100644
index 0000000..9fd7017
--- /dev/null
+++ b/tests/bfs/nohidden_depth.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -depth -nohidden
diff --git a/tests/bfs/nowarn.sh b/tests/bfs/nowarn.sh
new file mode 100644
index 0000000..d9f9ab3
--- /dev/null
+++ b/tests/bfs/nowarn.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null)
+[ -z "$stderr" ]
diff --git a/tests/test_ok_plus_semicolon.out b/tests/bfs/ok_plus_semicolon.out
index 85d27e5..2a3e14f 100644
--- a/tests/test_ok_plus_semicolon.out
+++ b/tests/bfs/ok_plus_semicolon.out
@@ -2,18 +2,18 @@ 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/k +
-basic/l +
-basic/c/d +
-basic/e/f +
-basic/g/h +
basic/j/foo +
+basic/k +
basic/k/foo +
-basic/l/foo +
basic/k/foo/bar +
+basic/l +
+basic/l/foo +
basic/l/foo/bar +
basic/l/foo/bar/baz +
diff --git a/tests/bfs/ok_plus_semicolon.sh b/tests/bfs/ok_plus_semicolon.sh
new file mode 100644
index 0000000..57d6103
--- /dev/null
+++ b/tests/bfs/ok_plus_semicolon.sh
@@ -0,0 +1,8 @@
+# The -ok primary shall be equivalent to -exec, except that the use of a
+# <plus-sign> to punctuate the end of the primary expression need not be
+# supported, ...
+#
+# bfs chooses not to support it, for compatibility with most other find
+# implementations.
+
+yes | bfs_diff basic -ok echo {} + \;
diff --git a/tests/test_okdir_plus_semicolon.out b/tests/bfs/okdir_plus_semicolon.out
index 1909d27..1909d27 100644
--- a/tests/test_okdir_plus_semicolon.out
+++ b/tests/bfs/okdir_plus_semicolon.out
diff --git a/tests/bfs/okdir_plus_semicolon.sh b/tests/bfs/okdir_plus_semicolon.sh
new file mode 100644
index 0000000..d316bd7
--- /dev/null
+++ b/tests/bfs/okdir_plus_semicolon.sh
@@ -0,0 +1 @@
+yes | bfs_diff basic -okdir echo {} + \;
diff --git a/tests/bfs/or_incomplete.sh b/tests/bfs/or_incomplete.sh
new file mode 100644
index 0000000..4af31b6
--- /dev/null
+++ b/tests/bfs/or_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -o
diff --git a/tests/test_flag_expr_path.out b/tests/bfs/path_expr_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_flag_expr_path.out
+++ b/tests/bfs/path_expr_flag.out
diff --git a/tests/bfs/path_expr_flag.sh b/tests/bfs/path_expr_flag.sh
new file mode 100644
index 0000000..7cfa1cd
--- /dev/null
+++ b/tests/bfs/path_expr_flag.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l -H
diff --git a/tests/test_path_expr_flag.out b/tests/bfs/path_flag_expr.out
index e67f10b..e67f10b 100644
--- a/tests/test_path_expr_flag.out
+++ b/tests/bfs/path_flag_expr.out
diff --git a/tests/bfs/path_flag_expr.sh b/tests/bfs/path_flag_expr.sh
new file mode 100644
index 0000000..ca00c8c
--- /dev/null
+++ b/tests/bfs/path_flag_expr.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -H -type l
diff --git a/tests/bfs/perm_leading_plus_symbolic.out b/tests/bfs/perm_leading_plus_symbolic.out
new file mode 100644
index 0000000..09bc88f
--- /dev/null
+++ b/tests/bfs/perm_leading_plus_symbolic.out
@@ -0,0 +1,3 @@
+perms
+perms/drwxr-xr-x
+perms/frwxr-xr-x
diff --git a/tests/bfs/perm_leading_plus_symbolic.sh b/tests/bfs/perm_leading_plus_symbolic.sh
new file mode 100644
index 0000000..4202ac1
--- /dev/null
+++ b/tests/bfs/perm_leading_plus_symbolic.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +rwx
diff --git a/tests/bfs/perm_symbolic_double_comma.sh b/tests/bfs/perm_symbolic_double_comma.sh
new file mode 100644
index 0000000..48f9d4b
--- /dev/null
+++ b/tests/bfs/perm_symbolic_double_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,,u+w
diff --git a/tests/bfs/perm_symbolic_missing_action.sh b/tests/bfs/perm_symbolic_missing_action.sh
new file mode 100644
index 0000000..28446ab
--- /dev/null
+++ b/tests/bfs/perm_symbolic_missing_action.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a
diff --git a/tests/bfs/perm_symbolic_trailing_comma.sh b/tests/bfs/perm_symbolic_trailing_comma.sh
new file mode 100644
index 0000000..01bbc16
--- /dev/null
+++ b/tests/bfs/perm_symbolic_trailing_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,
diff --git a/tests/bfs/printf_color.out b/tests/bfs/printf_color.out
new file mode 100644
index 0000000..77d21c3
--- /dev/null
+++ b/tests/bfs/printf_color.out
@@ -0,0 +1,28 @@
+. $'./rainbow/\e[1m' $'\e[0m' $'./rainbow/\e[1m/'$'\e[0m' $'rainbow/\e[1m/'$'\e[0m'
+. . . .
+. . rainbow ./rainbow rainbow
+. ./rainbow exec.sh ./rainbow/exec.sh rainbow/exec.sh
+. ./rainbow $'\e[1m' $'./rainbow/\e[1m' $'rainbow/\e[1m'
+. ./rainbow socket ./rainbow/socket rainbow/socket
+. ./rainbow broken ./rainbow/broken rainbow/broken nowhere
+. ./rainbow chardev_link ./rainbow/chardev_link rainbow/chardev_link /dev/null
+. ./rainbow link.txt ./rainbow/link.txt rainbow/link.txt file.txt
+. ./rainbow sticky_ow ./rainbow/sticky_ow rainbow/sticky_ow
+. ./rainbow sgid ./rainbow/sgid rainbow/sgid
+. ./rainbow pipe ./rainbow/pipe rainbow/pipe
+. ./rainbow ow ./rainbow/ow rainbow/ow
+. ./rainbow sugid ./rainbow/sugid rainbow/sugid
+. ./rainbow suid ./rainbow/suid rainbow/suid
+. ./rainbow sticky ./rainbow/sticky rainbow/sticky
+. ./rainbow file.dat ./rainbow/file.dat rainbow/file.dat
+. ./rainbow file.txt ./rainbow/file.txt rainbow/file.txt
+. ./rainbow lower.gz ./rainbow/lower.gz rainbow/lower.gz
+. ./rainbow lower.tar ./rainbow/lower.tar rainbow/lower.tar
+. ./rainbow lower.tar.gz ./rainbow/lower.tar.gz rainbow/lower.tar.gz
+. ./rainbow lu.tar.GZ ./rainbow/lu.tar.GZ rainbow/lu.tar.GZ
+. ./rainbow mh1 ./rainbow/mh1 rainbow/mh1
+. ./rainbow mh2 ./rainbow/mh2 rainbow/mh2
+. ./rainbow ul.TAR.gz ./rainbow/ul.TAR.gz rainbow/ul.TAR.gz
+. ./rainbow upper.GZ ./rainbow/upper.GZ rainbow/upper.GZ
+. ./rainbow upper.TAR ./rainbow/upper.TAR rainbow/upper.TAR
+. ./rainbow upper.TAR.GZ ./rainbow/upper.TAR.GZ rainbow/upper.TAR.GZ
diff --git a/tests/bfs/printf_color.sh b/tests/bfs/printf_color.sh
new file mode 100644
index 0000000..3641ddb
--- /dev/null
+++ b/tests/bfs/printf_color.sh
@@ -0,0 +1 @@
+bfs_diff -color -exclude \( -depth 1 -not -name rainbow \) -printf '%H %h %f %p %P %l\n'
diff --git a/tests/bfs/printf_duplicate_flag.sh b/tests/bfs/printf_duplicate_flag.sh
new file mode 100644
index 0000000..5ff29f1
--- /dev/null
+++ b/tests/bfs/printf_duplicate_flag.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%--p'
diff --git a/tests/bfs/printf_everything.sh b/tests/bfs/printf_everything.sh
new file mode 100644
index 0000000..07d574a
--- /dev/null
+++ b/tests/bfs/printf_everything.sh
@@ -0,0 +1,15 @@
+everything=(%{a,b,c,d,D,f,g,G,h,H,i,k,l,m,M,n,p,P,s,S,t,u,U,y,Y})
+
+# Check if we have fstypes
+if invoke_bfs basic -printf '%F' -quit >/dev/null; then
+ everything+=(%F)
+fi
+
+everything+=(%{A,C,T}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
+
+# Check if we have birth times
+if invoke_bfs basic -printf '%w' -quit >/dev/null; then
+ everything+=(%w %{B,W}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
+fi
+
+invoke_bfs rainbow -printf "${everything[*]}\n" >/dev/null
diff --git a/tests/bfs/printf_incomplete_escape.sh b/tests/bfs/printf_incomplete_escape.sh
new file mode 100644
index 0000000..f560d28
--- /dev/null
+++ b/tests/bfs/printf_incomplete_escape.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '\'
diff --git a/tests/bfs/printf_incomplete_format.sh b/tests/bfs/printf_incomplete_format.sh
new file mode 100644
index 0000000..92c6afc
--- /dev/null
+++ b/tests/bfs/printf_incomplete_format.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%'
diff --git a/tests/bfs/printf_invalid_escape.sh b/tests/bfs/printf_invalid_escape.sh
new file mode 100644
index 0000000..4338f9b
--- /dev/null
+++ b/tests/bfs/printf_invalid_escape.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '\!'
diff --git a/tests/bfs/printf_invalid_flag.sh b/tests/bfs/printf_invalid_flag.sh
new file mode 100644
index 0000000..70dfe97
--- /dev/null
+++ b/tests/bfs/printf_invalid_flag.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '% p'
diff --git a/tests/bfs/printf_invalid_format.sh b/tests/bfs/printf_invalid_format.sh
new file mode 100644
index 0000000..59d63a7
--- /dev/null
+++ b/tests/bfs/printf_invalid_format.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%!'
diff --git a/tests/bfs/printf_must_be_numeric.sh b/tests/bfs/printf_must_be_numeric.sh
new file mode 100644
index 0000000..7c7c3fa
--- /dev/null
+++ b/tests/bfs/printf_must_be_numeric.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%+p'
diff --git a/tests/test_and_purity.out b/tests/bfs/printf_w.out
index e69de29..e69de29 100644
--- a/tests/test_and_purity.out
+++ b/tests/bfs/printf_w.out
diff --git a/tests/bfs/printf_w.sh b/tests/bfs/printf_w.sh
new file mode 100644
index 0000000..3b27ee7
--- /dev/null
+++ b/tests/bfs/printf_w.sh
@@ -0,0 +1,2 @@
+# Birth times may not be supported, so just check that %w/%W/%B can be parsed
+bfs_diff times -false -printf '%w %WY %BY\n'
diff --git a/tests/bfs/status.sh b/tests/bfs/status.sh
new file mode 100644
index 0000000..83e12d3
--- /dev/null
+++ b/tests/bfs/status.sh
@@ -0,0 +1 @@
+bfs_pty basic -status -print -depth 0 -exec stty cols 123 rows 14 \; >"$OUT"
diff --git a/tests/bfs/stderr_fails_loudly.sh b/tests/bfs/stderr_fails_loudly.sh
new file mode 100644
index 0000000..8572d5a
--- /dev/null
+++ b/tests/bfs/stderr_fails_loudly.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full
diff --git a/tests/bfs/stderr_fails_silently.out b/tests/bfs/stderr_fails_silently.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/stderr_fails_silently.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/stderr_fails_silently.sh b/tests/bfs/stderr_fails_silently.sh
new file mode 100644
index 0000000..a37393d
--- /dev/null
+++ b/tests/bfs/stderr_fails_silently.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+bfs_diff -D all basic 2>/dev/full
diff --git a/tests/test_type_multi.out b/tests/bfs/type_multi.out
index d22ed9f..3cae08a 100644
--- a/tests/test_type_multi.out
+++ b/tests/bfs/type_multi.out
@@ -1,7 +1,7 @@
links
links/deeply
-links/file
-links/hardlink
links/deeply/nested
links/deeply/nested/dir
links/deeply/nested/file
+links/file
+links/hardlink
diff --git a/tests/bfs/type_multi.sh b/tests/bfs/type_multi.sh
new file mode 100644
index 0000000..59992c7
--- /dev/null
+++ b/tests/bfs/type_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -type f,d,c
diff --git a/tests/bfs/typo.sh b/tests/bfs/typo.sh
new file mode 100644
index 0000000..459e9fe
--- /dev/null
+++ b/tests/bfs/typo.sh
@@ -0,0 +1 @@
+invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null
diff --git a/tests/bfs/unexpected_operator.sh b/tests/bfs/unexpected_operator.sh
new file mode 100644
index 0000000..2eb0e71
--- /dev/null
+++ b/tests/bfs/unexpected_operator.sh
@@ -0,0 +1 @@
+! invoke_bfs \! -o -print
diff --git a/tests/test_unique.out b/tests/bfs/unique.out
index 289cbde..289cbde 100644
--- a/tests/test_unique.out
+++ b/tests/bfs/unique.out
diff --git a/tests/bfs/unique.sh b/tests/bfs/unique.sh
new file mode 100644
index 0000000..ea8adfd
--- /dev/null
+++ b/tests/bfs/unique.sh
@@ -0,0 +1 @@
+bfs_diff links/{file,symlink,hardlink} -unique
diff --git a/tests/bfs/unique_depth.out b/tests/bfs/unique_depth.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/unique_depth.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/unique_depth.sh b/tests/bfs/unique_depth.sh
new file mode 100644
index 0000000..c1d9716
--- /dev/null
+++ b/tests/bfs/unique_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -unique -depth
diff --git a/tests/bfs/version.sh b/tests/bfs/version.sh
new file mode 100644
index 0000000..e0417ca
--- /dev/null
+++ b/tests/bfs/version.sh
@@ -0,0 +1 @@
+invoke_bfs -version >/dev/null
diff --git a/tests/bfs/warn_O9.out b/tests/bfs/warn_O9.out
new file mode 100644
index 0000000..336a6e8
--- /dev/null
+++ b/tests/bfs/warn_O9.out
@@ -0,0 +1,19 @@
+.
+./a
+./b
+./c
+./c/d
+./e
+./e/f
+./g
+./g/h
+./i
+./j
+./j/foo
+./k
+./k/foo
+./k/foo/bar
+./l
+./l/foo
+./l/foo/bar
+./l/foo/bar/baz
diff --git a/tests/bfs/warn_O9.sh b/tests/bfs/warn_O9.sh
new file mode 100644
index 0000000..821789f
--- /dev/null
+++ b/tests/bfs/warn_O9.sh
@@ -0,0 +1,3 @@
+# Regression test: don't crash when warning if -O9 is the last argument
+cd basic
+bfs_diff -warn -O9
diff --git a/tests/bfs/warn_depth_prune.sh b/tests/bfs/warn_depth_prune.sh
new file mode 100644
index 0000000..0f613c8
--- /dev/null
+++ b/tests/bfs/warn_depth_prune.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/bfs/warn_exclude_path.sh b/tests/bfs/warn_exclude_path.sh
new file mode 100644
index 0000000..988544e
--- /dev/null
+++ b/tests/bfs/warn_exclude_path.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs -warn -exclude basic -name '*f*' 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/bfs/warn_without_noerror.sh b/tests/bfs/warn_without_noerror.sh
new file mode 100644
index 0000000..5167309
--- /dev/null
+++ b/tests/bfs/warn_without_noerror.sh
@@ -0,0 +1,2 @@
+# bfs shouldn't print "warning: Suppressed errors" without -noerror
+! invoke_bfs inaccessible -warn 2>&1 >/dev/null | grep warning >&2
diff --git a/tests/bfs/warn_xdev_mount.out b/tests/bfs/warn_xdev_mount.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/warn_xdev_mount.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/warn_xdev_mount.sh b/tests/bfs/warn_xdev_mount.sh
new file mode 100644
index 0000000..5d395f6
--- /dev/null
+++ b/tests/bfs/warn_xdev_mount.sh
@@ -0,0 +1,2 @@
+# Regression test: don't crash if -mount is the last option
+bfs_diff basic -warn -xdev -mount
diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh
new file mode 100644
index 0000000..4683764
--- /dev/null
+++ b/tests/bfs/xtype_depth.sh
@@ -0,0 +1,2 @@
+# Make sure -xtype is considered side-effecting for facts_when_impure
+! invoke_bfs inaccessible/link -xtype l -depth 100
diff --git a/tests/test_xtype_multi.out b/tests/bfs/xtype_multi.out
index 171c580..558e89c 100644
--- a/tests/test_xtype_multi.out
+++ b/tests/bfs/xtype_multi.out
@@ -1,10 +1,10 @@
links
links/deeply
-links/file
-links/hardlink
-links/skip
-links/symlink
links/deeply/nested
links/deeply/nested/dir
links/deeply/nested/file
links/deeply/nested/link
+links/file
+links/hardlink
+links/skip
+links/symlink
diff --git a/tests/bfs/xtype_multi.sh b/tests/bfs/xtype_multi.sh
new file mode 100644
index 0000000..ed20955
--- /dev/null
+++ b/tests/bfs/xtype_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f,d,c
diff --git a/tests/test_data_flow_type.out b/tests/bfs/xtype_reorder.out
index e69de29..e69de29 100644
--- a/tests/test_data_flow_type.out
+++ b/tests/bfs/xtype_reorder.out
diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh
new file mode 100644
index 0000000..c1d94f3
--- /dev/null
+++ b/tests/bfs/xtype_reorder.sh
@@ -0,0 +1,3 @@
+# Make sure -xtype is not reordered in front of anything -- if -xtype runs
+# before -links 100, it will report an ELOOP error
+bfs_diff inaccessible/link -links 100 -xtype l
diff --git a/tests/bfstd.c b/tests/bfstd.c
new file mode 100644
index 0000000..6e15e2b
--- /dev/null
+++ b/tests/bfstd.c
@@ -0,0 +1,212 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "bfstd.h"
+#include "diag.h"
+
+#include <errno.h>
+#include <langinfo.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+/** asciilen() test cases. */
+static void check_asciilen(void) {
+ bfs_check(asciilen("") == 0);
+ bfs_check(asciilen("@") == 1);
+ bfs_check(asciilen("@@") == 2);
+ bfs_check(asciilen("\xFF@") == 0);
+ bfs_check(asciilen("@\xFF") == 1);
+ bfs_check(asciilen("@@@@@@@@") == 8);
+ bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16);
+ bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24);
+ bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15);
+ bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16);
+ bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17);
+ bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7);
+ bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8);
+ bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9);
+}
+
+/** Check the result of xdirname()/xbasename(). */
+static void check_base_dir(const char *path, const char *dir, const char *base) {
+ char *xdir = xdirname(path);
+ bfs_everify(xdir, "xdirname()");
+ bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
+ free(xdir);
+
+ char *xbase = xbasename(path);
+ bfs_everify(xbase, "xbasename()");
+ bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
+ free(xbase);
+}
+
+/** xdirname()/xbasename() test cases. */
+static void check_basedirs(void) {
+ // From man 3p basename
+ check_base_dir("usr", ".", "usr");
+ check_base_dir("usr/", ".", "usr");
+ check_base_dir("", ".", ".");
+ check_base_dir("/", "/", "/");
+ // check_base_dir("//", "/" or "//", "/" or "//");
+ check_base_dir("///", "/", "/");
+ check_base_dir("/usr/", "/", "usr");
+ check_base_dir("/usr/lib", "/usr", "lib");
+ check_base_dir("//usr//lib//", "//usr", "lib");
+ check_base_dir("/home//dwc//test", "/home//dwc", "test");
+}
+
+/** Check the result of wordesc(). */
+static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) {
+ char buf[256];
+ char *end = buf + sizeof(buf);
+ char *esc = wordesc(buf, end, str, flags);
+
+ if (bfs_check(esc != end)) {
+ bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp);
+ }
+}
+
+/** wordesc() test cases. */
+static void check_wordescs(void) {
+ check_wordesc("", "\"\"", WESC_SHELL);
+ check_wordesc("word", "word", WESC_SHELL);
+ check_wordesc("two words", "\"two words\"", WESC_SHELL);
+ check_wordesc("word's", "\"word's\"", WESC_SHELL);
+ check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL);
+ check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL);
+ check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY);
+ check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY);
+ check_wordesc("~user", "\"~user\"", WESC_SHELL);
+
+ const char *charmap = nl_langinfo(CODESET);
+ if (strcmp(charmap, "UTF-8") == 0) {
+ check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY);
+ check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY);
+ check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY);
+ check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY);
+ check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL);
+ }
+}
+
+/** xstrto*() test cases. */
+static void check_strtox(void) {
+ short s;
+ unsigned short us;
+ int i;
+ unsigned int ui;
+ long l;
+ unsigned long ul;
+ long long ll;
+ unsigned long long ull;
+ char *end;
+
+#define check_strtouerr(err, str, end, base) \
+ do { \
+ bfs_echeck(xstrtous(str, end, base, &us) != 0 && errno == err); \
+ bfs_echeck(xstrtoui(str, end, base, &ui) != 0 && errno == err); \
+ bfs_echeck(xstrtoul(str, end, base, &ul) != 0 && errno == err); \
+ bfs_echeck(xstrtoull(str, end, base, &ull) != 0 && errno == err); \
+ } while (0)
+
+ check_strtouerr(ERANGE, "-1", NULL, 0);
+ check_strtouerr(ERANGE, "-0x1", NULL, 0);
+
+ check_strtouerr(EINVAL, "-", NULL, 0);
+ check_strtouerr(EINVAL, "-q", NULL, 0);
+ check_strtouerr(EINVAL, "-1q", NULL, 0);
+ check_strtouerr(EINVAL, "-0x", NULL, 0);
+
+#define check_strtoerr(err, str, end, base) \
+ do { \
+ bfs_echeck(xstrtos(str, end, base, &s) != 0 && errno == err); \
+ bfs_echeck(xstrtoi(str, end, base, &i) != 0 && errno == err); \
+ bfs_echeck(xstrtol(str, end, base, &l) != 0 && errno == err); \
+ bfs_echeck(xstrtoll(str, end, base, &ll) != 0 && errno == err); \
+ check_strtouerr(err, str, end, base); \
+ } while (0)
+
+ check_strtoerr(EINVAL, "", NULL, 0);
+ check_strtoerr(EINVAL, "", &end, 0);
+ check_strtoerr(EINVAL, " 1 ", &end, 0);
+ check_strtoerr(EINVAL, " -1", NULL, 0);
+ check_strtoerr(EINVAL, " 123", NULL, 0);
+ check_strtoerr(EINVAL, "123 ", NULL, 0);
+ check_strtoerr(EINVAL, "0789", NULL, 0);
+ check_strtoerr(EINVAL, "789A", NULL, 0);
+ check_strtoerr(EINVAL, "0x", NULL, 0);
+ check_strtoerr(EINVAL, "0x789A", NULL, 10);
+ check_strtoerr(EINVAL, "0x-1", NULL, 0);
+
+#define check_strtotype(type, min, max, fmt, fn, str, base, v, n) \
+ do { \
+ if ((n) >= min && (n) <= max) { \
+ bfs_echeck(fn(str, NULL, base, &v) == 0); \
+ bfs_check(v == (type)(n), "%s('%s') == " fmt " (!= " fmt ")", #fn, str, v, (type)(n)); \
+ } else { \
+ bfs_echeck(fn(str, NULL, base, &v) != 0 && errno == ERANGE); \
+ } \
+ } while (0)
+
+#define check_strtoint(str, base, n) \
+ do { \
+ check_strtotype( signed short, SHRT_MIN, SHRT_MAX, "%d", xstrtos, str, base, s, n); \
+ check_strtotype( signed int, INT_MIN, INT_MAX, "%d", xstrtoi, str, base, i, n); \
+ check_strtotype( signed long, LONG_MIN, LONG_MAX, "%ld", xstrtol, str, base, l, n); \
+ check_strtotype( signed long long, LLONG_MIN, LLONG_MAX, "%lld", xstrtoll, str, base, ll, n); \
+ check_strtotype(unsigned short, 0, USHRT_MAX, "%u", xstrtous, str, base, us, n); \
+ check_strtotype(unsigned int, 0, UINT_MAX, "%u", xstrtoui, str, base, ui, n); \
+ check_strtotype(unsigned long, 0, ULONG_MAX, "%lu", xstrtoul, str, base, ul, n); \
+ check_strtotype(unsigned long long, 0, ULLONG_MAX, "%llu", xstrtoull, str, base, ull, n); \
+ } while (0)
+
+ check_strtoint("123", 0, 123);
+ check_strtoint("+123", 0, 123);
+ check_strtoint("-123", 0, -123);
+
+ check_strtoint("0123", 0, 0123);
+ check_strtoint("0x789A", 0, 0x789A);
+
+ check_strtoint("0123", 10, 123);
+ check_strtoint("0789", 10, 789);
+
+ check_strtoint("123", 16, 0x123);
+
+ check_strtoint("0x7FFF", 0, 0x7FFF);
+ check_strtoint("-0x8000", 0, -0x8000);
+
+ check_strtoint("0x7FFFFFFF", 0, 0x7FFFFFFFL);
+ check_strtoint("-0x80000000", 0, -0x7FFFFFFFL - 1);
+
+ check_strtoint("0x7FFFFFFFFFFFFFFF", 0, 0x7FFFFFFFFFFFFFFFLL);
+ check_strtoint("-0x8000000000000000", 0, -0x7FFFFFFFFFFFFFFFLL - 1);
+
+#define check_strtoend(str, estr, base, n) \
+ do { \
+ bfs_echeck(xstrtoll(str, &end, base, &ll) == 0); \
+ bfs_check(ll == (n), "xstrtoll('%s') == %lld (!= %lld)", str, ll, (long long)(n)); \
+ bfs_check(strcmp(end, estr) == 0, "xstrtoll('%s'): end == '%s' (!= '%s')", str, end, estr); \
+ } while (0)
+
+ check_strtoend("123 ", " ", 0, 123);
+ check_strtoend("0789", "89", 0, 07);
+ check_strtoend("789A", "A", 0, 789);
+ check_strtoend("0xDEFG", "G", 0, 0xDEF);
+}
+
+/** xstrwidth() test cases. */
+static void check_strwidth(void) {
+ bfs_check(xstrwidth("Hello world") == 11);
+ bfs_check(xstrwidth("Hello\1world") == 10);
+}
+
+void check_bfstd(void) {
+ check_asciilen();
+ check_basedirs();
+ check_wordescs();
+ check_strtox();
+ check_strwidth();
+}
diff --git a/tests/bit.c b/tests/bit.c
new file mode 100644
index 0000000..09d470b
--- /dev/null
+++ b/tests/bit.c
@@ -0,0 +1,160 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "bfs.h"
+#include "bit.h"
+
+#include <limits.h>
+#include <stdint.h>
+#include <string.h>
+
+// Polyfill C23's one-argument static_assert()
+#if __STDC_VERSION__ < C23
+# undef static_assert
+# define static_assert(...) _Static_assert(__VA_ARGS__, #__VA_ARGS__)
+#endif
+
+static_assert(UMAX_WIDTH(0x1) == 1);
+static_assert(UMAX_WIDTH(0x3) == 2);
+static_assert(UMAX_WIDTH(0x7) == 3);
+static_assert(UMAX_WIDTH(0xF) == 4);
+static_assert(UMAX_WIDTH(0xFF) == 8);
+static_assert(UMAX_WIDTH(0xFFF) == 12);
+static_assert(UMAX_WIDTH(0xFFFF) == 16);
+
+#define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1)
+#define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1)
+#define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1)
+
+static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH));
+static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH));
+static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH));
+
+static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH));
+static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH));
+static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH));
+
+static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH));
+static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH));
+static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH));
+
+static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH));
+static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH));
+static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH));
+
+static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH));
+static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH));
+static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH));
+
+static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH));
+static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH));
+static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH));
+
+static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH));
+static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH));
+static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH));
+
+static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH));
+static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH));
+static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH));
+
+#define check_eq(a, b) \
+ bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b))
+
+void check_bit(void) {
+ const char *str = "\x1\x2\x3\x4\x5\x6\x7\x8";
+ uint32_t word;
+ memcpy(&word, str, sizeof(word));
+
+#if ENDIAN_NATIVE == ENDIAN_LITTLE
+ check_eq(word, 0x04030201);
+#elif ENDIAN_NATIVE == ENDIAN_BIG
+ check_eq(word, 0x01020304);
+#else
+# warning "Skipping byte order tests on mixed/unknown-endian machine"
+#endif
+
+ check_eq(bswap((uint8_t)0x12), 0x12);
+ check_eq(bswap((uint16_t)0x1234), 0x3412);
+ check_eq(bswap((uint32_t)0x12345678), 0x78563412);
+ check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412);
+
+ // Make sure we can bswap() every unsigned type
+ (void)bswap((unsigned char)0);
+ (void)bswap((unsigned short)0);
+ (void)bswap(0U);
+ (void)bswap(0UL);
+ (void)bswap(0ULL);
+
+ check_eq(load8_beu8(str), 0x01);
+ check_eq(load8_leu8(str), 0x01);
+ check_eq(load8_beu16(str), 0x0102);
+ check_eq(load8_leu16(str), 0x0201);
+ check_eq(load8_beu32(str), 0x01020304);
+ check_eq(load8_leu32(str), 0x04030201);
+ check_eq(load8_beu64(str), 0x0102030405060708ULL);
+ check_eq(load8_leu64(str), 0x0807060504030201ULL);
+
+ check_eq(count_ones(0x0U), 0);
+ check_eq(count_ones(0x1U), 1);
+ check_eq(count_ones(0x2U), 1);
+ check_eq(count_ones(0x3U), 2);
+ check_eq(count_ones(0x137FU), 10);
+
+ check_eq(count_zeros(0U), UINT_WIDTH);
+ check_eq(count_zeros(0UL), ULONG_WIDTH);
+ check_eq(count_zeros(0ULL), ULLONG_WIDTH);
+ check_eq(count_zeros((uint8_t)0), 8);
+ check_eq(count_zeros((uint16_t)0), 16);
+ check_eq(count_zeros((uint32_t)0), 32);
+ check_eq(count_zeros((uint64_t)0), 64);
+
+ check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A);
+ check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123);
+ check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345);
+ check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678);
+
+ check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A);
+ check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341);
+ check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123);
+ check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678);
+
+ for (unsigned int i = 0; i < 16; ++i) {
+ uint16_t n = (uint16_t)1 << i;
+ for (unsigned int j = i; j < 16; ++j) {
+ uint16_t m = (uint16_t)1 << j;
+ uint16_t nm = n | m;
+ check_eq(count_ones(nm), 1 + (n != m));
+ check_eq(count_zeros(nm), 15 - (n != m));
+ check_eq(leading_zeros(nm), 15 - j);
+ check_eq(trailing_zeros(nm), i);
+ check_eq(first_leading_one(nm), 16 - j);
+ check_eq(first_trailing_one(nm), i + 1);
+ check_eq(bit_width(nm), j + 1);
+ check_eq(bit_floor(nm), m);
+ if (n == m) {
+ check_eq(bit_ceil(nm), m);
+ bfs_check(has_single_bit(nm));
+ } else {
+ if (j < 15) {
+ check_eq(bit_ceil(nm), (m << 1));
+ }
+ bfs_check(!has_single_bit(nm));
+ }
+ }
+ }
+
+ check_eq(leading_zeros((uint16_t)0), 16);
+ check_eq(trailing_zeros((uint16_t)0), 16);
+ check_eq(first_leading_one(0U), 0);
+ check_eq(first_trailing_one(0U), 0);
+ check_eq(bit_width(0U), 0);
+ check_eq(bit_floor(0U), 0);
+ check_eq(bit_ceil(0U), 1);
+
+ bfs_check(!has_single_bit(0U));
+ bfs_check(!has_single_bit(UINT32_MAX));
+ bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1)));
+}
diff --git a/tests/test_E.out b/tests/bsd/E.out
index 0f0971e..0f0971e 100644
--- a/tests/test_E.out
+++ b/tests/bsd/E.out
diff --git a/tests/bsd/E.sh b/tests/bsd/E.sh
new file mode 100644
index 0000000..5d97178
--- /dev/null
+++ b/tests/bsd/E.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -E . -regex '\./(\()'
diff --git a/tests/test_H_mnewer.out b/tests/bsd/H_mnewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_H_mnewer.out
+++ b/tests/bsd/H_mnewer.out
diff --git a/tests/bsd/H_mnewer.sh b/tests/bsd/H_mnewer.sh
new file mode 100644
index 0000000..94fe08b
--- /dev/null
+++ b/tests/bsd/H_mnewer.sh
@@ -0,0 +1 @@
+bfs_diff -H times -mnewer times/l
diff --git a/tests/test_H.out b/tests/bsd/Hf.out
index ff635ff..ff635ff 100644
--- a/tests/test_H.out
+++ b/tests/bsd/Hf.out
diff --git a/tests/bsd/Hf.sh b/tests/bsd/Hf.sh
new file mode 100644
index 0000000..333280c
--- /dev/null
+++ b/tests/bsd/Hf.sh
@@ -0,0 +1 @@
+bfs_diff -Hf links/deeply/nested/dir
diff --git a/tests/bsd/L_acl.out b/tests/bsd/L_acl.out
new file mode 100644
index 0000000..dd89800
--- /dev/null
+++ b/tests/bsd/L_acl.out
@@ -0,0 +1,2 @@
+./acl
+./link
diff --git a/tests/bsd/L_acl.sh b/tests/bsd/L_acl.sh
new file mode 100644
index 0000000..a3fcbc8
--- /dev/null
+++ b/tests/bsd/L_acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff -L . -acl
diff --git a/tests/bsd/L_xattr.out b/tests/bsd/L_xattr.out
new file mode 100644
index 0000000..21eb50f
--- /dev/null
+++ b/tests/bsd/L_xattr.out
@@ -0,0 +1,3 @@
+./link
+./xattr
+./xattr_2
diff --git a/tests/bsd/L_xattr.sh b/tests/bsd/L_xattr.sh
new file mode 100644
index 0000000..f8b56d8
--- /dev/null
+++ b/tests/bsd/L_xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff -L . -xattr
diff --git a/tests/bsd/L_xattrname.out b/tests/bsd/L_xattrname.out
new file mode 100644
index 0000000..9e4c172
--- /dev/null
+++ b/tests/bsd/L_xattrname.out
@@ -0,0 +1,2 @@
+./link
+./xattr
diff --git a/tests/bsd/L_xattrname.sh b/tests/bsd/L_xattrname.sh
new file mode 100644
index 0000000..8108d57
--- /dev/null
+++ b/tests/bsd/L_xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff -L . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff -L . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/test_X.out b/tests/bsd/X.out
index 008297b..dbe2408 100644
--- a/tests/test_X.out
+++ b/tests/bsd/X.out
@@ -1,17 +1,23 @@
weirdnames
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/...
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/...
weirdnames/.../h
+weirdnames/[
+weirdnames/[/k
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bsd/X.sh b/tests/bsd/X.sh
new file mode 100644
index 0000000..54000cf
--- /dev/null
+++ b/tests/bsd/X.sh
@@ -0,0 +1 @@
+! bfs_diff -X weirdnames
diff --git a/tests/bsd/acl.out b/tests/bsd/acl.out
new file mode 100644
index 0000000..92e2f67
--- /dev/null
+++ b/tests/bsd/acl.out
@@ -0,0 +1 @@
+./acl
diff --git a/tests/bsd/acl.sh b/tests/bsd/acl.sh
new file mode 100644
index 0000000..a13c75f
--- /dev/null
+++ b/tests/bsd/acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff . -acl
diff --git a/tests/test_asince.out b/tests/bsd/asince.out
index 650e550..650e550 100644
--- a/tests/test_asince.out
+++ b/tests/bsd/asince.out
diff --git a/tests/bsd/asince.sh b/tests/bsd/asince.sh
new file mode 100644
index 0000000..32d5228
--- /dev/null
+++ b/tests/bsd/asince.sh
@@ -0,0 +1 @@
+bfs_diff times -asince 1991-12-14T00:01
diff --git a/tests/bsd/d_path.out b/tests/bsd/d_path.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/d_path.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/bsd/d_path.sh b/tests/bsd/d_path.sh
new file mode 100644
index 0000000..010f76f
--- /dev/null
+++ b/tests/bsd/d_path.sh
@@ -0,0 +1 @@
+bfs_diff -d basic
diff --git a/tests/test_data_flow_depth.out b/tests/bsd/data_flow_depth.out
index 350b489..ab127ec 100644
--- a/tests/test_data_flow_depth.out
+++ b/tests/bsd/data_flow_depth.out
@@ -3,6 +3,6 @@ basic/e/f
basic/g/h
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
diff --git a/tests/bsd/data_flow_depth.sh b/tests/bsd/data_flow_depth.sh
new file mode 100644
index 0000000..cd5d6b2
--- /dev/null
+++ b/tests/bsd/data_flow_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +1 -depth -4
diff --git a/tests/bsd/data_flow_sparse.out b/tests/bsd/data_flow_sparse.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/data_flow_sparse.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/bsd/data_flow_sparse.sh b/tests/bsd/data_flow_sparse.sh
new file mode 100644
index 0000000..bd7e1f7
--- /dev/null
+++ b/tests/bsd/data_flow_sparse.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \)
diff --git a/tests/test_depth_depth_n.out b/tests/bsd/depth_depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_depth_n.out
+++ b/tests/bsd/depth_depth_n.out
diff --git a/tests/bsd/depth_depth_n.sh b/tests/bsd/depth_depth_n.sh
new file mode 100644
index 0000000..5989b3c
--- /dev/null
+++ b/tests/bsd/depth_depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth 2
diff --git a/tests/test_depth_depth_n_minus.out b/tests/bsd/depth_depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_depth_n_minus.out
+++ b/tests/bsd/depth_depth_n_minus.out
diff --git a/tests/bsd/depth_depth_n_minus.sh b/tests/bsd/depth_depth_n_minus.sh
new file mode 100644
index 0000000..1d8ac79
--- /dev/null
+++ b/tests/bsd/depth_depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth -2
diff --git a/tests/test_depth_depth_n_plus.out b/tests/bsd/depth_depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_depth_n_plus.out
+++ b/tests/bsd/depth_depth_n_plus.out
diff --git a/tests/bsd/depth_depth_n_plus.sh b/tests/bsd/depth_depth_n_plus.sh
new file mode 100644
index 0000000..64e392b
--- /dev/null
+++ b/tests/bsd/depth_depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth +2
diff --git a/tests/test_depth_n.out b/tests/bsd/depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_n.out
+++ b/tests/bsd/depth_n.out
diff --git a/tests/bsd/depth_n.sh b/tests/bsd/depth_n.sh
new file mode 100644
index 0000000..4852952
--- /dev/null
+++ b/tests/bsd/depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth 2
diff --git a/tests/test_depth_maxdepth_1.out b/tests/bsd/depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_maxdepth_1.out
+++ b/tests/bsd/depth_n_minus.out
diff --git a/tests/bsd/depth_n_minus.sh b/tests/bsd/depth_n_minus.sh
new file mode 100644
index 0000000..192bf8a
--- /dev/null
+++ b/tests/bsd/depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -2
diff --git a/tests/test_depth_n_plus.out b/tests/bsd/depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_n_plus.out
+++ b/tests/bsd/depth_n_plus.out
diff --git a/tests/bsd/depth_n_plus.sh b/tests/bsd/depth_n_plus.sh
new file mode 100644
index 0000000..858e1c4
--- /dev/null
+++ b/tests/bsd/depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +2
diff --git a/tests/bsd/depth_overflow.out b/tests/bsd/depth_overflow.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/depth_overflow.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/bsd/depth_overflow.sh b/tests/bsd/depth_overflow.sh
new file mode 100644
index 0000000..4685d03
--- /dev/null
+++ b/tests/bsd/depth_overflow.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -4294967296
diff --git a/tests/test_exit.out b/tests/bsd/exit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_exit.out
+++ b/tests/bsd/exit.out
diff --git a/tests/bsd/exit.sh b/tests/bsd/exit.sh
new file mode 100644
index 0000000..248349c
--- /dev/null
+++ b/tests/bsd/exit.sh
@@ -0,0 +1,5 @@
+check_exit 42 invoke_bfs basic -name foo -exit 42
+
+check_exit 0 invoke_bfs basic -name qux -exit 42
+
+bfs_diff basic/g -print -name g -exit
diff --git a/tests/test_exclude_mindepth.out b/tests/bsd/exit_no_implicit_print.out
index e69de29..e69de29 100644
--- a/tests/test_exclude_mindepth.out
+++ b/tests/bsd/exit_no_implicit_print.out
diff --git a/tests/bsd/exit_no_implicit_print.sh b/tests/bsd/exit_no_implicit_print.sh
new file mode 100644
index 0000000..c48b43c
--- /dev/null
+++ b/tests/bsd/exit_no_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo -o -exit
diff --git a/tests/test_f.out b/tests/bsd/f.out
index 2998538..77eac77 100644
--- a/tests/test_f.out
+++ b/tests/bsd/f.out
@@ -1,4 +1,4 @@
(
--
(/b
+-
-/a
diff --git a/tests/bsd/f.sh b/tests/bsd/f.sh
new file mode 100644
index 0000000..42d2dfd
--- /dev/null
+++ b/tests/bsd/f.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -f '-' -f '('
diff --git a/tests/bsd/f_incomplete.sh b/tests/bsd/f_incomplete.sh
new file mode 100644
index 0000000..0dfb19f
--- /dev/null
+++ b/tests/bsd/f_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -f
diff --git a/tests/bsd/flags.out b/tests/bsd/flags.out
new file mode 100644
index 0000000..3216ff5
--- /dev/null
+++ b/tests/bsd/flags.out
@@ -0,0 +1 @@
+./bar
diff --git a/tests/bsd/flags.sh b/tests/bsd/flags.sh
new file mode 100644
index 0000000..eb9bc22
--- /dev/null
+++ b/tests/bsd/flags.sh
@@ -0,0 +1,8 @@
+invoke_bfs . -quit -flags offline || skip
+
+cd "$TEST"
+
+"$XTOUCH" foo bar
+chflags offline bar || skip
+
+bfs_diff . -flags -offline,nohidden
diff --git a/tests/bsd/gid_name.out b/tests/bsd/gid_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/gid_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/gid_name.sh b/tests/bsd/gid_name.sh
new file mode 100644
index 0000000..c7e0e71
--- /dev/null
+++ b/tests/bsd/gid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -gn)"
diff --git a/tests/test_H_newer.out b/tests/bsd/mnewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_H_newer.out
+++ b/tests/bsd/mnewer.out
diff --git a/tests/bsd/mnewer.sh b/tests/bsd/mnewer.sh
new file mode 100644
index 0000000..5d9f1a7
--- /dev/null
+++ b/tests/bsd/mnewer.sh
@@ -0,0 +1 @@
+bfs_diff times -mnewer times/a
diff --git a/tests/test_msince.out b/tests/bsd/msince.out
index 650e550..650e550 100644
--- a/tests/test_msince.out
+++ b/tests/bsd/msince.out
diff --git a/tests/bsd/msince.sh b/tests/bsd/msince.sh
new file mode 100644
index 0000000..ec22f02
--- /dev/null
+++ b/tests/bsd/msince.sh
@@ -0,0 +1 @@
+bfs_diff times -msince 1991-12-14T00:01
diff --git a/tests/bsd/mtime_bad_unit.sh b/tests/bsd/mtime_bad_unit.sh
new file mode 100644
index 0000000..6e2caf1
--- /dev/null
+++ b/tests/bsd/mtime_bad_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1q
diff --git a/tests/bsd/mtime_missing_unit.sh b/tests/bsd/mtime_missing_unit.sh
new file mode 100644
index 0000000..f6b1f93
--- /dev/null
+++ b/tests/bsd/mtime_missing_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1w2
diff --git a/tests/test_mtime_units.out b/tests/bsd/mtime_units.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_mtime_units.out
+++ b/tests/bsd/mtime_units.out
diff --git a/tests/bsd/mtime_units.sh b/tests/bsd/mtime_units.sh
new file mode 100644
index 0000000..a1e587e
--- /dev/null
+++ b/tests/bsd/mtime_units.sh
@@ -0,0 +1 @@
+bfs_diff times -mtime +500w400d300h200m100s
diff --git a/tests/test_okdir_stdin.out b/tests/bsd/okdir_stdin.out
index ef2a68b..ef2a68b 100644
--- a/tests/test_okdir_stdin.out
+++ b/tests/bsd/okdir_stdin.out
diff --git a/tests/bsd/okdir_stdin.sh b/tests/bsd/okdir_stdin.sh
new file mode 100644
index 0000000..7908ac0
--- /dev/null
+++ b/tests/bsd/okdir_stdin.sh
@@ -0,0 +1,2 @@
+# -okdir should *not* close stdin
+yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/bsd/perm_000_plus.out b/tests/bsd/perm_000_plus.out
new file mode 100644
index 0000000..e279684
--- /dev/null
+++ b/tests/bsd/perm_000_plus.out
@@ -0,0 +1,29 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f---------
+perms/f--x------
+perms/f--x--x--x
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/bsd/perm_000_plus.sh b/tests/bsd/perm_000_plus.sh
new file mode 100644
index 0000000..9ab3146
--- /dev/null
+++ b/tests/bsd/perm_000_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +000
diff --git a/tests/bsd/perm_222_plus.out b/tests/bsd/perm_222_plus.out
new file mode 100644
index 0000000..1b6d885
--- /dev/null
+++ b/tests/bsd/perm_222_plus.out
@@ -0,0 +1,20 @@
+perms
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/bsd/perm_222_plus.sh b/tests/bsd/perm_222_plus.sh
new file mode 100644
index 0000000..ac3c4eb
--- /dev/null
+++ b/tests/bsd/perm_222_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +222
diff --git a/tests/bsd/perm_644_plus.out b/tests/bsd/perm_644_plus.out
new file mode 100644
index 0000000..eef88ca
--- /dev/null
+++ b/tests/bsd/perm_644_plus.out
@@ -0,0 +1,26 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/bsd/perm_644_plus.sh b/tests/bsd/perm_644_plus.sh
new file mode 100644
index 0000000..b3f5bc6
--- /dev/null
+++ b/tests/bsd/perm_644_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +644
diff --git a/tests/test_printx.out b/tests/bsd/printx.out
index 53c7547..034b2da 100644
--- a/tests/test_printx.out
+++ b/tests/bsd/printx.out
@@ -1,21 +1,31 @@
+
+/n
weirdnames
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/...
-weirdnames/\
-weirdnames/\\
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/*
+weirdnames/*/m
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/...
weirdnames/.../h
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
+weirdnames/\
+weirdnames/\
weirdnames/\ /j
+weirdnames/\\
weirdnames/\\/i
+weirdnames/{
+weirdnames/{/l
diff --git a/tests/bsd/printx.sh b/tests/bsd/printx.sh
new file mode 100644
index 0000000..cb24aab
--- /dev/null
+++ b/tests/bsd/printx.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -printx
diff --git a/tests/test_and_false_or_true.out b/tests/bsd/quit_implicit_print.out
index 15a13db..15a13db 100644
--- a/tests/test_and_false_or_true.out
+++ b/tests/bsd/quit_implicit_print.out
diff --git a/tests/bsd/quit_implicit_print.sh b/tests/bsd/quit_implicit_print.sh
new file mode 100644
index 0000000..ea8fd5d
--- /dev/null
+++ b/tests/bsd/quit_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name basic -o -quit
diff --git a/tests/bsd/rm.out b/tests/bsd/rm.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/bsd/rm.out
@@ -0,0 +1 @@
+.
diff --git a/tests/bsd/rm.sh b/tests/bsd/rm.sh
new file mode 100644
index 0000000..595d514
--- /dev/null
+++ b/tests/bsd/rm.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -rm
+bfs_diff .
diff --git a/tests/test_s.out b/tests/bsd/s.out
index e736cb5..5c85ac8 100644
--- a/tests/test_s.out
+++ b/tests/bsd/s.out
@@ -1,11 +1,16 @@
weirdnames
+weirdnames/
+
weirdnames/
weirdnames/!
weirdnames/!-
weirdnames/(
weirdnames/(-
weirdnames/)
+weirdnames/*
weirdnames/,
weirdnames/-
weirdnames/...
+weirdnames/[
weirdnames/\
+weirdnames/{
diff --git a/tests/bsd/s.sh b/tests/bsd/s.sh
new file mode 100644
index 0000000..52f8eb3
--- /dev/null
+++ b/tests/bsd/s.sh
@@ -0,0 +1,2 @@
+invoke_bfs -s weirdnames -maxdepth 1 >"$OUT"
+diff_output
diff --git a/tests/bsd/s_quit.out b/tests/bsd/s_quit.out
new file mode 100644
index 0000000..5ea492b
--- /dev/null
+++ b/tests/bsd/s_quit.out
@@ -0,0 +1 @@
+basic/j/foo
diff --git a/tests/bsd/s_quit.sh b/tests/bsd/s_quit.sh
new file mode 100644
index 0000000..6bd55ab
--- /dev/null
+++ b/tests/bsd/s_quit.sh
@@ -0,0 +1,4 @@
+# Regression test: bfs -S ids -s -name foo -quit would not actually quit,
+# ending up in a confused state and erroring/crashing
+
+bfs_diff -s basic -name foo -print -quit
diff --git a/tests/test_size_T.out b/tests/bsd/size_T.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_T.out
+++ b/tests/bsd/size_T.out
diff --git a/tests/bsd/size_T.sh b/tests/bsd/size_T.sh
new file mode 100644
index 0000000..1023a10
--- /dev/null
+++ b/tests/bsd/size_T.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 1T
diff --git a/tests/bsd/sparse.out b/tests/bsd/sparse.out
new file mode 100644
index 0000000..52dcf49
--- /dev/null
+++ b/tests/bsd/sparse.out
@@ -0,0 +1 @@
+mnt/sparse
diff --git a/tests/bsd/sparse.sh b/tests/bsd/sparse.sh
new file mode 100644
index 0000000..7fcdeed
--- /dev/null
+++ b/tests/bsd/sparse.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+truncate -s 1M mnt/sparse
+dd if=/dev/zero of=mnt/dense bs=1M count=1
+
+bfs_diff mnt -type f -sparse
diff --git a/tests/bsd/type_w.out b/tests/bsd/type_w.out
new file mode 100644
index 0000000..a20a4f3
--- /dev/null
+++ b/tests/bsd/type_w.out
@@ -0,0 +1,34 @@
+1: -rw-r--r-- mnt/lower/bar
+1: -rw-r--r-- mnt/lower/baz
+1: -rw-r--r-- mnt/lower/foo
+1: -rw-r--r-- mnt/upper/baz/qux
+1: -rw-r--r-- mnt/upper/foo
+1: drwxr-xr-x mnt/lower
+1: drwxr-xr-x mnt/upper
+1: drwxr-xr-x mnt/upper/baz
+2: w--------- mnt/upper/bar
+3: -rw-r--r-- mnt/lower/bar
+3: -rw-r--r-- mnt/lower/baz
+3: -rw-r--r-- mnt/lower/foo
+3: -rw-r--r-- mnt/upper/baz/qux
+3: -rw-r--r-- mnt/upper/foo
+3: drwxr-xr-x mnt/lower
+3: drwxr-xr-x mnt/upper
+3: drwxr-xr-x mnt/upper/baz
+3: w--------- mnt/upper/bar
+4: -rw-r--r-- mnt/lower/bar
+4: -rw-r--r-- mnt/lower/baz
+4: -rw-r--r-- mnt/lower/foo
+4: -rw-r--r-- mnt/upper/baz/qux
+4: drwxr-xr-x mnt/lower
+4: drwxr-xr-x mnt/upper
+4: drwxr-xr-x mnt/upper/baz
+5: w--------- mnt/upper/bar
+6: -rw-r--r-- mnt/lower/bar
+6: -rw-r--r-- mnt/lower/baz
+6: -rw-r--r-- mnt/lower/foo
+6: -rw-r--r-- mnt/upper/baz/qux
+6: drwxr-xr-x mnt/lower
+6: drwxr-xr-x mnt/upper
+6: drwxr-xr-x mnt/upper/baz
+6: w--------- mnt/upper/bar
diff --git a/tests/bsd/type_w.sh b/tests/bsd/type_w.sh
new file mode 100644
index 0000000..3aa50d5
--- /dev/null
+++ b/tests/bsd/type_w.sh
@@ -0,0 +1,56 @@
+# Only ffs supports whiteouts on FreeBSD
+command -v mdconfig &>/dev/null || skip
+command -v newfs &>/dev/null || skip
+
+cd "$TEST"
+
+# Create a ramdisk
+if command -v truncate &>/dev/null; then
+ truncate -s1M img
+else
+ dd if=/dev/zero of=img bs=1k count=1k
+fi
+md=$(bfs_sudo mdconfig img) || skip
+defer bfs_sudo mdconfig -du "$md"
+
+# Make an ffs filesystem
+bfs_sudo newfs -n "/dev/$md" >&2 || skip
+mkdir mnt
+
+# Mount it
+bfs_sudo mount "/dev/$md" mnt || skip
+defer bfs_sudo umount mnt
+
+# Make it owned by us
+bfs_sudo chown "$(id -u):$(id -g)" mnt
+"$XTOUCH" -p mnt/{lower/{foo,bar,baz},upper/{bar,baz/qux}}
+
+# Mount a union filesystem within it
+bfs_sudo mount -t unionfs -o below mnt/{lower,upper}
+defer bfs_sudo umount mnt/upper
+
+# Create a whiteout
+rm mnt/upper/bar
+
+# FreeBSD find doesn't have -printf, so munge -ls output
+munge_ls() {
+ sed -En 's|.*([-drwx]{10}).*(mnt/.*)|'"$1"': \1 \2|p'
+}
+
+# Do a few tests in one
+{
+ # Normally, we shouldn't see the whiteouts
+ invoke_bfs mnt -ls | munge_ls 1
+ # -type w adds whiteouts to the output
+ invoke_bfs mnt -type w -ls | munge_ls 2
+ # So this is not the same as test 1
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 3
+ # Unmount the unionfs
+ pop_defer
+ # Now repeat the same tests
+ invoke_bfs mnt -ls | munge_ls 4
+ invoke_bfs mnt -type w -ls | munge_ls 5
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 6
+} >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bsd/uid_name.out b/tests/bsd/uid_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/uid_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/uid_name.sh b/tests/bsd/uid_name.sh
new file mode 100644
index 0000000..7d3ba82
--- /dev/null
+++ b/tests/bsd/uid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -un)"
diff --git a/tests/bsd/xattr.out b/tests/bsd/xattr.out
new file mode 100644
index 0000000..0afed35
--- /dev/null
+++ b/tests/bsd/xattr.out
@@ -0,0 +1,3 @@
+./xattr
+./xattr_2
+./xattr_link
diff --git a/tests/bsd/xattr.sh b/tests/bsd/xattr.sh
new file mode 100644
index 0000000..68f729a
--- /dev/null
+++ b/tests/bsd/xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff . -xattr
diff --git a/tests/bsd/xattrname.out b/tests/bsd/xattrname.out
new file mode 100644
index 0000000..ef732bd
--- /dev/null
+++ b/tests/bsd/xattrname.out
@@ -0,0 +1,2 @@
+./xattr
+./xattr_link
diff --git a/tests/bsd/xattrname.sh b/tests/bsd/xattrname.sh
new file mode 100644
index 0000000..38b111a
--- /dev/null
+++ b/tests/bsd/xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/color.sh b/tests/color.sh
new file mode 100644
index 0000000..9e2e0f6
--- /dev/null
+++ b/tests/color.sh
@@ -0,0 +1,151 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Colored output
+
+# Common escape sequences
+BLD=$'\e[01m'
+RED=$'\e[01;31m'
+GRN=$'\e[01;32m'
+YLW=$'\e[01;33m'
+BLU=$'\e[01;34m'
+MAG=$'\e[01;35m'
+CYN=$'\e[01;36m'
+RST=$'\e[0m'
+
+# Check if we should color output to the given fd
+color_fd() {
+ [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
+}
+
+# Cache the color status for std{out,err}
+color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0
+color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0
+
+# Save this in case the tests unset PATH
+SED=$(command -v sed)
+
+# Filter out escape sequences if necessary
+color() {
+ if color_fd 1; then
+ "$@"
+ else
+ "$@" | "$SED" $'s/\e\\[[^m]*m//g'
+ fi
+}
+
+## Status bar
+
+# Show the terminal status bar
+show_bar() {
+ if [ -z "$TTY" ]; then
+ return 1
+ fi
+
+ # Name the pipe deterministically based on the ttyname, so that concurrent
+ # tests.sh runs on the same terminal (e.g. make -jN check) cooperate
+ local pipe="${TMPDIR:-/tmp}/bfs${TTY//\//-}.bar"
+
+ if mkfifo "$pipe" 2>/dev/null; then
+ # We won the race, create the background process to manage the bar
+ bar_proc "$pipe" &
+ exec {BAR}>"$pipe"
+ elif [ -p "$pipe" ]; then
+ # We lost the race, connect to the existing process.
+ # There is a small TOCTTOU race here but I don't see how to avoid it.
+ exec {BAR}>"$pipe"
+ else
+ return 1
+ fi
+}
+
+# Print to the terminal status bar
+print_bar() {
+ printf 'PRINT:%d:%s\0' $$ "$1" >&$BAR
+}
+
+# Hide the terminal status bar
+hide_bar() {
+ printf 'HIDE:%d:\0' $$ >&$BAR
+ exec {BAR}>&-
+ unset BAR
+}
+
+# The background process that muxes multiple status bars for one TTY
+bar_proc() {
+ # Read from the pipe, write to the TTY
+ exec <"$1" >/dev/tty
+
+ # Delete the pipe when done
+ defer rm "$1"
+ # Reset the scroll region when done
+ defer printf '\e7\e[r\e8\e[J'
+
+ # Workaround for bash 4: checkwinsize is off by default. We can turn it
+ # on, but we also have to explicitly trigger a foreground job to finish
+ # so that it will update the window size before we use $LINES
+ shopt -s checkwinsize
+ (:)
+
+ BAR_HEIGHT=0
+ resize_bar
+ # Adjust the bar when the TTY size changes
+ trap resize_bar WINCH
+
+ # Map from PID to status bar
+ local -A pid2bar
+
+ # Read commands of the form "OP:PID:STRING\0"
+ while IFS=':' read -r -d '' op pid str; do
+ # Map the pid to a bar, creating a new one if necessary
+ if [ -z "${pid2bar[$pid]:-}" ]; then
+ pid2bar["$pid"]=$((BAR_HEIGHT++))
+ resize_bar
+ fi
+ bar="${pid2bar[$pid]}"
+
+ case "$op" in
+ PRINT)
+ printf '\e7\e[%d;0f\e[K%s\e8' $((TTY_HEIGHT - bar)) "$str"
+ ;;
+ HIDE)
+ bar="${pid2bar[$pid]}"
+ # Delete this status bar
+ unset 'pid2bar[$pid]'
+ # Shift all higher status bars down
+ for i in "${!pid2bar[@]}"; do
+ ibar="${pid2bar[$i]}"
+ if ((ibar > bar)); then
+ pid2bar["$i"]=$((ibar - 1))
+ fi
+ done
+ ((BAR_HEIGHT--))
+ resize_bar
+ ;;
+ esac
+ done
+}
+
+# Resize the status bar
+resize_bar() {
+ # Bash gets $LINES from stderr, so if it's redirected use tput instead
+ TTY_HEIGHT="${LINES:-$(tput lines 2>/dev/tty)}"
+
+ if ((BAR_HEIGHT == 0)); then
+ return
+ fi
+
+ # Hide the bars temporarily
+ local seq='\e7\e[r\e8\e[J'
+ # Print \eD (IND) N times to ensure N blank lines at the bottom
+ for ((i = 0; i < BAR_HEIGHT; ++i)); do
+ seq="${seq}\\eD"
+ done
+ # Go back up N lines
+ seq="${seq}\\e[${BAR_HEIGHT}A"
+ # Create the new scroll region
+ seq="${seq}\\e7\\e[;$((TTY_HEIGHT - BAR_HEIGHT))r\\e8\\e[J"
+ printf "$seq"
+}
diff --git a/tests/test_P.out b/tests/common/HLP.out
index ff635ff..ff635ff 100644
--- a/tests/test_P.out
+++ b/tests/common/HLP.out
diff --git a/tests/common/HLP.sh b/tests/common/HLP.sh
new file mode 100644
index 0000000..4b6d631
--- /dev/null
+++ b/tests/common/HLP.sh
@@ -0,0 +1 @@
+bfs_diff -HLP links/deeply/nested/dir
diff --git a/tests/test_anewer.out b/tests/common/H_newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_anewer.out
+++ b/tests/common/H_newer.out
diff --git a/tests/common/H_newer.sh b/tests/common/H_newer.sh
new file mode 100644
index 0000000..c72bff7
--- /dev/null
+++ b/tests/common/H_newer.sh
@@ -0,0 +1 @@
+bfs_diff -H times -newer times/l
diff --git a/tests/test_H_broken.out b/tests/common/H_samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_H_broken.out
+++ b/tests/common/H_samefile_broken.out
diff --git a/tests/common/H_samefile_broken.sh b/tests/common/H_samefile_broken.sh
new file mode 100644
index 0000000..7a3366d
--- /dev/null
+++ b/tests/common/H_samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/broken
diff --git a/tests/test_H_notdir.out b/tests/common/H_samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_H_notdir.out
+++ b/tests/common/H_samefile_notdir.out
diff --git a/tests/common/H_samefile_notdir.sh b/tests/common/H_samefile_notdir.sh
new file mode 100644
index 0000000..25ad88d
--- /dev/null
+++ b/tests/common/H_samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/notdir
diff --git a/tests/test_H_samefile_symlink.out b/tests/common/H_samefile_symlink.out
index 996ffc8..996ffc8 100644
--- a/tests/test_H_samefile_symlink.out
+++ b/tests/common/H_samefile_symlink.out
diff --git a/tests/common/H_samefile_symlink.sh b/tests/common/H_samefile_symlink.sh
new file mode 100644
index 0000000..c73ddb2
--- /dev/null
+++ b/tests/common/H_samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/symlink
diff --git a/tests/test_false.out b/tests/common/L_ilname.out
index e69de29..e69de29 100644
--- a/tests/test_false.out
+++ b/tests/common/L_ilname.out
diff --git a/tests/common/L_ilname.sh b/tests/common/L_ilname.sh
new file mode 100644
index 0000000..e0495ed
--- /dev/null
+++ b/tests/common/L_ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff -L links -ilname '[AQ]'
diff --git a/tests/test_ilname.out b/tests/common/L_lname.out
index e69de29..e69de29 100644
--- a/tests/test_ilname.out
+++ b/tests/common/L_lname.out
diff --git a/tests/common/L_lname.sh b/tests/common/L_lname.sh
new file mode 100644
index 0000000..65b9da5
--- /dev/null
+++ b/tests/common/L_lname.sh
@@ -0,0 +1 @@
+bfs_diff -L links -lname '[aq]'
diff --git a/tests/common/L_ls.sh b/tests/common/L_ls.sh
new file mode 100644
index 0000000..7ee2b44
--- /dev/null
+++ b/tests/common/L_ls.sh
@@ -0,0 +1 @@
+invoke_bfs -L rainbow -ls >"$OUT"
diff --git a/tests/test_H_samefile_broken.out b/tests/common/L_samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_H_samefile_broken.out
+++ b/tests/common/L_samefile_broken.out
diff --git a/tests/common/L_samefile_broken.sh b/tests/common/L_samefile_broken.sh
new file mode 100644
index 0000000..5f860cc
--- /dev/null
+++ b/tests/common/L_samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/broken
diff --git a/tests/test_H_samefile_notdir.out b/tests/common/L_samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_H_samefile_notdir.out
+++ b/tests/common/L_samefile_notdir.out
diff --git a/tests/common/L_samefile_notdir.sh b/tests/common/L_samefile_notdir.sh
new file mode 100644
index 0000000..9b63429
--- /dev/null
+++ b/tests/common/L_samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/notdir
diff --git a/tests/test_L_samefile_symlink.out b/tests/common/L_samefile_symlink.out
index 222ac78..222ac78 100644
--- a/tests/test_L_samefile_symlink.out
+++ b/tests/common/L_samefile_symlink.out
diff --git a/tests/common/L_samefile_symlink.sh b/tests/common/L_samefile_symlink.sh
new file mode 100644
index 0000000..4a7a8dd
--- /dev/null
+++ b/tests/common/L_samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/symlink
diff --git a/tests/common/P.out b/tests/common/P.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/common/P.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/common/P.sh b/tests/common/P.sh
new file mode 100644
index 0000000..a7a09d1
--- /dev/null
+++ b/tests/common/P.sh
@@ -0,0 +1 @@
+bfs_diff -P links/deeply/nested/dir
diff --git a/tests/test_H_slash.out b/tests/common/P_slash.out
index df7701b..df7701b 100644
--- a/tests/test_H_slash.out
+++ b/tests/common/P_slash.out
diff --git a/tests/common/P_slash.sh b/tests/common/P_slash.sh
new file mode 100644
index 0000000..9b9ffa0
--- /dev/null
+++ b/tests/common/P_slash.sh
@@ -0,0 +1 @@
+bfs_diff -P links/deeply/nested/dir/
diff --git a/tests/common/amin.out b/tests/common/amin.out
new file mode 100644
index 0000000..af57325
--- /dev/null
+++ b/tests/common/amin.out
@@ -0,0 +1,6 @@
+-amin 1: ./one_minute_ago
+-amin +1: ./one_hour_ago
+-amin +1: ./two_minutes_ago
+-amin -1: ./in_one_hour
+-amin -1: ./in_one_minute
+-amin -1: ./thirty_seconds_ago
diff --git a/tests/common/amin.sh b/tests/common/amin.sh
new file mode 100644
index 0000000..92c3531
--- /dev/null
+++ b/tests/common/amin.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -at "@$((now - 121))" two_minutes_ago
+"$XTOUCH" -at "@$((now - 61))" one_minute_ago
+"$XTOUCH" -at "@$((now - 30))" thirty_seconds_ago
+"$XTOUCH" -at "@$((now + 60))" in_one_minute
+"$XTOUCH" -at "@$((now + 60 * 60))" in_one_hour
+
+bfs_diff . -mindepth 1 \
+ \( -amin -1 -exec printf -- '-amin -1: %s\n' {} \; -o -true \) \
+ \( -amin 1 -exec printf -- '-amin 1: %s\n' {} \; -o -true \) \
+ \( -amin +1 -exec printf -- '-amin +1: %s\n' {} \; -o -true \)
diff --git a/tests/test_mnewer.out b/tests/common/anewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_mnewer.out
+++ b/tests/common/anewer.out
diff --git a/tests/common/anewer.sh b/tests/common/anewer.sh
new file mode 100644
index 0000000..0cdd759
--- /dev/null
+++ b/tests/common/anewer.sh
@@ -0,0 +1 @@
+bfs_diff times -anewer times/a
diff --git a/tests/common/delete.out b/tests/common/delete.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete.sh b/tests/common/delete.sh
new file mode 100644
index 0000000..638f307
--- /dev/null
+++ b/tests/common/delete.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -delete
+bfs_diff .
diff --git a/tests/common/delete_error.out b/tests/common/delete_error.out
new file mode 100644
index 0000000..b6b6505
--- /dev/null
+++ b/tests/common/delete_error.out
@@ -0,0 +1,8 @@
+.
+.
+./baz
+./baz
+./baz/qux
+./baz/qux
+./foo
+./foo/bar
diff --git a/tests/common/delete_error.sh b/tests/common/delete_error.sh
new file mode 100644
index 0000000..e6327f3
--- /dev/null
+++ b/tests/common/delete_error.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/bar baz/qux
+chmod -w foo
+defer chmod +w foo
+
+! invoke_bfs . -print -delete -print >"$OUT" || fail
+sort_output
+diff_output
diff --git a/tests/common/delete_many.out b/tests/common/delete_many.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete_many.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete_many.sh b/tests/common/delete_many.sh
new file mode 100644
index 0000000..48fe4c2
--- /dev/null
+++ b/tests/common/delete_many.sh
@@ -0,0 +1,8 @@
+# Test for https://github.com/tavianator/bfs/issues/67
+
+cd "$TEST"
+mkdir foo
+"$XTOUCH" foo/{1..256}
+
+invoke_bfs foo -delete
+bfs_diff .
diff --git a/tests/test_depth_n_minus.out b/tests/common/depth_maxdepth_1.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_n_minus.out
+++ b/tests/common/depth_maxdepth_1.out
diff --git a/tests/common/depth_maxdepth_1.sh b/tests/common/depth_maxdepth_1.sh
new file mode 100644
index 0000000..4b7e538
--- /dev/null
+++ b/tests/common/depth_maxdepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1 -depth
diff --git a/tests/test_depth_maxdepth_2.out b/tests/common/depth_maxdepth_2.out
index b79fef1..c53041e 100644
--- a/tests/test_depth_maxdepth_2.out
+++ b/tests/common/depth_maxdepth_2.out
@@ -2,15 +2,15 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
diff --git a/tests/common/depth_maxdepth_2.sh b/tests/common/depth_maxdepth_2.sh
new file mode 100644
index 0000000..2c49a65
--- /dev/null
+++ b/tests/common/depth_maxdepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 2 -depth
diff --git a/tests/test_depth_mindepth_1.out b/tests/common/depth_mindepth_1.out
index 1f00c58..3b461cf 100644
--- a/tests/test_depth_mindepth_1.out
+++ b/tests/common/depth_mindepth_1.out
@@ -1,18 +1,18 @@
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/depth_mindepth_1.sh b/tests/common/depth_mindepth_1.sh
new file mode 100644
index 0000000..868d9e1
--- /dev/null
+++ b/tests/common/depth_mindepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1 -depth
diff --git a/tests/test_depth_mindepth_2.out b/tests/common/depth_mindepth_2.out
index 4198bf9..6ccd80a 100644
--- a/tests/test_depth_mindepth_2.out
+++ b/tests/common/depth_mindepth_2.out
@@ -3,7 +3,7 @@ basic/e/f
basic/g/h
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/depth_mindepth_2.sh b/tests/common/depth_mindepth_2.sh
new file mode 100644
index 0000000..2031b2c
--- /dev/null
+++ b/tests/common/depth_mindepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 2 -depth
diff --git a/tests/test_double_dash.out b/tests/common/double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_double_dash.out
+++ b/tests/common/double_dash.out
diff --git a/tests/common/double_dash.sh b/tests/common/double_dash.sh
new file mode 100644
index 0000000..1a7a118
--- /dev/null
+++ b/tests/common/double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -- . -type f
diff --git a/tests/test_empty.out b/tests/common/empty.out
index 81bdb0a..a0f4b76 100644
--- a/tests/test_empty.out
+++ b/tests/common/empty.out
@@ -1,8 +1,8 @@
basic/a
basic/b
-basic/i
basic/c/d
basic/e/f
basic/g/h
+basic/i
basic/j/foo
basic/k/foo/bar
diff --git a/tests/common/empty.sh b/tests/common/empty.sh
new file mode 100644
index 0000000..95ee988
--- /dev/null
+++ b/tests/common/empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -empty
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
new file mode 100644
index 0000000..49f773d
--- /dev/null
+++ b/tests/common/empty_error.out
@@ -0,0 +1 @@
+inaccessible/file
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
new file mode 100644
index 0000000..3438cca
--- /dev/null
+++ b/tests/common/empty_error.sh
@@ -0,0 +1 @@
+! bfs_diff inaccessible -empty
diff --git a/tests/common/empty_special.out b/tests/common/empty_special.out
new file mode 100644
index 0000000..fa35478
--- /dev/null
+++ b/tests/common/empty_special.out
@@ -0,0 +1,20 @@
+rainbow//
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/sgid
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/common/empty_special.sh b/tests/common/empty_special.sh
new file mode 100644
index 0000000..31e9d2e
--- /dev/null
+++ b/tests/common/empty_special.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -empty
diff --git a/tests/test_exec_substring.out b/tests/common/exec_substring.out
index 1c58fb2..32a6353 100644
--- a/tests/test_exec_substring.out
+++ b/tests/common/exec_substring.out
@@ -2,18 +2,18 @@
-basic/a-
-basic/b-
-basic/c-
+-basic/c/d-
-basic/e-
+-basic/e/f-
-basic/g-
+-basic/g/h-
-basic/i-
-basic/j-
--basic/k-
--basic/l-
--basic/c/d-
--basic/e/f-
--basic/g/h-
-basic/j/foo-
+-basic/k-
-basic/k/foo-
--basic/l/foo-
-basic/k/foo/bar-
+-basic/l-
+-basic/l/foo-
-basic/l/foo/bar-
-basic/l/foo/bar/baz-
diff --git a/tests/common/exec_substring.sh b/tests/common/exec_substring.sh
new file mode 100644
index 0000000..5cf8e85
--- /dev/null
+++ b/tests/common/exec_substring.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec echo '-{}-' \;
diff --git a/tests/common/execdir_nonexistent.out b/tests/common/execdir_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/execdir_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/execdir_nonexistent.sh b/tests/common/execdir_nonexistent.sh
new file mode 100644
index 0000000..0ec013c
--- /dev/null
+++ b/tests/common/execdir_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_execdir_pwd.out b/tests/common/execdir_pwd.out
index e66db9a..a11cd25 100644
--- a/tests/test_execdir_pwd.out
+++ b/tests/common/execdir_pwd.out
@@ -13,7 +13,7 @@ basic/e
basic/g
basic/j
basic/k
-basic/l
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/common/execdir_pwd.sh b/tests/common/execdir_pwd.sh
new file mode 100644
index 0000000..1c0165a
--- /dev/null
+++ b/tests/common/execdir_pwd.sh
@@ -0,0 +1,3 @@
+TMP_REAL=$(cd "$TMP" && pwd)
+OFFSET=$((${#TMP_REAL} + 2))
+bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \;
diff --git a/tests/test_execdir_slash.out b/tests/common/execdir_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash.out
+++ b/tests/common/execdir_slash.out
diff --git a/tests/common/execdir_slash.sh b/tests/common/execdir_slash.sh
new file mode 100644
index 0000000..965f679
--- /dev/null
+++ b/tests/common/execdir_slash.sh
@@ -0,0 +1,2 @@
+# Don't prepend ./ for absolute paths in -execdir
+bfs_diff / -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_slash_pwd.out b/tests/common/execdir_slash_pwd.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash_pwd.out
+++ b/tests/common/execdir_slash_pwd.out
diff --git a/tests/common/execdir_slash_pwd.sh b/tests/common/execdir_slash_pwd.sh
new file mode 100644
index 0000000..9c82e09
--- /dev/null
+++ b/tests/common/execdir_slash_pwd.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -execdir pwd \;
diff --git a/tests/test_execdir_slashes.out b/tests/common/execdir_slashes.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slashes.out
+++ b/tests/common/execdir_slashes.out
diff --git a/tests/common/execdir_slashes.sh b/tests/common/execdir_slashes.sh
new file mode 100644
index 0000000..4e2b327
--- /dev/null
+++ b/tests/common/execdir_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_ulimit.out b/tests/common/execdir_ulimit.out
index 7f53982..bf52c09 100644
--- a/tests/test_execdir_ulimit.out
+++ b/tests/common/execdir_ulimit.out
@@ -1,3 +1,4 @@
+./.
./0
./1
./2
@@ -30,7 +31,6 @@
./q
./r
./s
-./scratch
./t
./u
./v
diff --git a/tests/common/execdir_ulimit.sh b/tests/common/execdir_ulimit.sh
new file mode 100644
index 0000000..122c282
--- /dev/null
+++ b/tests/common/execdir_ulimit.sh
@@ -0,0 +1,6 @@
+cd "$TEST"
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
+
+ulimit -n $((NOPENFD + 10))
+bfs_diff . -execdir echo {} \;
diff --git a/tests/test_flag_double_dash.out b/tests/common/flag_double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_flag_double_dash.out
+++ b/tests/common/flag_double_dash.out
diff --git a/tests/common/flag_double_dash.sh b/tests/common/flag_double_dash.sh
new file mode 100644
index 0000000..1075b06
--- /dev/null
+++ b/tests/common/flag_double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -L -- . -type f
diff --git a/tests/test_follow.out b/tests/common/follow.out
index e24f4f7..ec9e861 100644
--- a/tests/test_follow.out
+++ b/tests/common/follow.out
@@ -1,17 +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/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/link
+links/symlink
diff --git a/tests/common/follow.sh b/tests/common/follow.sh
new file mode 100644
index 0000000..b5a2ae1
--- /dev/null
+++ b/tests/common/follow.sh
@@ -0,0 +1 @@
+bfs_diff links -follow
diff --git a/tests/common/gid.out b/tests/common/gid.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/gid.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/common/gid.sh b/tests/common/gid.sh
new file mode 100644
index 0000000..2707b4a
--- /dev/null
+++ b/tests/common/gid.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -g)"
diff --git a/tests/common/gid_invalid_id.sh b/tests/common/gid_invalid_id.sh
new file mode 100644
index 0000000..74f0055
--- /dev/null
+++ b/tests/common/gid_invalid_id.sh
@@ -0,0 +1 @@
+! invoke_bfs -gid 1eW6f5RM9Qi
diff --git a/tests/common/gid_invalid_name.sh b/tests/common/gid_invalid_name.sh
new file mode 100644
index 0000000..0e2e5f5
--- /dev/null
+++ b/tests/common/gid_invalid_name.sh
@@ -0,0 +1 @@
+! invoke_bfs -gid eW6f5RM9Qi
diff --git a/tests/common/gid_minus.out b/tests/common/gid_minus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/gid_minus.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/common/gid_minus.sh b/tests/common/gid_minus.sh
new file mode 100644
index 0000000..e3822f0
--- /dev/null
+++ b/tests/common/gid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-$(($(id -g) + 1))"
diff --git a/tests/common/gid_minus_plus.out b/tests/common/gid_minus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/gid_minus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/gid_minus_plus.sh b/tests/common/gid_minus_plus.sh
new file mode 100644
index 0000000..4ff0877
--- /dev/null
+++ b/tests/common/gid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-+$(($(id -g) + 1))"
diff --git a/tests/common/gid_plus.out b/tests/common/gid_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/gid_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/gid_plus.sh b/tests/common/gid_plus.sh
new file mode 100644
index 0000000..ccba0e6
--- /dev/null
+++ b/tests/common/gid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid +0
diff --git a/tests/common/gid_plus_plus.out b/tests/common/gid_plus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/gid_plus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/gid_plus_plus.sh b/tests/common/gid_plus_plus.sh
new file mode 100644
index 0000000..ec7ae86
--- /dev/null
+++ b/tests/common/gid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid ++0
diff --git a/tests/test_lname.out b/tests/common/ilname.out
index e69de29..e69de29 100644
--- a/tests/test_lname.out
+++ b/tests/common/ilname.out
diff --git a/tests/common/ilname.sh b/tests/common/ilname.sh
new file mode 100644
index 0000000..fc7e9e4
--- /dev/null
+++ b/tests/common/ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff links -ilname '[AQ]'
diff --git a/tests/test_inum.out b/tests/common/inum.out
index ebcaf79..ebcaf79 100644
--- a/tests/test_inum.out
+++ b/tests/common/inum.out
diff --git a/tests/common/inum.sh b/tests/common/inum.sh
new file mode 100644
index 0000000..ca63bbb
--- /dev/null
+++ b/tests/common/inum.sh
@@ -0,0 +1 @@
+bfs_diff basic -inum "$(inum basic/k/foo/bar)"
diff --git a/tests/common/inum_bind_mount.out b/tests/common/inum_bind_mount.out
new file mode 100644
index 0000000..ede8749
--- /dev/null
+++ b/tests/common/inum_bind_mount.out
@@ -0,0 +1,2 @@
+./bar
+./foo
diff --git a/tests/common/inum_bind_mount.sh b/tests/common/inum_bind_mount.sh
new file mode 100644
index 0000000..892713e
--- /dev/null
+++ b/tests/common/inum_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" foo bar baz
+
+bfs_sudo mount --bind foo bar || skip
+defer bfs_sudo umount bar
+
+bfs_diff . -inum "$(inum bar)"
diff --git a/tests/common/inum_mount.out b/tests/common/inum_mount.out
new file mode 100644
index 0000000..99fa01e
--- /dev/null
+++ b/tests/common/inum_mount.out
@@ -0,0 +1 @@
+./mnt
diff --git a/tests/common/inum_mount.sh b/tests/common/inum_mount.sh
new file mode 100644
index 0000000..7facf57
--- /dev/null
+++ b/tests/common/inum_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff . -inum "$(inum mnt)"
diff --git a/tests/test_path.out b/tests/common/ipath.out
index 0d36df9..ae1ae21 100644
--- a/tests/test_path.out
+++ b/tests/common/ipath.out
@@ -1,7 +1,7 @@
basic/e/f
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/ipath.sh b/tests/common/ipath.sh
new file mode 100644
index 0000000..7d05f31
--- /dev/null
+++ b/tests/common/ipath.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ipath PATTERN || skip
+bfs_diff basic -ipath 'basic/*F*'
diff --git a/tests/test_iregex.out b/tests/common/iregex.out
index cfc113b..cfc113b 100644
--- a/tests/test_iregex.out
+++ b/tests/common/iregex.out
diff --git a/tests/common/iregex.sh b/tests/common/iregex.sh
new file mode 100644
index 0000000..fc782f5
--- /dev/null
+++ b/tests/common/iregex.sh
@@ -0,0 +1 @@
+bfs_diff basic -iregex 'basic/[A-Z]/[a-z]'
diff --git a/tests/test_nogroup.out b/tests/common/lname.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup.out
+++ b/tests/common/lname.out
diff --git a/tests/common/lname.sh b/tests/common/lname.sh
new file mode 100644
index 0000000..cf8a2a1
--- /dev/null
+++ b/tests/common/lname.sh
@@ -0,0 +1 @@
+bfs_diff links -lname '[aq]'
diff --git a/tests/common/ls.sh b/tests/common/ls.sh
new file mode 100644
index 0000000..bc50d90
--- /dev/null
+++ b/tests/common/ls.sh
@@ -0,0 +1 @@
+invoke_bfs rainbow -ls >"$OUT"
diff --git a/tests/test_maxdepth.out b/tests/common/maxdepth.out
index 7575ae4..7575ae4 100644
--- a/tests/test_maxdepth.out
+++ b/tests/common/maxdepth.out
diff --git a/tests/common/maxdepth.sh b/tests/common/maxdepth.sh
new file mode 100644
index 0000000..bb47cc9
--- /dev/null
+++ b/tests/common/maxdepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1
diff --git a/tests/common/maxdepth_incomplete.sh b/tests/common/maxdepth_incomplete.sh
new file mode 100644
index 0000000..0bcb461
--- /dev/null
+++ b/tests/common/maxdepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -maxdepth
diff --git a/tests/test_mindepth.out b/tests/common/mindepth.out
index 1f00c58..3b461cf 100644
--- a/tests/test_mindepth.out
+++ b/tests/common/mindepth.out
@@ -1,18 +1,18 @@
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/mindepth.sh b/tests/common/mindepth.sh
new file mode 100644
index 0000000..22d7770
--- /dev/null
+++ b/tests/common/mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1
diff --git a/tests/common/mindepth_incomplete.sh b/tests/common/mindepth_incomplete.sh
new file mode 100644
index 0000000..6f55a42
--- /dev/null
+++ b/tests/common/mindepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -mindepth
diff --git a/tests/common/mmin.out b/tests/common/mmin.out
new file mode 100644
index 0000000..4c79a16
--- /dev/null
+++ b/tests/common/mmin.out
@@ -0,0 +1,6 @@
+-mmin 1: ./one_minute_ago
+-mmin +1: ./one_hour_ago
+-mmin +1: ./two_minutes_ago
+-mmin -1: ./in_one_hour
+-mmin -1: ./in_one_minute
+-mmin -1: ./thirty_seconds_ago
diff --git a/tests/common/mmin.sh b/tests/common/mmin.sh
new file mode 100644
index 0000000..4e1d19c
--- /dev/null
+++ b/tests/common/mmin.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -mt "@$((now - 121))" two_minutes_ago
+"$XTOUCH" -mt "@$((now - 61))" one_minute_ago
+"$XTOUCH" -mt "@$((now - 30))" thirty_seconds_ago
+"$XTOUCH" -mt "@$((now + 60))" in_one_minute
+"$XTOUCH" -mt "@$((now + 60 * 60))" in_one_hour
+
+bfs_diff . -mindepth 1 \
+ \( -mmin -1 -exec printf -- '-mmin -1: %s\n' {} \; -o -true \) \
+ \( -mmin 1 -exec printf -- '-mmin 1: %s\n' {} \; -o -true \) \
+ \( -mmin +1 -exec printf -- '-mmin +1: %s\n' {} \; -o -true \)
diff --git a/tests/test_newer.out b/tests/common/newerma.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newer.out
+++ b/tests/common/newerma.out
diff --git a/tests/common/newerma.sh b/tests/common/newerma.sh
new file mode 100644
index 0000000..b05af8d
--- /dev/null
+++ b/tests/common/newerma.sh
@@ -0,0 +1 @@
+bfs_diff times -newerma times/a
diff --git a/tests/test_newermt.out b/tests/common/newermt.out
index 650e550..650e550 100644
--- a/tests/test_newermt.out
+++ b/tests/common/newermt.out
diff --git a/tests/common/newermt.sh b/tests/common/newermt.sh
new file mode 100644
index 0000000..e816b29
--- /dev/null
+++ b/tests/common/newermt.sh
@@ -0,0 +1,3 @@
+bfs_diff times -newermt 1991-12-14T00:01 \
+ -newermt "1991-12-14 01:01+01:00" \
+ -newermt "19911213 20:31:00-0330"
diff --git a/tests/test_newermt_epoch_minus_one.out b/tests/common/newermt_epoch_minus_one.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_newermt_epoch_minus_one.out
+++ b/tests/common/newermt_epoch_minus_one.out
diff --git a/tests/common/newermt_epoch_minus_one.sh b/tests/common/newermt_epoch_minus_one.sh
new file mode 100644
index 0000000..568e2f3
--- /dev/null
+++ b/tests/common/newermt_epoch_minus_one.sh
@@ -0,0 +1 @@
+bfs_diff times -newermt 1969-12-31T23:59:59Z
diff --git a/tests/test_nogroup_ulimit.out b/tests/common/ok_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup_ulimit.out
+++ b/tests/common/ok_closed_stdin.out
diff --git a/tests/common/ok_closed_stdin.sh b/tests/common/ok_closed_stdin.sh
new file mode 100644
index 0000000..687e998
--- /dev/null
+++ b/tests/common/ok_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -ok echo \; <&-
diff --git a/tests/test_nouser.out b/tests/common/okdir_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_nouser.out
+++ b/tests/common/okdir_closed_stdin.out
diff --git a/tests/common/okdir_closed_stdin.sh b/tests/common/okdir_closed_stdin.sh
new file mode 100644
index 0000000..a515298
--- /dev/null
+++ b/tests/common/okdir_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -okdir echo {} \; <&-
diff --git a/tests/test_name_root_depth.out b/tests/common/quit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_name_root_depth.out
+++ b/tests/common/quit.out
diff --git a/tests/common/quit.sh b/tests/common/quit.sh
new file mode 100644
index 0000000..46b60c5
--- /dev/null
+++ b/tests/common/quit.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name g -quit
diff --git a/tests/test_comma_reachability.out b/tests/common/quit_after_print.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_reachability.out
+++ b/tests/common/quit_after_print.out
diff --git a/tests/common/quit_after_print.sh b/tests/common/quit_after_print.sh
new file mode 100644
index 0000000..ee5653a
--- /dev/null
+++ b/tests/common/quit_after_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -print -quit
diff --git a/tests/test_nouser_ulimit.out b/tests/common/quit_before_print.out
index e69de29..e69de29 100644
--- a/tests/test_nouser_ulimit.out
+++ b/tests/common/quit_before_print.out
diff --git a/tests/common/quit_before_print.sh b/tests/common/quit_before_print.sh
new file mode 100644
index 0000000..cda3a2c
--- /dev/null
+++ b/tests/common/quit_before_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -quit -print
diff --git a/tests/test_quit_child.out b/tests/common/quit_child.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_child.out
+++ b/tests/common/quit_child.out
diff --git a/tests/common/quit_child.sh b/tests/common/quit_child.sh
new file mode 100644
index 0000000..bd27eff
--- /dev/null
+++ b/tests/common/quit_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name h -quit
diff --git a/tests/test_quit_depth.out b/tests/common/quit_depth.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_depth.out
+++ b/tests/common/quit_depth.out
diff --git a/tests/common/quit_depth.sh b/tests/common/quit_depth.sh
new file mode 100644
index 0000000..f5f82ba
--- /dev/null
+++ b/tests/common/quit_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name g -quit
diff --git a/tests/test_quit_depth_child.out b/tests/common/quit_depth_child.out
index 72b086d..72b086d 100644
--- a/tests/test_quit_depth_child.out
+++ b/tests/common/quit_depth_child.out
diff --git a/tests/common/quit_depth_child.sh b/tests/common/quit_depth_child.sh
new file mode 100644
index 0000000..dd09d5b
--- /dev/null
+++ b/tests/common/quit_depth_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name h -quit
diff --git a/tests/test_regex.out b/tests/common/regex.out
index cfc113b..cfc113b 100644
--- a/tests/test_regex.out
+++ b/tests/common/regex.out
diff --git a/tests/common/regex.sh b/tests/common/regex.sh
new file mode 100644
index 0000000..a3bdae8
--- /dev/null
+++ b/tests/common/regex.sh
@@ -0,0 +1 @@
+bfs_diff basic -regex 'basic/./.'
diff --git a/tests/test_regex_parens.out b/tests/common/regex_parens.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regex_parens.out
+++ b/tests/common/regex_parens.out
diff --git a/tests/common/regex_parens.sh b/tests/common/regex_parens.sh
new file mode 100644
index 0000000..fe0abf6
--- /dev/null
+++ b/tests/common/regex_parens.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . -regex '\./\((\)'
diff --git a/tests/test_links.out b/tests/common/samefile.out
index 996ffc8..996ffc8 100644
--- a/tests/test_links.out
+++ b/tests/common/samefile.out
diff --git a/tests/common/samefile.sh b/tests/common/samefile.sh
new file mode 100644
index 0000000..8e51966
--- /dev/null
+++ b/tests/common/samefile.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/file
diff --git a/tests/test_L_broken.out b/tests/common/samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_L_broken.out
+++ b/tests/common/samefile_broken.out
diff --git a/tests/common/samefile_broken.sh b/tests/common/samefile_broken.sh
new file mode 100644
index 0000000..1cb52db
--- /dev/null
+++ b/tests/common/samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/broken
diff --git a/tests/test_L_notdir.out b/tests/common/samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_L_notdir.out
+++ b/tests/common/samefile_notdir.out
diff --git a/tests/common/samefile_notdir.sh b/tests/common/samefile_notdir.sh
new file mode 100644
index 0000000..f274ef6
--- /dev/null
+++ b/tests/common/samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/notdir
diff --git a/tests/test_samefile_symlink.out b/tests/common/samefile_symlink.out
index 299a572..299a572 100644
--- a/tests/test_samefile_symlink.out
+++ b/tests/common/samefile_symlink.out
diff --git a/tests/common/samefile_symlink.sh b/tests/common/samefile_symlink.sh
new file mode 100644
index 0000000..55ccf5c
--- /dev/null
+++ b/tests/common/samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/symlink
diff --git a/tests/common/samefile_wordesc.sh b/tests/common/samefile_wordesc.sh
new file mode 100644
index 0000000..b5d158f
--- /dev/null
+++ b/tests/common/samefile_wordesc.sh
@@ -0,0 +1,4 @@
+# Regression test: don't abort on incomplete UTF-8 sequences
+export LC_ALL=$(locale -a | grep -Ei 'utf-?8$' | head -n1)
+test -n "$LC_ALL" || skip
+! invoke_bfs -samefile $'\xFA\xFA'
diff --git a/tests/test_ok_closed_stdin.out b/tests/common/size_big.out
index e69de29..e69de29 100644
--- a/tests/test_ok_closed_stdin.out
+++ b/tests/common/size_big.out
diff --git a/tests/common/size_big.sh b/tests/common/size_big.sh
new file mode 100644
index 0000000..6c100eb
--- /dev/null
+++ b/tests/common/size_big.sh
@@ -0,0 +1 @@
+bfs_diff basic -size 9223372036854775807
diff --git a/tests/common/uid.out b/tests/common/uid.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/uid.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/common/uid.sh b/tests/common/uid.sh
new file mode 100644
index 0000000..fb3cd93
--- /dev/null
+++ b/tests/common/uid.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -u)"
diff --git a/tests/common/uid_invalid_id.sh b/tests/common/uid_invalid_id.sh
new file mode 100644
index 0000000..f5b952d
--- /dev/null
+++ b/tests/common/uid_invalid_id.sh
@@ -0,0 +1 @@
+! invoke_bfs -uid 1eW6f5RM9Qi
diff --git a/tests/common/uid_invalid_name.sh b/tests/common/uid_invalid_name.sh
new file mode 100644
index 0000000..a2c359f
--- /dev/null
+++ b/tests/common/uid_invalid_name.sh
@@ -0,0 +1 @@
+! invoke_bfs -uid eW6f5RM9Qi
diff --git a/tests/common/uid_minus.out b/tests/common/uid_minus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/uid_minus.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/common/uid_minus.sh b/tests/common/uid_minus.sh
new file mode 100644
index 0000000..6d371f2
--- /dev/null
+++ b/tests/common/uid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-$(($(id -u) + 1))"
diff --git a/tests/common/uid_minus_plus.out b/tests/common/uid_minus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/uid_minus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/uid_minus_plus.sh b/tests/common/uid_minus_plus.sh
new file mode 100644
index 0000000..e7a0496
--- /dev/null
+++ b/tests/common/uid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-+$(($(id -u) + 1))"
diff --git a/tests/common/uid_plus.out b/tests/common/uid_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/uid_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/uid_plus.sh b/tests/common/uid_plus.sh
new file mode 100644
index 0000000..22b2c8e
--- /dev/null
+++ b/tests/common/uid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid +0
diff --git a/tests/common/uid_plus_plus.out b/tests/common/uid_plus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/uid_plus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/uid_plus_plus.sh b/tests/common/uid_plus_plus.sh
new file mode 100644
index 0000000..e021888
--- /dev/null
+++ b/tests/common/uid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid ++0
diff --git a/tests/find-color.sh b/tests/find-color.sh
index ecdd5af..47de2a2 100755
--- a/tests/find-color.sh
+++ b/tests/find-color.sh
@@ -1,20 +1,7 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
set -e
diff --git a/tests/getopts.sh b/tests/getopts.sh
new file mode 100644
index 0000000..a16511f
--- /dev/null
+++ b/tests/getopts.sh
@@ -0,0 +1,174 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Argument parsing
+
+JOBS=$(_nproc)
+MAKE=
+PATTERNS=()
+SUDO=()
+STOP=0
+CLEAN=1
+UPDATE=0
+VERBOSE_COMMANDS=0
+VERBOSE_ERRORS=0
+VERBOSE_SKIPPED=0
+VERBOSE_TESTS=0
+
+# Print usage information
+usage() {
+ color cat <<EOF
+Usage: ${GRN}$0${RST}
+ [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]]
+ [${BLU}--stop${RST}] [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
+ [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
+
+ ${BLU}-j${RST}${BLD}N${RST}
+ Run ${BLD}N${RST} tests in parallel (default: ${BLD}$JOBS${RST})
+
+ ${BLU}--make${RST}=${BLD}MAKE${RST}
+ Use the jobserver from ${BLD}MAKE${RST}, e.g. ${BLU}--make${RST}=${BLD}"make -j$JOBS"${RST}
+
+ ${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}
+ Set the path to the bfs executable to test (default: ${BLD}./bin/bfs${RST})
+
+ ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
+ Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
+
+ ${BLU}--stop${RST}
+ Stop when the first error occurs
+
+ ${BLU}--no-clean${RST}
+ Keep the test directories around after the run
+
+ ${BLU}--update${RST}
+ Update the expected outputs for the test cases
+
+ ${BLU}--verbose${RST}=${BLD}commands${RST}
+ Log the commands that get executed
+ ${BLU}--verbose${RST}=${BLD}errors${RST}
+ Don't redirect standard error
+ ${BLU}--verbose${RST}=${BLD}skipped${RST}
+ Log which tests get skipped
+ ${BLU}--verbose${RST}=${BLD}tests${RST}
+ Log all tests that get run
+ ${BLU}--verbose${RST}
+ Log everything
+
+ ${BLU}--help${RST}
+ This message
+
+ ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
+ Choose which test cases to run (default: ${BLU}--all${RST})
+
+ ${BLD}TEST${RST}
+ Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
+EOF
+}
+
+# Parse the command line
+parse_args() {
+ for arg; do
+ case "$arg" in
+ -j?*)
+ JOBS="${arg#-j}"
+ ;;
+ --make=*)
+ MAKE="${arg#*=}"
+ ;;
+ --bfs=*)
+ BFS="${arg#*=}"
+ ;;
+ --posix)
+ PATTERNS+=("posix/*")
+ ;;
+ --bsd)
+ PATTERNS+=("posix/*" "common/*" "bsd/*")
+ ;;
+ --gnu)
+ PATTERNS+=("posix/*" "common/*" "gnu/*")
+ ;;
+ --all)
+ PATTERNS+=("*")
+ ;;
+ --sudo)
+ SUDO=(sudo)
+ ;;
+ --sudo=*)
+ read -a SUDO <<<"${arg#*=}"
+ ;;
+ --stop)
+ STOP=1
+ ;;
+ --no-clean|--noclean)
+ CLEAN=0
+ ;;
+ --update)
+ UPDATE=1
+ ;;
+ --verbose=commands)
+ VERBOSE_COMMANDS=1
+ ;;
+ --verbose=errors)
+ VERBOSE_ERRORS=1
+ ;;
+ --verbose=skipped)
+ VERBOSE_SKIPPED=1
+ ;;
+ --verbose=tests)
+ VERBOSE_TESTS=1
+ ;;
+ --verbose)
+ VERBOSE_COMMANDS=1
+ VERBOSE_ERRORS=1
+ VERBOSE_SKIPPED=1
+ VERBOSE_TESTS=1
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ -*)
+ color printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
+ usage >&2
+ exit 1
+ ;;
+ *)
+ PATTERNS+=("$arg")
+ ;;
+ esac
+ done
+
+ read -a MAKE <<<"$MAKE"
+
+ # Try to resolve the path to $BFS before we cd, while also supporting
+ # --bfs="./bin/bfs -S ids"
+ read -a BFS <<<"${BFS:-$BIN/bfs}"
+ BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
+
+ if ((${#PATTERNS[@]} == 0)); then
+ PATTERNS=("*")
+ fi
+
+ TEST_CASES=()
+ ALL_TESTS=($(cd "$TESTS" && quote {posix,common,bsd,gnu,bfs}/*.sh))
+ for TEST in "${ALL_TESTS[@]}"; do
+ TEST="${TEST%.sh}"
+ for PATTERN in "${PATTERNS[@]}"; do
+ if [[ $TEST == $PATTERN ]]; then
+ TEST_CASES+=("$TEST")
+ break
+ fi
+ done
+ done
+
+ if ((${#TEST_CASES[@]} == 0)); then
+ color printf "${RED}error:${RST} No tests matched" >&2
+ color printf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
+ color printf ".\n\n" >&2
+ usage >&2
+ exit 1
+ fi
+}
diff --git a/tests/gnu/L_delete.out b/tests/gnu/L_delete.out
new file mode 100644
index 0000000..7ed5f0d
--- /dev/null
+++ b/tests/gnu/L_delete.out
@@ -0,0 +1,2 @@
+.
+./foo
diff --git a/tests/gnu/L_delete.sh b/tests/gnu/L_delete.sh
new file mode 100644
index 0000000..0559c49
--- /dev/null
+++ b/tests/gnu/L_delete.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+mkdir foo bar
+ln -s ../foo bar/baz
+
+# Don't try to rmdir() a symlink
+invoke_bfs -L bar -delete
+
+bfs_diff .
diff --git a/tests/gnu/L_loops_continue.out b/tests/gnu/L_loops_continue.out
new file mode 100644
index 0000000..a514555
--- /dev/null
+++ b/tests/gnu/L_loops_continue.out
@@ -0,0 +1,11 @@
+loops
+loops/broken
+loops/deeply
+loops/deeply/nested
+loops/deeply/nested/dir
+loops/file
+loops/notdir
+loops/skip
+loops/skip/dir
+loops/skip/loop
+loops/symlink
diff --git a/tests/gnu/L_loops_continue.sh b/tests/gnu/L_loops_continue.sh
new file mode 100644
index 0000000..55aeb33
--- /dev/null
+++ b/tests/gnu/L_loops_continue.sh
@@ -0,0 +1 @@
+! bfs_diff -L loops
diff --git a/tests/gnu/L_printf_types.out b/tests/gnu/L_printf_types.out
new file mode 100644
index 0000000..734b15f
--- /dev/null
+++ b/tests/gnu/L_printf_types.out
@@ -0,0 +1,17 @@
+(links) () d d
+(links/broken) (nowhere) l N
+(links/deeply) () d d
+(links/deeply/nested) () d d
+(links/deeply/nested/broken) (nowhere) l N
+(links/deeply/nested/dir) () d d
+(links/deeply/nested/file) () f f
+(links/deeply/nested/link) () f f
+(links/file) () f f
+(links/hardlink) () f f
+(links/notdir) (symlink/file) l N
+(links/skip) () d d
+(links/skip/broken) (nowhere) l N
+(links/skip/dir) () d d
+(links/skip/file) () f f
+(links/skip/link) () f f
+(links/symlink) () f f
diff --git a/tests/gnu/L_printf_types.sh b/tests/gnu/L_printf_types.sh
new file mode 100644
index 0000000..caa9083
--- /dev/null
+++ b/tests/gnu/L_printf_types.sh
@@ -0,0 +1 @@
+bfs_diff -L links -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/test_L_xtype_f.out b/tests/gnu/L_xtype_f.out
index 78953d1..8b95397 100644
--- a/tests/test_L_xtype_f.out
+++ b/tests/gnu/L_xtype_f.out
@@ -1,4 +1,4 @@
+links/deeply/nested/file
links/file
links/hardlink
links/skip/file
-links/deeply/nested/file
diff --git a/tests/gnu/L_xtype_f.sh b/tests/gnu/L_xtype_f.sh
new file mode 100644
index 0000000..47f7be7
--- /dev/null
+++ b/tests/gnu/L_xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype f
diff --git a/tests/test_L_xtype_l.out b/tests/gnu/L_xtype_l.out
index 93cfc26..973864f 100644
--- a/tests/test_L_xtype_l.out
+++ b/tests/gnu/L_xtype_l.out
@@ -1,8 +1,8 @@
links/broken
+links/deeply/nested/broken
+links/deeply/nested/link
links/notdir
links/skip
-links/symlink
links/skip/broken
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/link
+links/symlink
diff --git a/tests/gnu/L_xtype_l.sh b/tests/gnu/L_xtype_l.sh
new file mode 100644
index 0000000..afe52ef
--- /dev/null
+++ b/tests/gnu/L_xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype l
diff --git a/tests/test_a.out b/tests/gnu/and.out
index 722962c..722962c 100644
--- a/tests/test_a.out
+++ b/tests/gnu/and.out
diff --git a/tests/gnu/and.sh b/tests/gnu/and.sh
new file mode 100644
index 0000000..1606455
--- /dev/null
+++ b/tests/gnu/and.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -and -type d
diff --git a/tests/test_comma_redundant_false.out b/tests/gnu/and_false_or_true.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_false.out
+++ b/tests/gnu/and_false_or_true.out
diff --git a/tests/gnu/and_false_or_true.sh b/tests/gnu/and_false_or_true.sh
new file mode 100644
index 0000000..e500722
--- /dev/null
+++ b/tests/gnu/and_false_or_true.sh
@@ -0,0 +1,3 @@
+# Test (-a lhs(always_true) -false) <==> (! lhs),
+# (-o lhs(always_false) -true) <==> (! lhs)
+bfs_diff basic -prune -false -o -true
diff --git a/tests/test_okdir_closed_stdin.out b/tests/gnu/and_purity.out
index e69de29..e69de29 100644
--- a/tests/test_okdir_closed_stdin.out
+++ b/tests/gnu/and_purity.out
diff --git a/tests/gnu/and_purity.sh b/tests/gnu/and_purity.sh
new file mode 100644
index 0000000..55e2cfc
--- /dev/null
+++ b/tests/gnu/and_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name nonexistent \( -print , -false \)
diff --git a/tests/test_comma.out b/tests/gnu/comma.out
index 56b2bde..740eefc 100644
--- a/tests/test_comma.out
+++ b/tests/gnu/comma.out
@@ -2,22 +2,22 @@ basic
basic/a
basic/b
basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
basic/c/d
+basic/e
basic/e/f
basic/e/f
+basic/g
basic/g/h
+basic/i
+basic/j
basic/j/foo
basic/j/foo
+basic/k
basic/k/foo
basic/k/foo
+basic/k/foo/bar
+basic/l
basic/l/foo
basic/l/foo
-basic/k/foo/bar
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/comma.sh b/tests/gnu/comma.sh
new file mode 100644
index 0000000..cdcebf8
--- /dev/null
+++ b/tests/gnu/comma.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*' -print , -print
diff --git a/tests/test_comma_redundant_true.out b/tests/gnu/comma_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_true.out
+++ b/tests/gnu/comma_reachability.out
diff --git a/tests/gnu/comma_reachability.sh b/tests/gnu/comma_reachability.sh
new file mode 100644
index 0000000..60c26bc
--- /dev/null
+++ b/tests/gnu/comma_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -quit , -print
diff --git a/tests/test_not_reachability.out b/tests/gnu/comma_redundant_false.out
index 15a13db..15a13db 100644
--- a/tests/test_not_reachability.out
+++ b/tests/gnu/comma_redundant_false.out
diff --git a/tests/gnu/comma_redundant_false.sh b/tests/gnu/comma_redundant_false.sh
new file mode 100644
index 0000000..f35d9b8
--- /dev/null
+++ b/tests/gnu/comma_redundant_false.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_false) -false) <==> lhs
+bfs_diff basic -print -not -prune , -false
diff --git a/tests/test_printf_leak.out b/tests/gnu/comma_redundant_true.out
index 15a13db..15a13db 100644
--- a/tests/test_printf_leak.out
+++ b/tests/gnu/comma_redundant_true.out
diff --git a/tests/gnu/comma_redundant_true.sh b/tests/gnu/comma_redundant_true.sh
new file mode 100644
index 0000000..f9eef57
--- /dev/null
+++ b/tests/gnu/comma_redundant_true.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_true) -true) <==> lhs
+bfs_diff basic -prune , -true
diff --git a/tests/gnu/daystart.out b/tests/gnu/daystart.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/daystart.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/daystart.sh b/tests/gnu/daystart.sh
new file mode 100644
index 0000000..9c3be1a
--- /dev/null
+++ b/tests/gnu/daystart.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -mtime 0
diff --git a/tests/gnu/daystart_twice.out b/tests/gnu/daystart_twice.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/daystart_twice.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/daystart_twice.sh b/tests/gnu/daystart_twice.sh
new file mode 100644
index 0000000..edbf18d
--- /dev/null
+++ b/tests/gnu/daystart_twice.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -daystart -mtime 0
diff --git a/tests/gnu/exec_flush.out b/tests/gnu/exec_flush.out
new file mode 100644
index 0000000..fdd7b16
--- /dev/null
+++ b/tests/gnu/exec_flush.out
@@ -0,0 +1,19 @@
+basic found
+basic/a found
+basic/b found
+basic/c found
+basic/c/d found
+basic/e found
+basic/e/f found
+basic/g found
+basic/g/h found
+basic/i found
+basic/j found
+basic/j/foo found
+basic/k found
+basic/k/foo found
+basic/k/foo/bar found
+basic/l found
+basic/l/foo found
+basic/l/foo/bar found
+basic/l/foo/bar/baz found
diff --git a/tests/gnu/exec_flush.sh b/tests/gnu/exec_flush.sh
new file mode 100644
index 0000000..ff6088e
--- /dev/null
+++ b/tests/gnu/exec_flush.sh
@@ -0,0 +1,4 @@
+# I/O streams should be flushed before executing programs
+invoke_bfs basic -print0 -exec echo found \; | tr '\0' ' ' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/exec_flush_fail.sh b/tests/gnu/exec_flush_fail.sh
new file mode 100644
index 0000000..5505f7a
--- /dev/null
+++ b/tests/gnu/exec_flush_fail.sh
@@ -0,0 +1,3 @@
+# Failure to flush streams before exec should be caught
+test -e /dev/full || skip
+! invoke_bfs basic -print0 -exec true \; >/dev/full
diff --git a/tests/gnu/exec_nothing.sh b/tests/gnu/exec_nothing.sh
new file mode 100644
index 0000000..443aa0d
--- /dev/null
+++ b/tests/gnu/exec_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -exec \;
diff --git a/tests/gnu/exec_plus_flush.out b/tests/gnu/exec_plus_flush.out
new file mode 100644
index 0000000..3e276be
--- /dev/null
+++ b/tests/gnu/exec_plus_flush.out
Binary files differ
diff --git a/tests/gnu/exec_plus_flush.sh b/tests/gnu/exec_plus_flush.sh
new file mode 100644
index 0000000..0c03837
--- /dev/null
+++ b/tests/gnu/exec_plus_flush.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a -print0 -exec echo found {} + >"$OUT"
+diff_output
diff --git a/tests/gnu/exec_plus_flush_fail.sh b/tests/gnu/exec_plus_flush_fail.sh
new file mode 100644
index 0000000..53a50e5
--- /dev/null
+++ b/tests/gnu/exec_plus_flush_fail.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full
diff --git a/tests/gnu/execdir.out b/tests/gnu/execdir.out
new file mode 100644
index 0000000..62b31f6
--- /dev/null
+++ b/tests/gnu/execdir.out
@@ -0,0 +1,19 @@
+./a
+./b
+./bar
+./bar
+./basic
+./baz
+./c
+./d
+./e
+./f
+./foo
+./foo
+./foo
+./g
+./h
+./i
+./j
+./k
+./l
diff --git a/tests/gnu/execdir.sh b/tests/gnu/execdir.sh
new file mode 100644
index 0000000..5a3a95a
--- /dev/null
+++ b/tests/gnu/execdir.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo {} \;
diff --git a/tests/gnu/execdir_path_dot.sh b/tests/gnu/execdir_path_dot.sh
new file mode 100644
index 0000000..632dbb4
--- /dev/null
+++ b/tests/gnu/execdir_path_dot.sh
@@ -0,0 +1 @@
+! PATH=".:$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/gnu/execdir_path_empty.sh b/tests/gnu/execdir_path_empty.sh
new file mode 100644
index 0000000..eda6b1c
--- /dev/null
+++ b/tests/gnu/execdir_path_empty.sh
@@ -0,0 +1 @@
+! PATH=":$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/gnu/execdir_path_relative.sh b/tests/gnu/execdir_path_relative.sh
new file mode 100644
index 0000000..69899ad
--- /dev/null
+++ b/tests/gnu/execdir_path_relative.sh
@@ -0,0 +1 @@
+! PATH="foo:$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/test_execdir_plus_semicolon.out b/tests/gnu/execdir_plus_semicolon.out
index e39f452..e39f452 100644
--- a/tests/test_execdir_plus_semicolon.out
+++ b/tests/gnu/execdir_plus_semicolon.out
diff --git a/tests/gnu/execdir_plus_semicolon.sh b/tests/gnu/execdir_plus_semicolon.sh
new file mode 100644
index 0000000..c5cdafe
--- /dev/null
+++ b/tests/gnu/execdir_plus_semicolon.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo foo {} bar + baz \;
diff --git a/tests/gnu/execdir_self.out b/tests/gnu/execdir_self.out
new file mode 100644
index 0000000..3ad0640
--- /dev/null
+++ b/tests/gnu/execdir_self.out
@@ -0,0 +1 @@
+./bar.sh
diff --git a/tests/gnu/execdir_self.sh b/tests/gnu/execdir_self.sh
new file mode 100644
index 0000000..1fc5d04
--- /dev/null
+++ b/tests/gnu/execdir_self.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+mkdir foo
+cat >foo/bar.sh <<EOF
+#!/bin/sh
+printf '%s\n' "\$@"
+EOF
+chmod +x foo/bar.sh
+
+bfs_diff . -name bar.sh -execdir {} {} \;
diff --git a/tests/test_execdir_substring.out b/tests/gnu/execdir_substring.out
index f7a9ac0..f7a9ac0 100644
--- a/tests/test_execdir_substring.out
+++ b/tests/gnu/execdir_substring.out
diff --git a/tests/gnu/execdir_substring.sh b/tests/gnu/execdir_substring.sh
new file mode 100644
index 0000000..feeabc4
--- /dev/null
+++ b/tests/gnu/execdir_substring.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo '-{}-' \;
diff --git a/tests/gnu/execdir_ulimit.out b/tests/gnu/execdir_ulimit.out
new file mode 100644
index 0000000..6749f7d
--- /dev/null
+++ b/tests/gnu/execdir_ulimit.out
@@ -0,0 +1,16 @@
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/gnu/execdir_ulimit.sh b/tests/gnu/execdir_ulimit.sh
new file mode 100644
index 0000000..e14e716
--- /dev/null
+++ b/tests/gnu/execdir_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -Sn 64
+bfs_diff deep -type f -execdir bash -c 'printf "%d %s\n" $(ulimit -Sn) "$1"' bash {} \;
diff --git a/tests/gnu/executable.out b/tests/gnu/executable.out
new file mode 100644
index 0000000..e256554
--- /dev/null
+++ b/tests/gnu/executable.out
@@ -0,0 +1,19 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f--x------
+perms/f--x--x--x
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/executable.sh b/tests/gnu/executable.sh
new file mode 100644
index 0000000..f7f6633
--- /dev/null
+++ b/tests/gnu/executable.sh
@@ -0,0 +1 @@
+bfs_diff perms -executable
diff --git a/tests/test_or_purity.out b/tests/gnu/false.out
index e69de29..e69de29 100644
--- a/tests/test_or_purity.out
+++ b/tests/gnu/false.out
diff --git a/tests/gnu/false.sh b/tests/gnu/false.sh
new file mode 100644
index 0000000..89d86c2
--- /dev/null
+++ b/tests/gnu/false.sh
@@ -0,0 +1 @@
+bfs_diff basic -false
diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh
new file mode 100644
index 0000000..7b42772
--- /dev/null
+++ b/tests/gnu/files0_from_empty.sh
@@ -0,0 +1 @@
+! printf '\0' | invoke_bfs -files0-from -
diff --git a/tests/gnu/files0_from_error.sh b/tests/gnu/files0_from_error.sh
new file mode 100644
index 0000000..1515d0b
--- /dev/null
+++ b/tests/gnu/files0_from_error.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic
diff --git a/tests/test_files0_from_stdin.out b/tests/gnu/files0_from_file.out
index 3648854..0f6b00d 100644
--- a/tests/test_files0_from_stdin.out
+++ b/tests/gnu/files0_from_file.out
@@ -1,30 +1,45 @@
+
+
+
+
-!
-!-
-(
-(-
-)
-,
--
-...
-\
/j
/j
+!
+!-
!-/e
!-/e
!/d
!/d
+(
+(-
(-/c
(-/c
(/b
(/b
+)
)/g
)/g
+*
+*/m
+*/m
+,
,/f
,/f
+-
-/a
-/a
+...
.../h
.../h
+/n
+/n
+[
+[/k
+[/k
+\
\/i
\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_file.sh b/tests/gnu/files0_from_file.sh
new file mode 100644
index 0000000..81435a0
--- /dev/null
+++ b/tests/gnu/files0_from_file.sh
@@ -0,0 +1,4 @@
+FILE="$TMP/$TEST.in"
+cd weirdnames
+invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0"
+bfs_diff -files0-from "$FILE"
diff --git a/tests/gnu/files0_from_file_file.out b/tests/gnu/files0_from_file_file.out
new file mode 100644
index 0000000..fb683c7
--- /dev/null
+++ b/tests/gnu/files0_from_file_file.out
@@ -0,0 +1,2 @@
+basic/g
+basic/g/h
diff --git a/tests/gnu/files0_from_file_file.sh b/tests/gnu/files0_from_file_file.sh
new file mode 100644
index 0000000..1119952
--- /dev/null
+++ b/tests/gnu/files0_from_file_file.sh
@@ -0,0 +1,3 @@
+printf 'basic/c\0' >"$TEST/in1"
+printf 'basic/g\0' >"$TEST/in2"
+bfs_diff -files0-from "$TEST/in1" -files0-from "$TEST/in2"
diff --git a/tests/test_perm_leading_plus_symbolic.out b/tests/gnu/files0_from_none.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic.out
+++ b/tests/gnu/files0_from_none.out
diff --git a/tests/gnu/files0_from_none.sh b/tests/gnu/files0_from_none.sh
new file mode 100644
index 0000000..1633163
--- /dev/null
+++ b/tests/gnu/files0_from_none.sh
@@ -0,0 +1 @@
+printf "" | bfs_diff -files0-from -
diff --git a/tests/gnu/files0_from_nothing.sh b/tests/gnu/files0_from_nothing.sh
new file mode 100644
index 0000000..fee50a8
--- /dev/null
+++ b/tests/gnu/files0_from_nothing.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic/nonexistent
diff --git a/tests/gnu/files0_from_nowhere.sh b/tests/gnu/files0_from_nowhere.sh
new file mode 100644
index 0000000..68eea4b
--- /dev/null
+++ b/tests/gnu/files0_from_nowhere.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from
diff --git a/tests/test_files0_from_file.out b/tests/gnu/files0_from_stdin.out
index 3648854..0f6b00d 100644
--- a/tests/test_files0_from_file.out
+++ b/tests/gnu/files0_from_stdin.out
@@ -1,30 +1,45 @@
+
+
+
+
-!
-!-
-(
-(-
-)
-,
--
-...
-\
/j
/j
+!
+!-
!-/e
!-/e
!/d
!/d
+(
+(-
(-/c
(-/c
(/b
(/b
+)
)/g
)/g
+*
+*/m
+*/m
+,
,/f
,/f
+-
-/a
-/a
+...
.../h
.../h
+/n
+/n
+[
+[/k
+[/k
+\
\/i
\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin.sh b/tests/gnu/files0_from_stdin.sh
new file mode 100644
index 0000000..9df7736
--- /dev/null
+++ b/tests/gnu/files0_from_stdin.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from -
diff --git a/tests/gnu/files0_from_stdin_ok.sh b/tests/gnu/files0_from_stdin_ok.sh
new file mode 100644
index 0000000..0283c8d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok.sh
@@ -0,0 +1 @@
+! printf 'basic\0' | invoke_bfs -files0-from - -ok echo {} \;
diff --git a/tests/gnu/files0_from_stdin_ok_file.out b/tests/gnu/files0_from_stdin_ok_file.out
new file mode 100644
index 0000000..0f6b00d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok_file.out
@@ -0,0 +1,45 @@
+
+
+
+
+
+ /j
+ /j
+!
+!-
+!-/e
+!-/e
+!/d
+!/d
+(
+(-
+(-/c
+(-/c
+(/b
+(/b
+)
+)/g
+)/g
+*
+*/m
+*/m
+,
+,/f
+,/f
+-
+-/a
+-/a
+...
+.../h
+.../h
+/n
+/n
+[
+[/k
+[/k
+\
+\/i
+\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin_ok_file.sh b/tests/gnu/files0_from_stdin_ok_file.sh
new file mode 100644
index 0000000..028df0c
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok_file.sh
@@ -0,0 +1,4 @@
+FILE="$TMP/$TEST.in"
+cd weirdnames
+invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0"
+yes | bfs_diff -files0-from - -ok printf '%s\n' {} \; -files0-from "$FILE"
diff --git a/tests/gnu/files0_from_stdin_stdin.out b/tests/gnu/files0_from_stdin_stdin.out
new file mode 100644
index 0000000..0f6b00d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_stdin.out
@@ -0,0 +1,45 @@
+
+
+
+
+
+ /j
+ /j
+!
+!-
+!-/e
+!-/e
+!/d
+!/d
+(
+(-
+(-/c
+(-/c
+(/b
+(/b
+)
+)/g
+)/g
+*
+*/m
+*/m
+,
+,/f
+,/f
+-
+-/a
+-/a
+...
+.../h
+.../h
+/n
+/n
+[
+[/k
+[/k
+\
+\/i
+\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin_stdin.sh b/tests/gnu/files0_from_stdin_stdin.sh
new file mode 100644
index 0000000..8f6368f
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_stdin.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -files0-from -
diff --git a/tests/gnu/fls.sh b/tests/gnu/fls.sh
new file mode 100644
index 0000000..d2ff794
--- /dev/null
+++ b/tests/gnu/fls.sh
@@ -0,0 +1 @@
+invoke_bfs rainbow -fls "$OUT"
diff --git a/tests/gnu/fls_nonexistent.sh b/tests/gnu/fls_nonexistent.sh
new file mode 100644
index 0000000..2854569
--- /dev/null
+++ b/tests/gnu/fls_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs rainbow -fls nonexistent/path
diff --git a/tests/gnu/fls_overflow.sh b/tests/gnu/fls_overflow.sh
new file mode 100644
index 0000000..067bc86
--- /dev/null
+++ b/tests/gnu/fls_overflow.sh
@@ -0,0 +1,4 @@
+# Regression test: times that overflow localtime() should still print
+cd "$TEST"
+"$XTOUCH" -t "@1111111111111111111" overflow || skip
+invoke_bfs . -fls "$OUT"
diff --git a/tests/test_follow_comma.out b/tests/gnu/follow_comma.out
index 8b90e76..5e4b806 100644
--- a/tests/test_follow_comma.out
+++ b/tests/gnu/follow_comma.out
@@ -1,21 +1,31 @@
+
.
+./
+./
./
+./ /j
./!
./!-
-./(
-./(-
-./)
-./,
-./-
-./...
-./\
-./ /j
./!-/e
./!/d
+./(
+./(-
./(-/c
./(/b
+./)
./)/g
+./*
+./*/m
+./,
./,/f
+./-
./-/a
+./...
./.../h
+./[
+./[/k
+./\
./\/i
+./{
+./{/l
+/n
diff --git a/tests/gnu/follow_comma.sh b/tests/gnu/follow_comma.sh
new file mode 100644
index 0000000..f57b932
--- /dev/null
+++ b/tests/gnu/follow_comma.sh
@@ -0,0 +1,3 @@
+# , is an operator after a non-flag is seen
+cd weirdnames
+bfs_diff -follow ',' -print
diff --git a/tests/gnu/follow_files0_from.out b/tests/gnu/follow_files0_from.out
new file mode 100644
index 0000000..c77d546
--- /dev/null
+++ b/tests/gnu/follow_files0_from.out
@@ -0,0 +1,42 @@
+links
+links/broken
+links/broken
+links/deeply
+links/deeply
+links/deeply/nested
+links/deeply/nested
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/link
+links/deeply/nested/link
+links/deeply/nested/link
+links/deeply/nested/link
+links/file
+links/file
+links/hardlink
+links/hardlink
+links/notdir
+links/notdir
+links/skip
+links/skip
+links/skip/broken
+links/skip/broken
+links/skip/dir
+links/skip/dir
+links/skip/file
+links/skip/file
+links/skip/link
+links/skip/link
+links/symlink
+links/symlink
diff --git a/tests/gnu/follow_files0_from.sh b/tests/gnu/follow_files0_from.sh
new file mode 100644
index 0000000..8c20f6d
--- /dev/null
+++ b/tests/gnu/follow_files0_from.sh
@@ -0,0 +1 @@
+invoke_bfs links -print0 | bfs_diff -follow -files0-from -
diff --git a/tests/gnu/fprint.out b/tests/gnu/fprint.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/fprint.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/fprint.sh b/tests/gnu/fprint.sh
new file mode 100644
index 0000000..04b50fa
--- /dev/null
+++ b/tests/gnu/fprint.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprint0.out b/tests/gnu/fprint0.out
index 1347444..1347444 100644
--- a/tests/test_fprint0.out
+++ b/tests/gnu/fprint0.out
Binary files differ
diff --git a/tests/gnu/fprint0.sh b/tests/gnu/fprint0.sh
new file mode 100644
index 0000000..dd10b5f
--- /dev/null
+++ b/tests/gnu/fprint0.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a basic/b -fprint0 "$OUT"
+diff_output
diff --git a/tests/gnu/fprint0_nonexistent.sh b/tests/gnu/fprint0_nonexistent.sh
new file mode 100644
index 0000000..4906081
--- /dev/null
+++ b/tests/gnu/fprint0_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint0 nonexistent/path
diff --git a/tests/test_fprint_duplicate.out b/tests/gnu/fprint_duplicate.out
index 2575f35..2575f35 100644
--- a/tests/test_fprint_duplicate.out
+++ b/tests/gnu/fprint_duplicate.out
diff --git a/tests/gnu/fprint_duplicate.sh b/tests/gnu/fprint_duplicate.sh
new file mode 100644
index 0000000..8533b05
--- /dev/null
+++ b/tests/gnu/fprint_duplicate.sh
@@ -0,0 +1,7 @@
+"$XTOUCH" -p "$TEST/foo.out"
+ln "$TEST/foo.out" "$TEST/foo.hard"
+ln -s foo.out "$TEST/foo.soft"
+
+invoke_bfs basic -fprint "$TEST/foo.out" -fprint "$TEST/foo.hard" -fprint "$TEST/foo.soft"
+sort "$TEST/foo.out" >"$OUT"
+diff_output
diff --git a/tests/gnu/fprint_error.sh b/tests/gnu/fprint_error.sh
new file mode 100644
index 0000000..7617034
--- /dev/null
+++ b/tests/gnu/fprint_error.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full
diff --git a/tests/gnu/fprint_noarg.sh b/tests/gnu/fprint_noarg.sh
new file mode 100644
index 0000000..8511649
--- /dev/null
+++ b/tests/gnu/fprint_noarg.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint
diff --git a/tests/gnu/fprint_nonexistent.sh b/tests/gnu/fprint_nonexistent.sh
new file mode 100644
index 0000000..2a403a2
--- /dev/null
+++ b/tests/gnu/fprint_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint nonexistent/path
diff --git a/tests/test_quit_after_print.out b/tests/gnu/fprint_truncate.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_after_print.out
+++ b/tests/gnu/fprint_truncate.out
diff --git a/tests/gnu/fprint_truncate.sh b/tests/gnu/fprint_truncate.sh
new file mode 100644
index 0000000..db58a7a
--- /dev/null
+++ b/tests/gnu/fprint_truncate.sh
@@ -0,0 +1,5 @@
+printf "basic\nbasic\n" >"$OUT"
+
+invoke_bfs basic -maxdepth 0 -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/fprint_unreached_error.sh b/tests/gnu/fprint_unreached_error.sh
new file mode 100644
index 0000000..f13a62b
--- /dev/null
+++ b/tests/gnu/fprint_unreached_error.sh
@@ -0,0 +1,3 @@
+# Regression test: /dev/full should not fail until actually written to
+test -e /dev/full || skip
+invoke_bfs basic -false -fprint /dev/full
diff --git a/tests/test_fprintf.out b/tests/gnu/fprintf.out
index 77ce17a..77ce17a 100644
--- a/tests/test_fprintf.out
+++ b/tests/gnu/fprintf.out
diff --git a/tests/gnu/fprintf.sh b/tests/gnu/fprintf.sh
new file mode 100644
index 0000000..9c6355a
--- /dev/null
+++ b/tests/gnu/fprintf.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
+sort_output
+diff_output
diff --git a/tests/gnu/fprintf_nofile.sh b/tests/gnu/fprintf_nofile.sh
new file mode 100644
index 0000000..4e79002
--- /dev/null
+++ b/tests/gnu/fprintf_nofile.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf
diff --git a/tests/gnu/fprintf_noformat.sh b/tests/gnu/fprintf_noformat.sh
new file mode 100644
index 0000000..fd97f4c
--- /dev/null
+++ b/tests/gnu/fprintf_noformat.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf /dev/null
diff --git a/tests/gnu/fprintf_nonexistent.sh b/tests/gnu/fprintf_nonexistent.sh
new file mode 100644
index 0000000..b1eea10
--- /dev/null
+++ b/tests/gnu/fprintf_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf nonexistent/path '%p\n'
diff --git a/tests/gnu/fstype.out b/tests/gnu/fstype.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/fstype.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/fstype.sh b/tests/gnu/fstype.sh
new file mode 100644
index 0000000..05645c3
--- /dev/null
+++ b/tests/gnu/fstype.sh
@@ -0,0 +1,2 @@
+fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') || skip
+bfs_diff basic -fstype "$fstype"
diff --git a/tests/gnu/fstype_btrfs_subvol.out b/tests/gnu/fstype_btrfs_subvol.out
new file mode 100644
index 0000000..8871fb9
--- /dev/null
+++ b/tests/gnu/fstype_btrfs_subvol.out
@@ -0,0 +1,4 @@
+mnt
+mnt/file
+mnt/subvol
+mnt/subvol/file
diff --git a/tests/gnu/fstype_btrfs_subvol.sh b/tests/gnu/fstype_btrfs_subvol.sh
new file mode 100644
index 0000000..71df45c
--- /dev/null
+++ b/tests/gnu/fstype_btrfs_subvol.sh
@@ -0,0 +1,25 @@
+# Test that -fstype works in btrfs subvolumes
+
+command -v btrfs &>/dev/null || skip
+
+cd "$TEST"
+
+# Make a btrfs filesystem image
+truncate -s128M img
+mkfs.btrfs img >&2
+
+# Mount it
+mkdir mnt
+bfs_sudo mount img mnt || skip
+defer bfs_sudo umount mnt
+
+# Make it owned by us
+bfs_sudo chown "$(id -u):$(id -g)" mnt
+
+# Create a subvolume inside it
+btrfs subvolume create mnt/subvol >&2
+
+# Make a file in and outside the subvolume
+"$XTOUCH" mnt/file mnt/subvol/file
+
+bfs_diff mnt -fstype btrfs -print -o -printf '%p %F\n'
diff --git a/tests/gnu/fstype_stacked.out b/tests/gnu/fstype_stacked.out
new file mode 100644
index 0000000..c1e0e6c
--- /dev/null
+++ b/tests/gnu/fstype_stacked.out
@@ -0,0 +1 @@
+mnt
diff --git a/tests/gnu/fstype_stacked.sh b/tests/gnu/fstype_stacked.sh
new file mode 100644
index 0000000..a9739bb
--- /dev/null
+++ b/tests/gnu/fstype_stacked.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_sudo mount -t ramfs ramfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff mnt -fstype ramfs -print -o -printf '%p: %F\n'
diff --git a/tests/test_perm_leading_plus_symbolic_minus.out b/tests/gnu/fstype_umount.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic_minus.out
+++ b/tests/gnu/fstype_umount.out
diff --git a/tests/gnu/fstype_umount.sh b/tests/gnu/fstype_umount.sh
new file mode 100644
index 0000000..81c195f
--- /dev/null
+++ b/tests/gnu/fstype_umount.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+
+mkdir tmp
+bfs_sudo mount -t tmpfs tmpfs tmp || skip
+defer bfs_sudo umount -R tmp
+
+mkdir tmp/ram
+bfs_sudo mount -t ramfs ramfs tmp/ram || skip
+
+bfs_diff tmp -path tmp -exec "${SUDO[@]}" umount tmp/ram \; , -fstype ramfs -print
diff --git a/tests/gnu/ignore_readdir_race.sh b/tests/gnu/ignore_readdir_race.sh
new file mode 100644
index 0000000..75165f6
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race.sh
@@ -0,0 +1,5 @@
+cd "$TEST"
+"$XTOUCH" foo bar
+
+# -links 1 forces a stat() call, which will fail for the second file
+invoke_bfs . -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \;
diff --git a/tests/gnu/ignore_readdir_race_loop.out b/tests/gnu/ignore_readdir_race_loop.out
new file mode 100644
index 0000000..a514555
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_loop.out
@@ -0,0 +1,11 @@
+loops
+loops/broken
+loops/deeply
+loops/deeply/nested
+loops/deeply/nested/dir
+loops/file
+loops/notdir
+loops/skip
+loops/skip/dir
+loops/skip/loop
+loops/symlink
diff --git a/tests/gnu/ignore_readdir_race_loop.sh b/tests/gnu/ignore_readdir_race_loop.sh
new file mode 100644
index 0000000..3329169
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_loop.sh
@@ -0,0 +1,2 @@
+# Make sure -ignore_readdir_race doesn't suppress ELOOP from an actual filesystem loop
+! bfs_diff -L loops -ignore_readdir_race
diff --git a/tests/gnu/ignore_readdir_race_notdir.sh b/tests/gnu/ignore_readdir_race_notdir.sh
new file mode 100644
index 0000000..12e9fe6
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_notdir.sh
@@ -0,0 +1,7 @@
+# Check -ignore_readdir_race handling when a directory is replaced with a file
+cd "$TEST"
+mkdir foo
+
+invoke_bfs . -mindepth 1 -ignore_readdir_race \
+ -type d -execdir rmdir {} \; \
+ -execdir "$XTOUCH" {} \;
diff --git a/tests/gnu/ignore_readdir_race_rmdir.out b/tests/gnu/ignore_readdir_race_rmdir.out
new file mode 100644
index 0000000..ede8749
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_rmdir.out
@@ -0,0 +1,2 @@
+./bar
+./foo
diff --git a/tests/gnu/ignore_readdir_race_rmdir.sh b/tests/gnu/ignore_readdir_race_rmdir.sh
new file mode 100644
index 0000000..87f36a9
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_rmdir.sh
@@ -0,0 +1,5 @@
+cd "$TEST"
+"$XTOUCH" -p foo/ bar/
+
+# Check that -ignore_readdir_race suppresses errors from opendir()
+bfs_diff . -ignore_readdir_race -mindepth 1 -print -name foo -exec rmdir {} \;
diff --git a/tests/gnu/ignore_readdir_race_root.sh b/tests/gnu/ignore_readdir_race_root.sh
new file mode 100644
index 0000000..dc41e7f
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_root.sh
@@ -0,0 +1,2 @@
+# Make sure -ignore_readdir_race doesn't suppress ENOENT at the root
+! invoke_bfs basic/nonexistent -ignore_readdir_race
diff --git a/tests/gnu/inum_automount.out b/tests/gnu/inum_automount.out
new file mode 100644
index 0000000..3378e2d
--- /dev/null
+++ b/tests/gnu/inum_automount.out
@@ -0,0 +1 @@
+./automnt
diff --git a/tests/gnu/inum_automount.sh b/tests/gnu/inum_automount.sh
new file mode 100644
index 0000000..86b23e1
--- /dev/null
+++ b/tests/gnu/inum_automount.sh
@@ -0,0 +1,14 @@
+# bfs shouldn't trigger automounts unless it descends into them
+
+command -v systemd-mount &>/dev/null || skip
+
+cd "$TEST"
+mkdir foo automnt
+
+bfs_sudo systemd-mount -A -o bind "$TMP/basic" automnt || skip
+defer bfs_sudo systemd-umount automnt
+
+before=$(inum automnt)
+bfs_diff . -inum "$before" -prune
+after=$(inum automnt)
+((before == after))
diff --git a/tests/test_ipath.out b/tests/gnu/iwholename.out
index 0d36df9..ae1ae21 100644
--- a/tests/test_ipath.out
+++ b/tests/gnu/iwholename.out
@@ -1,7 +1,7 @@
basic/e/f
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/iwholename.sh b/tests/gnu/iwholename.sh
new file mode 100644
index 0000000..0b2d038
--- /dev/null
+++ b/tests/gnu/iwholename.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -iwholename PATTERN || skip
+bfs_diff basic -iwholename 'basic/*F*'
diff --git a/tests/test_newer_link.out b/tests/gnu/newer_link.out
index d2dcdd1..d2dcdd1 100644
--- a/tests/test_newer_link.out
+++ b/tests/gnu/newer_link.out
diff --git a/tests/gnu/newer_link.sh b/tests/gnu/newer_link.sh
new file mode 100644
index 0000000..685ac78
--- /dev/null
+++ b/tests/gnu/newer_link.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/l
diff --git a/tests/gnu/noleaf.out b/tests/gnu/noleaf.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/noleaf.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/noleaf.sh b/tests/gnu/noleaf.sh
new file mode 100644
index 0000000..b19438c
--- /dev/null
+++ b/tests/gnu/noleaf.sh
@@ -0,0 +1 @@
+bfs_diff basic -noleaf
diff --git a/tests/test_bang.out b/tests/gnu/not.out
index 2501b2f..b286454 100644
--- a/tests/test_bang.out
+++ b/tests/gnu/not.out
@@ -2,15 +2,15 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo/bar
+basic/l
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/not.sh b/tests/gnu/not.sh
new file mode 100644
index 0000000..9fa9edc
--- /dev/null
+++ b/tests/gnu/not.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo
diff --git a/tests/gnu/not_comma.out b/tests/gnu/not_comma.out
new file mode 100644
index 0000000..b90468e
--- /dev/null
+++ b/tests/gnu/not_comma.out
@@ -0,0 +1,34 @@
+basic
+basic
+basic/a
+basic/a
+basic/b
+basic/b
+basic/c
+basic/c
+basic/c/d
+basic/c/d
+basic/e
+basic/e
+basic/e/f
+basic/g
+basic/g
+basic/g/h
+basic/g/h
+basic/i
+basic/i
+basic/j
+basic/j
+basic/j/foo
+basic/k
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/k/foo/bar
+basic/l
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar
+basic/l/foo/bar/baz
+basic/l/foo/bar/baz
diff --git a/tests/gnu/not_comma.sh b/tests/gnu/not_comma.sh
new file mode 100644
index 0000000..04c0195
--- /dev/null
+++ b/tests/gnu/not_comma.sh
@@ -0,0 +1,2 @@
+# Regression test: assertion failure in sink_not_comma()
+bfs_diff basic -not \( -print , -name '*f*' \) -print
diff --git a/tests/test_quit_implicit_print.out b/tests/gnu/not_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_implicit_print.out
+++ b/tests/gnu/not_reachability.out
diff --git a/tests/gnu/not_reachability.sh b/tests/gnu/not_reachability.sh
new file mode 100644
index 0000000..7fd3c74
--- /dev/null
+++ b/tests/gnu/not_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print \! -quit -print
diff --git a/tests/gnu/ok_files0_from_stdin.sh b/tests/gnu/ok_files0_from_stdin.sh
new file mode 100644
index 0000000..2c4de7b
--- /dev/null
+++ b/tests/gnu/ok_files0_from_stdin.sh
@@ -0,0 +1 @@
+! printf 'basic\0' | invoke_bfs -ok echo {} \; -files0-from -
diff --git a/tests/gnu/ok_flush.out b/tests/gnu/ok_flush.out
new file mode 100644
index 0000000..6731408
--- /dev/null
+++ b/tests/gnu/ok_flush.out
@@ -0,0 +1,19 @@
+basic ? found
+basic/a ? found
+basic/b ? found
+basic/c ? found
+basic/c/d ? found
+basic/e ? found
+basic/e/f ? found
+basic/g ? found
+basic/g/h ? found
+basic/i ? found
+basic/j ? found
+basic/j/foo ? found
+basic/k ? found
+basic/k/foo ? found
+basic/k/foo/bar ? found
+basic/l ? found
+basic/l/foo ? found
+basic/l/foo/bar ? found
+basic/l/foo/bar/baz ? found
diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh
new file mode 100644
index 0000000..a5dc0d0
--- /dev/null
+++ b/tests/gnu/ok_flush.sh
@@ -0,0 +1,4 @@
+# I/O streams should be flushed before -ok prompts
+yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/ok_nothing.sh b/tests/gnu/ok_nothing.sh
new file mode 100644
index 0000000..52c3547
--- /dev/null
+++ b/tests/gnu/ok_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -ok \;
diff --git a/tests/gnu/okdir_path_dot.sh b/tests/gnu/okdir_path_dot.sh
new file mode 100644
index 0000000..5b40e27
--- /dev/null
+++ b/tests/gnu/okdir_path_dot.sh
@@ -0,0 +1 @@
+! PATH=".:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_empty.sh b/tests/gnu/okdir_path_empty.sh
new file mode 100644
index 0000000..2669ee8
--- /dev/null
+++ b/tests/gnu/okdir_path_empty.sh
@@ -0,0 +1 @@
+! PATH=":$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_relative.sh b/tests/gnu/okdir_path_relative.sh
new file mode 100644
index 0000000..05100a1
--- /dev/null
+++ b/tests/gnu/okdir_path_relative.sh
@@ -0,0 +1 @@
+! PATH="foo:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/test_o.out b/tests/gnu/or.out
index 9a82ee2..1650c4d 100644
--- a/tests/test_o.out
+++ b/tests/gnu/or.out
@@ -2,12 +2,12 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/gnu/or.sh b/tests/gnu/or.sh
new file mode 100644
index 0000000..eb28030
--- /dev/null
+++ b/tests/gnu/or.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -or -type d
diff --git a/tests/gnu/path_d.out b/tests/gnu/path_d.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/path_d.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/path_d.sh b/tests/gnu/path_d.sh
new file mode 100644
index 0000000..e546ff3
--- /dev/null
+++ b/tests/gnu/path_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -d
diff --git a/tests/gnu/perm_000_slash.out b/tests/gnu/perm_000_slash.out
new file mode 100644
index 0000000..e279684
--- /dev/null
+++ b/tests/gnu/perm_000_slash.out
@@ -0,0 +1,29 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f---------
+perms/f--x------
+perms/f--x--x--x
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/perm_000_slash.sh b/tests/gnu/perm_000_slash.sh
new file mode 100644
index 0000000..f4b2665
--- /dev/null
+++ b/tests/gnu/perm_000_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /000
diff --git a/tests/gnu/perm_222_slash.out b/tests/gnu/perm_222_slash.out
new file mode 100644
index 0000000..1b6d885
--- /dev/null
+++ b/tests/gnu/perm_222_slash.out
@@ -0,0 +1,20 @@
+perms
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/perm_222_slash.sh b/tests/gnu/perm_222_slash.sh
new file mode 100644
index 0000000..f4be665
--- /dev/null
+++ b/tests/gnu/perm_222_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /222
diff --git a/tests/gnu/perm_644_slash.out b/tests/gnu/perm_644_slash.out
new file mode 100644
index 0000000..eef88ca
--- /dev/null
+++ b/tests/gnu/perm_644_slash.out
@@ -0,0 +1,26 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/perm_644_slash.sh b/tests/gnu/perm_644_slash.sh
new file mode 100644
index 0000000..e883f17
--- /dev/null
+++ b/tests/gnu/perm_644_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /644
diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out
new file mode 100644
index 0000000..fcbf49e
--- /dev/null
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.out
@@ -0,0 +1,28 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f--x------
+perms/f--x--x--x
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.sh b/tests/gnu/perm_leading_plus_symbolic_slash.sh
new file mode 100644
index 0000000..3db27bd
--- /dev/null
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /+rwx
diff --git a/tests/gnu/perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out
new file mode 100644
index 0000000..5a21321
--- /dev/null
+++ b/tests/gnu/perm_symbolic_slash.out
@@ -0,0 +1,24 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--r--r--
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/perm_symbolic_slash.sh b/tests/gnu/perm_symbolic_slash.sh
new file mode 100644
index 0000000..253b14e
--- /dev/null
+++ b/tests/gnu/perm_symbolic_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /a+r,u=wX,g+wX-w
diff --git a/tests/test_precedence.out b/tests/gnu/precedence.out
index b3d7a51..7f589f2 100644
--- a/tests/test_precedence.out
+++ b/tests/gnu/precedence.out
@@ -1,4 +1,4 @@
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar/baz
diff --git a/tests/gnu/precedence.sh b/tests/gnu/precedence.sh
new file mode 100644
index 0000000..b35d160
--- /dev/null
+++ b/tests/gnu/precedence.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print
diff --git a/tests/gnu/print_error.sh b/tests/gnu/print_error.sh
new file mode 100644
index 0000000..bc79637
--- /dev/null
+++ b/tests/gnu/print_error.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 >/dev/full
diff --git a/tests/test_printf.out b/tests/gnu/printf.out
index 801ddbb..77ce17a 100644
--- a/tests/test_printf.out
+++ b/tests/gnu/printf.out
@@ -2,18 +2,18 @@
%p(basic/a) %d(1) %f(a) %h(basic) %H(basic) %P(a) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/b) %d(1) %f(b) %h(basic) %H(basic) %P(b) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/c) %d(1) %f(c) %h(basic) %H(basic) %P(c) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/e) %d(1) %f(e) %h(basic) %H(basic) %P(e) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/g) %d(1) %f(g) %h(basic) %H(basic) %P(g) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/i) %d(1) %f(i) %h(basic) %H(basic) %P(i) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/j) %d(1) %f(j) %h(basic) %H(basic) %P(j) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %m(644) %M(-rw-r--r--) %y(f)
-%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %m(644) %M(-rw-r--r--) %y(f)
-%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %m(644) %M(-rw-r--r--) %y(f)
+%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %m(644) %M(-rw-r--r--) %y(f)
+%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %m(644) %M(-rw-r--r--) %y(f)
diff --git a/tests/gnu/printf.sh b/tests/gnu/printf.sh
new file mode 100644
index 0000000..4dd48e8
--- /dev/null
+++ b/tests/gnu/printf.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
diff --git a/tests/test_printf_H.out b/tests/gnu/printf_H.out
index 116b425..6b605ff 100644
--- a/tests/test_printf_H.out
+++ b/tests/gnu/printf_H.out
@@ -1,32 +1,32 @@
%p(basic) %d(0) %f(basic) %h(.) %H(basic) %P() %y(d)
-%p(links) %d(0) %f(links) %h(.) %H(links) %P() %y(d)
%p(basic/a) %d(1) %f(a) %h(basic) %H(basic) %P(a) %y(f)
%p(basic/b) %d(1) %f(b) %h(basic) %H(basic) %P(b) %y(f)
%p(basic/c) %d(1) %f(c) %h(basic) %H(basic) %P(c) %y(d)
+%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %y(f)
%p(basic/e) %d(1) %f(e) %h(basic) %H(basic) %P(e) %y(d)
+%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %y(f)
%p(basic/g) %d(1) %f(g) %h(basic) %H(basic) %P(g) %y(d)
+%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %y(d)
%p(basic/i) %d(1) %f(i) %h(basic) %H(basic) %P(i) %y(d)
%p(basic/j) %d(1) %f(j) %h(basic) %H(basic) %P(j) %y(d)
+%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %y(f)
%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %y(d)
+%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %y(d)
+%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %y(f)
%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %y(d)
+%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %y(d)
+%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %y(d)
+%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %y(f)
+%p(links) %d(0) %f(links) %h(.) %H(links) %P() %y(d)
%p(links/broken) %d(1) %f(broken) %h(links) %H(links) %P(broken) %y(l)
%p(links/deeply) %d(1) %f(deeply) %h(links) %H(links) %P(deeply) %y(d)
-%p(links/file) %d(1) %f(file) %h(links) %H(links) %P(file) %y(f)
-%p(links/hardlink) %d(1) %f(hardlink) %h(links) %H(links) %P(hardlink) %y(f)
-%p(links/notdir) %d(1) %f(notdir) %h(links) %H(links) %P(notdir) %y(l)
-%p(links/skip) %d(1) %f(skip) %h(links) %H(links) %P(skip) %y(l)
-%p(links/symlink) %d(1) %f(symlink) %h(links) %H(links) %P(symlink) %y(l)
-%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %y(f)
-%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %y(f)
-%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %y(d)
-%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %y(f)
-%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %y(d)
-%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %y(d)
%p(links/deeply/nested) %d(2) %f(nested) %h(links/deeply) %H(links) %P(deeply/nested) %y(d)
-%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %y(f)
-%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %y(d)
%p(links/deeply/nested/broken) %d(3) %f(broken) %h(links/deeply/nested) %H(links) %P(deeply/nested/broken) %y(l)
%p(links/deeply/nested/dir) %d(3) %f(dir) %h(links/deeply/nested) %H(links) %P(deeply/nested/dir) %y(d)
%p(links/deeply/nested/file) %d(3) %f(file) %h(links/deeply/nested) %H(links) %P(deeply/nested/file) %y(f)
%p(links/deeply/nested/link) %d(3) %f(link) %h(links/deeply/nested) %H(links) %P(deeply/nested/link) %y(l)
-%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %y(f)
+%p(links/file) %d(1) %f(file) %h(links) %H(links) %P(file) %y(f)
+%p(links/hardlink) %d(1) %f(hardlink) %h(links) %H(links) %P(hardlink) %y(f)
+%p(links/notdir) %d(1) %f(notdir) %h(links) %H(links) %P(notdir) %y(l)
+%p(links/skip) %d(1) %f(skip) %h(links) %H(links) %P(skip) %y(l)
+%p(links/symlink) %d(1) %f(symlink) %h(links) %H(links) %P(symlink) %y(l)
diff --git a/tests/gnu/printf_H.sh b/tests/gnu/printf_H.sh
new file mode 100644
index 0000000..ddef7e2
--- /dev/null
+++ b/tests/gnu/printf_H.sh
@@ -0,0 +1 @@
+bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n'
diff --git a/tests/gnu/printf_Y_error.out b/tests/gnu/printf_Y_error.out
new file mode 100644
index 0000000..1dd554e
--- /dev/null
+++ b/tests/gnu/printf_Y_error.out
@@ -0,0 +1,3 @@
+(.) () d d
+(./bar) (foo/bar) l ?
+(./foo) () d d
diff --git a/tests/gnu/printf_Y_error.sh b/tests/gnu/printf_Y_error.sh
new file mode 100644
index 0000000..d3130ce
--- /dev/null
+++ b/tests/gnu/printf_Y_error.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+mkdir foo
+ln -s foo/bar bar
+
+chmod -x foo
+defer chmod +x foo
+
+! bfs_diff . -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/test_perm_symbolic.out b/tests/gnu/printf_empty.out
index e69de29..e69de29 100644
--- a/tests/test_perm_symbolic.out
+++ b/tests/gnu/printf_empty.out
diff --git a/tests/gnu/printf_empty.sh b/tests/gnu/printf_empty.sh
new file mode 100644
index 0000000..ed5eb04
--- /dev/null
+++ b/tests/gnu/printf_empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf ''
diff --git a/tests/test_printf_escapes.out b/tests/gnu/printf_escapes.out
index 20ea120..20ea120 100644
--- a/tests/test_printf_escapes.out
+++ b/tests/gnu/printf_escapes.out
diff --git a/tests/gnu/printf_escapes.sh b/tests/gnu/printf_escapes.sh
new file mode 100644
index 0000000..ece7c0e
--- /dev/null
+++ b/tests/gnu/printf_escapes.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo'
diff --git a/tests/test_printf_flags.out b/tests/gnu/printf_flags.out
index 1a92b6e..c2c1f0a 100644
--- a/tests/test_printf_flags.out
+++ b/tests/gnu/printf_flags.out
@@ -2,18 +2,18 @@
|basic/a | +01 0644
|basic/b | +01 0644
|basic/c | +01 0755
+|basic/c/d | +02 0644
|basic/e | +01 0755
+|basic/e/f | +02 0644
|basic/g | +01 0755
+|basic/g/h | +02 0755
|basic/i | +01 0755
|basic/j | +01 0755
-|basic/k | +01 0755
-|basic/l | +01 0755
-|basic/c/d | +02 0644
-|basic/e/f | +02 0644
-|basic/g/h | +02 0755
|basic/j/fo| +02 0644
+|basic/k | +01 0755
|basic/k/fo| +02 0755
|basic/k/fo| +03 0644
+|basic/l | +01 0755
|basic/l/fo| +02 0755
|basic/l/fo| +03 0755
|basic/l/fo| +04 0644
diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh
new file mode 100644
index 0000000..98e8faa
--- /dev/null
+++ b/tests/gnu/printf_flags.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '|%-10.10p| %+03d % #4m\n'
diff --git a/tests/test_printf_l_nonlink.out b/tests/gnu/printf_l_nonlink.out
index 66e2908..30df155 100644
--- a/tests/test_printf_l_nonlink.out
+++ b/tests/gnu/printf_l_nonlink.out
@@ -1,11 +1,11 @@
| links -> |
| links/file -> |
+| links/skip -> deeply/nested |
| links/broken -> nowhere |
| links/deeply -> |
+| links/notdir -> symlink/file |
| links/symlink -> file |
| links/hardlink -> |
-| links/skip -> deeply/nested |
-| links/notdir -> symlink/file |
| links/deeply/nested -> |
| links/deeply/nested/dir -> |
| links/deeply/nested/file -> |
diff --git a/tests/gnu/printf_l_nonlink.sh b/tests/gnu/printf_l_nonlink.sh
new file mode 100644
index 0000000..1c66442
--- /dev/null
+++ b/tests/gnu/printf_l_nonlink.sh
@@ -0,0 +1 @@
+bfs_diff links -printf '| %26p -> %-26l |\n'
diff --git a/tests/gnu/printf_leak.out b/tests/gnu/printf_leak.out
new file mode 100644
index 0000000..15a13db
--- /dev/null
+++ b/tests/gnu/printf_leak.out
@@ -0,0 +1 @@
+basic
diff --git a/tests/gnu/printf_leak.sh b/tests/gnu/printf_leak.sh
new file mode 100644
index 0000000..c4092c7
--- /dev/null
+++ b/tests/gnu/printf_leak.sh
@@ -0,0 +1,2 @@
+# Memory leak regression test
+bfs_diff basic -maxdepth 0 -printf '%p'
diff --git a/tests/gnu/printf_nul.out b/tests/gnu/printf_nul.out
new file mode 100644
index 0000000..fdb6c6b
--- /dev/null
+++ b/tests/gnu/printf_nul.out
Binary files differ
diff --git a/tests/gnu/printf_nul.sh b/tests/gnu/printf_nul.sh
new file mode 100644
index 0000000..0b8b928
--- /dev/null
+++ b/tests/gnu/printf_nul.sh
@@ -0,0 +1,3 @@
+# NUL byte regression test
+invoke_bfs basic/a basic/b -maxdepth 0 -printf '%h\0%f\n' >"$OUT"
+diff_output
diff --git a/tests/test_printf_slash.out b/tests/gnu/printf_slash.out
index 5571971..5571971 100644
--- a/tests/test_printf_slash.out
+++ b/tests/gnu/printf_slash.out
diff --git a/tests/gnu/printf_slash.sh b/tests/gnu/printf_slash.sh
new file mode 100644
index 0000000..b64ff10
--- /dev/null
+++ b/tests/gnu/printf_slash.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_slashes.out b/tests/gnu/printf_slashes.out
index 5571971..5571971 100644
--- a/tests/test_printf_slashes.out
+++ b/tests/gnu/printf_slashes.out
diff --git a/tests/gnu/printf_slashes.sh b/tests/gnu/printf_slashes.sh
new file mode 100644
index 0000000..d56a287
--- /dev/null
+++ b/tests/gnu/printf_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_times.out b/tests/gnu/printf_times.out
index 7e7d256..7e7d256 100644
--- a/tests/test_printf_times.out
+++ b/tests/gnu/printf_times.out
diff --git a/tests/gnu/printf_times.sh b/tests/gnu/printf_times.sh
new file mode 100644
index 0000000..e4f5155
--- /dev/null
+++ b/tests/gnu/printf_times.sh
@@ -0,0 +1 @@
+bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %A@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n'
diff --git a/tests/test_printf_trailing_slash.out b/tests/gnu/printf_trailing_slash.out
index 0aa4ffc..017ac0d 100644
--- a/tests/test_printf_trailing_slash.out
+++ b/tests/gnu/printf_trailing_slash.out
@@ -1,3 +1,4 @@
+(.)/(basic/)
(basic)/(a)
(basic)/(b)
(basic)/(c)
@@ -7,13 +8,12 @@
(basic)/(j)
(basic)/(k)
(basic)/(l)
-(.)/(basic/)
(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)
(basic/l/foo)/(bar)
(basic/l/foo/bar)/(baz)
diff --git a/tests/gnu/printf_trailing_slash.sh b/tests/gnu/printf_trailing_slash.sh
new file mode 100644
index 0000000..2df818d
--- /dev/null
+++ b/tests/gnu/printf_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_trailing_slashes.out b/tests/gnu/printf_trailing_slashes.out
index cbb54a8..fd27101 100644
--- a/tests/test_printf_trailing_slashes.out
+++ b/tests/gnu/printf_trailing_slashes.out
@@ -1,3 +1,4 @@
+(.)/(basic///)
(basic//)/(a)
(basic//)/(b)
(basic//)/(c)
@@ -7,13 +8,12 @@
(basic//)/(j)
(basic//)/(k)
(basic//)/(l)
-(.)/(basic///)
(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)
(basic///l/foo)/(bar)
(basic///l/foo/bar)/(baz)
diff --git a/tests/gnu/printf_trailing_slashes.sh b/tests/gnu/printf_trailing_slashes.sh
new file mode 100644
index 0000000..6dc532c
--- /dev/null
+++ b/tests/gnu/printf_trailing_slashes.sh
@@ -0,0 +1 @@
+bfs_diff basic/// -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_types.out b/tests/gnu/printf_types.out
index 9cfe347..8144c7c 100644
--- a/tests/test_printf_types.out
+++ b/tests/gnu/printf_types.out
@@ -1,11 +1,11 @@
(loops) () d d
(loops/broken) (nowhere) l N
(loops/deeply) () d d
+(loops/deeply/nested) () d d
+(loops/deeply/nested/dir) () d d
+(loops/deeply/nested/loop) (../../deeply) l d
(loops/file) () f f
(loops/loop) (loop) l L
-(loops/symlink) (file) l f
-(loops/deeply/nested) () d d
(loops/notdir) (symlink/file) l N
-(loops/deeply/nested/dir) () d d
(loops/skip) (deeply/nested/loop/nested) l d
-(loops/deeply/nested/loop) (../../deeply) l d
+(loops/symlink) (file) l f
diff --git a/tests/gnu/printf_types.sh b/tests/gnu/printf_types.sh
new file mode 100644
index 0000000..6ed1d75
--- /dev/null
+++ b/tests/gnu/printf_types.sh
@@ -0,0 +1 @@
+bfs_diff loops -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh
new file mode 100644
index 0000000..c621b9b
--- /dev/null
+++ b/tests/gnu/printf_u_g_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+[ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
diff --git a/tests/gnu/readable.out b/tests/gnu/readable.out
new file mode 100644
index 0000000..56d1f52
--- /dev/null
+++ b/tests/gnu/readable.out
@@ -0,0 +1,19 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/readable.sh b/tests/gnu/readable.sh
new file mode 100644
index 0000000..a496667
--- /dev/null
+++ b/tests/gnu/readable.sh
@@ -0,0 +1 @@
+bfs_diff perms -readable
diff --git a/tests/gnu/regex_error.sh b/tests/gnu/regex_error.sh
new file mode 100644
index 0000000..4af933f
--- /dev/null
+++ b/tests/gnu/regex_error.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -regex '['
diff --git a/tests/gnu/regex_invalid_utf8.out b/tests/gnu/regex_invalid_utf8.out
new file mode 100644
index 0000000..a133b1a
--- /dev/null
+++ b/tests/gnu/regex_invalid_utf8.out
@@ -0,0 +1 @@
+./â„
diff --git a/tests/gnu/regex_invalid_utf8.sh b/tests/gnu/regex_invalid_utf8.sh
new file mode 100644
index 0000000..7006dcd
--- /dev/null
+++ b/tests/gnu/regex_invalid_utf8.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+
+# Incomplete UTF-8 sequences
+touch $'\xC3' || skip
+touch $'\xE2\x84' || skip
+touch $'\xF0\x9F\x92' || skip
+
+bfs_diff . -regex '\./..'
diff --git a/tests/gnu/regextype_awk.out b/tests/gnu/regextype_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_awk.sh b/tests/gnu/regextype_awk.sh
new file mode 100644
index 0000000..3718473
--- /dev/null
+++ b/tests/gnu/regextype_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype awk -quit || skip
+
+bfs_diff weirdnames -regextype awk -regex '.*/[\[\*]/.*'
diff --git a/tests/test_regextype_posix_basic.out b/tests/gnu/regextype_ed.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_basic.out
+++ b/tests/gnu/regextype_ed.out
diff --git a/tests/gnu/regextype_ed.sh b/tests/gnu/regextype_ed.sh
new file mode 100644
index 0000000..0e92db3
--- /dev/null
+++ b/tests/gnu/regextype_ed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype ed -regex '\./\((\)'
diff --git a/tests/test_printf_empty.out b/tests/gnu/regextype_egrep.out
index e69de29..e69de29 100644
--- a/tests/test_printf_empty.out
+++ b/tests/gnu/regextype_egrep.out
diff --git a/tests/gnu/regextype_egrep.sh b/tests/gnu/regextype_egrep.sh
new file mode 100644
index 0000000..281d9c0
--- /dev/null
+++ b/tests/gnu/regextype_egrep.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype egrep -quit || skip
+
+bfs_diff weirdnames -regextype egrep -regex '*.*/{l'
diff --git a/tests/gnu/regextype_emacs.out b/tests/gnu/regextype_emacs.out
new file mode 100644
index 0000000..95942b4
--- /dev/null
+++ b/tests/gnu/regextype_emacs.out
@@ -0,0 +1,6 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh
new file mode 100644
index 0000000..164d17a
--- /dev/null
+++ b/tests/gnu/regextype_emacs.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype emacs -quit || skip
+
+bfs_diff basic -regextype emacs -regex '.*/\(?:f+o?o?\|bar\)'
diff --git a/tests/gnu/regextype_findutils_default.out b/tests/gnu/regextype_findutils_default.out
new file mode 100644
index 0000000..709a7ba
--- /dev/null
+++ b/tests/gnu/regextype_findutils_default.out
@@ -0,0 +1,3 @@
+/n
+weirdnames/
+weirdnames/*/m
diff --git a/tests/gnu/regextype_findutils_default.sh b/tests/gnu/regextype_findutils_default.sh
new file mode 100644
index 0000000..c870312
--- /dev/null
+++ b/tests/gnu/regextype_findutils_default.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype findutils-default -quit || skip
+
+bfs_diff weirdnames -regextype findutils-default -regex '.*/./\(m\|n\)'
diff --git a/tests/gnu/regextype_gnu_awk.out b/tests/gnu/regextype_gnu_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_gnu_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_gnu_awk.sh b/tests/gnu/regextype_gnu_awk.sh
new file mode 100644
index 0000000..6b66496
--- /dev/null
+++ b/tests/gnu/regextype_gnu_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype gnu-awk -quit || skip
+
+bfs_diff weirdnames -regextype gnu-awk -regex '.*/[\[\*]/(\<.\>)'
diff --git a/tests/test_iname.out b/tests/gnu/regextype_grep.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_iname.out
+++ b/tests/gnu/regextype_grep.out
diff --git a/tests/gnu/regextype_grep.sh b/tests/gnu/regextype_grep.sh
new file mode 100644
index 0000000..0830667
--- /dev/null
+++ b/tests/gnu/regextype_grep.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype grep -quit || skip
+
+bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?'
diff --git a/tests/gnu/regextype_posix_awk.out b/tests/gnu/regextype_posix_awk.out
new file mode 100644
index 0000000..0f32fc4
--- /dev/null
+++ b/tests/gnu/regextype_posix_awk.out
@@ -0,0 +1,2 @@
+weirdnames/*/m
+weirdnames/[/k
diff --git a/tests/gnu/regextype_posix_awk.sh b/tests/gnu/regextype_posix_awk.sh
new file mode 100644
index 0000000..86377d7
--- /dev/null
+++ b/tests/gnu/regextype_posix_awk.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype posix-awk -quit || skip
+
+bfs_diff weirdnames -regextype posix-awk -regex '.*/[\[\*]/.*'
diff --git a/tests/test_regextype_posix_extended.out b/tests/gnu/regextype_posix_basic.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_extended.out
+++ b/tests/gnu/regextype_posix_basic.out
diff --git a/tests/gnu/regextype_posix_basic.sh b/tests/gnu/regextype_posix_basic.sh
new file mode 100644
index 0000000..fa2254c
--- /dev/null
+++ b/tests/gnu/regextype_posix_basic.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-basic -regex '\./\((\)'
diff --git a/tests/gnu/regextype_posix_extended.out b/tests/gnu/regextype_posix_extended.out
new file mode 100644
index 0000000..0f0971e
--- /dev/null
+++ b/tests/gnu/regextype_posix_extended.out
@@ -0,0 +1 @@
+./(
diff --git a/tests/gnu/regextype_posix_extended.sh b/tests/gnu/regextype_posix_extended.sh
new file mode 100644
index 0000000..f82ed65
--- /dev/null
+++ b/tests/gnu/regextype_posix_extended.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-extended -regex '\./(\()'
diff --git a/tests/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/regextype_sed.out b/tests/gnu/regextype_sed.out
new file mode 100644
index 0000000..0f0971e
--- /dev/null
+++ b/tests/gnu/regextype_sed.out
@@ -0,0 +1 @@
+./(
diff --git a/tests/gnu/regextype_sed.sh b/tests/gnu/regextype_sed.sh
new file mode 100644
index 0000000..9ce6f4e
--- /dev/null
+++ b/tests/gnu/regextype_sed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype sed -regex '\./\((\)'
diff --git a/tests/gnu/true.out b/tests/gnu/true.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/true.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/true.sh b/tests/gnu/true.sh
new file mode 100644
index 0000000..65f3254
--- /dev/null
+++ b/tests/gnu/true.sh
@@ -0,0 +1 @@
+bfs_diff basic -true
diff --git a/tests/gnu/used.out b/tests/gnu/used.out
new file mode 100644
index 0000000..647621b
--- /dev/null
+++ b/tests/gnu/used.out
@@ -0,0 +1,4 @@
+-used +7: ./nextyear
+-used 1: ./tomorrow
+-used 2: ./dayafter
+-used 7: ./nextweek
diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh
new file mode 100644
index 0000000..fe0a778
--- /dev/null
+++ b/tests/gnu/used.sh
@@ -0,0 +1,21 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+# -used is always false if atime < ctime
+"$XTOUCH" -at "@$((now - 60 * 60 * 24))" yesterday
+
+# -used rounds up
+"$XTOUCH" -at "@$((now + 60 * 60))" tomorrow
+
+"$XTOUCH" -at "@$((now + 60 * 60 * 25))" dayafter
+
+"$XTOUCH" -at "@$((now + 60 * 60 * (24 * 6 + 1)))" nextweek
+
+"$XTOUCH" -at "@$((now + 60 * 60 * 24 * 365))" nextyear
+
+bfs_diff -mindepth 1 \
+ -a -used 1 -printf '-used 1: %p\n' \
+ -o -used 2 -printf '-used 2: %p\n' \
+ -o -used 7 -printf '-used 7: %p\n' \
+ -o -used +7 -printf '-used +7: %p\n'
diff --git a/tests/gnu/wholename.out b/tests/gnu/wholename.out
new file mode 100644
index 0000000..ae1ae21
--- /dev/null
+++ b/tests/gnu/wholename.out
@@ -0,0 +1,7 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/wholename.sh b/tests/gnu/wholename.sh
new file mode 100644
index 0000000..4c641b8
--- /dev/null
+++ b/tests/gnu/wholename.sh
@@ -0,0 +1 @@
+bfs_diff basic -wholename 'basic/*f*'
diff --git a/tests/gnu/writable.out b/tests/gnu/writable.out
new file mode 100644
index 0000000..1b6d885
--- /dev/null
+++ b/tests/gnu/writable.out
@@ -0,0 +1,20 @@
+perms
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/gnu/writable.sh b/tests/gnu/writable.sh
new file mode 100644
index 0000000..93c666f
--- /dev/null
+++ b/tests/gnu/writable.sh
@@ -0,0 +1 @@
+bfs_diff perms -writable
diff --git a/tests/gnu/xtype_bind_mount.out b/tests/gnu/xtype_bind_mount.out
new file mode 100644
index 0000000..d18d706
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.out
@@ -0,0 +1,2 @@
+./link
+./null
diff --git a/tests/gnu/xtype_bind_mount.sh b/tests/gnu/xtype_bind_mount.sh
new file mode 100644
index 0000000..35fb3f5
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+ln -s /dev/null link
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -xtype c
diff --git a/tests/test_xtype_f.out b/tests/gnu/xtype_f.out
index dbe0c3f..e6ba322 100644
--- a/tests/test_xtype_f.out
+++ b/tests/gnu/xtype_f.out
@@ -1,5 +1,5 @@
+links/deeply/nested/file
+links/deeply/nested/link
links/file
links/hardlink
links/symlink
-links/deeply/nested/file
-links/deeply/nested/link
diff --git a/tests/gnu/xtype_f.sh b/tests/gnu/xtype_f.sh
new file mode 100644
index 0000000..0ea27bb
--- /dev/null
+++ b/tests/gnu/xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f
diff --git a/tests/test_xtype_l.out b/tests/gnu/xtype_l.out
index 62d3ae4..f29c978 100644
--- a/tests/test_xtype_l.out
+++ b/tests/gnu/xtype_l.out
@@ -1,3 +1,3 @@
links/broken
-links/notdir
links/deeply/nested/broken
+links/notdir
diff --git a/tests/gnu/xtype_l.sh b/tests/gnu/xtype_l.sh
new file mode 100644
index 0000000..39c8ea4
--- /dev/null
+++ b/tests/gnu/xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype l
diff --git a/tests/gnu/xtype_l_loops.out b/tests/gnu/xtype_l_loops.out
new file mode 100644
index 0000000..fdaccab
--- /dev/null
+++ b/tests/gnu/xtype_l_loops.out
@@ -0,0 +1,3 @@
+loops/broken
+loops/loop
+loops/notdir
diff --git a/tests/gnu/xtype_l_loops.sh b/tests/gnu/xtype_l_loops.sh
new file mode 100644
index 0000000..6893134
--- /dev/null
+++ b/tests/gnu/xtype_l_loops.sh
@@ -0,0 +1 @@
+bfs_diff loops -xtype l
diff --git a/tests/ioq.c b/tests/ioq.c
new file mode 100644
index 0000000..1a0da97
--- /dev/null
+++ b/tests/ioq.c
@@ -0,0 +1,76 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "diag.h"
+#include "dir.h"
+#include "ioq.h"
+
+#include <fcntl.h>
+#include <stdlib.h>
+
+/**
+ * Test for blocking within ioq_slot_push().
+ *
+ * struct ioqq only supports non-blocking reads; if a write encounters a full
+ * slot, it must block until someone pops from that slot:
+ *
+ * Reader Writer
+ * ────────────────────────── ─────────────────────────
+ * tail: 0 → 1
+ * slots[0]: empty → full
+ * tail: 1 → 0
+ * slots[1]: empty → full
+ * tail: 0 → 1
+ * slots[0]: full → full* (IOQ_BLOCKED)
+ * ioq_slot_wait() ...
+ * head: 0 → 1
+ * slots[0]: full* → empty
+ * ioq_slot_wake()
+ * ...
+ * slots[0]: empty → full
+ *
+ * To reproduce this unlikely scenario, we must fill up the ready queue, then
+ * call ioq_cancel() which pushes an additional sentinel IOQ_STOP operation.
+ */
+static void check_ioq_push_block(void) {
+ // Must be a power of two to fill the entire queue
+ const size_t depth = 2;
+
+ struct ioq *ioq = ioq_create(depth, 1);
+ bfs_everify(ioq, "ioq_create()");
+
+ // Push enough operations to fill the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct bfs_dir *dir = bfs_allocdir();
+ bfs_everify(dir, "bfs_allocdir()");
+
+ int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL);
+ bfs_everify(ret == 0, "ioq_opendir()");
+ }
+ ioq_submit(ioq);
+ bfs_verify(ioq_capacity(ioq) == 0);
+
+ // Now cancel the queue, pushing an additional IOQ_STOP message
+ ioq_cancel(ioq);
+
+ // Drain the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct ioq_ent *ent = ioq_pop(ioq, true);
+ bfs_verify(ent && ent->op == IOQ_OPENDIR);
+
+ if (ent->result >= 0) {
+ bfs_closedir(ent->opendir.dir);
+ }
+ free(ent->opendir.dir);
+ ioq_free(ioq, ent);
+ }
+ bfs_verify(!ioq_pop(ioq, true));
+
+ ioq_destroy(ioq);
+}
+
+void check_ioq(void) {
+ check_ioq_push_block();
+}
diff --git a/tests/list.c b/tests/list.c
new file mode 100644
index 0000000..5d0403f
--- /dev/null
+++ b/tests/list.c
@@ -0,0 +1,99 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "bfs.h"
+#include "diag.h"
+#include "list.h"
+
+#include <stddef.h>
+
+struct item {
+ int n;
+ struct item *next;
+};
+
+struct list {
+ struct item *head;
+ struct item **tail;
+};
+
+static bool check_list_items(struct list *list, int *array, size_t size) {
+ struct item **cur = &list->head;
+ for (size_t i = 0; i < size; ++i) {
+ if (!bfs_check(*cur != NULL)) {
+ return false;
+ }
+ int n = (*cur)->n;
+ if (!bfs_check(n == array[i], "%d != %d", n, array[i])) {
+ return false;
+ }
+ cur = &(*cur)->next;
+ }
+
+ if (!bfs_check(*cur == NULL)) {
+ return false;
+ }
+ if (!bfs_check(list->tail == cur)) {
+ return false;
+ }
+
+ return true;
+}
+
+#define ARRAY(...) (int[]){ __VA_ARGS__ }, countof((int[]){ __VA_ARGS__ })
+#define EMPTY() NULL, 0
+
+void check_list(void) {
+ struct list l1;
+ SLIST_INIT(&l1);
+ bfs_verify(check_list_items(&l1, EMPTY()));
+
+ struct list l2;
+ SLIST_INIT(&l2);
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, EMPTY()));
+
+ struct item i10 = { .n = 10 };
+ SLIST_APPEND(&l1, &i10);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ SLIST_SPLICE(&l1, &l1.head, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10)));
+
+ struct item i20 = { .n = 20 };
+ SLIST_PREPEND(&l2, &i20);
+ bfs_verify(check_list_items(&l2, ARRAY(20)));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 20)));
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ struct item i15 = { .n = 15 };
+ SLIST_APPEND(&l2, &i15);
+ SLIST_SPLICE(&l1, &i10.next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+ bfs_verify(check_list_items(&l2, EMPTY()));
+
+ SLIST_EXTEND(&l1, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ SLIST_SPLICE(&l1, &i10.next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ SLIST_SPLICE(&l1, &l1.head, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 15, 20)));
+
+ struct item i11 = { .n = 11 };
+ struct item i12 = { .n = 12 };
+ SLIST_APPEND(&l2, &i11);
+ SLIST_APPEND(&l2, &i12);
+ SLIST_SPLICE(&l1, &l1.head->next, &l2);
+ bfs_verify(check_list_items(&l1, ARRAY(10, 11, 12, 15, 20)));
+}
diff --git a/tests/ls-color.sh b/tests/ls-color.sh
index c82a58d..b9a0402 100755
--- a/tests/ls-color.sh
+++ b/tests/ls-color.sh
@@ -1,36 +1,50 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
# Prints the "ground truth" coloring of a path using ls
set -e
+parse_ls_colors() {
+ for key; do
+ local -n var="$key"
+ if [[ "$LS_COLORS" =~ (^|:)$key=(([^:]|\\:)*) ]]; then
+ var="${BASH_REMATCH[2]}"
+ # Interpret escapes
+ var=$(printf "$var" | sed $'s/\^\[/\033/g; s/\\\\:/:/g')
+ fi
+ done
+}
+
+re_escape() {
+ # https://stackoverflow.com/a/29613573/502399
+ sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"
+}
+
+rs=0
+lc=$'\033['
+rc=m
+ec=
+no=
+
+parse_ls_colors rs lc rc ec no
+: "${ec:=$lc$rs$rc}"
+
+strip="(($(re_escape "$lc$no$rc"))?($(re_escape "$ec")|$(re_escape "$lc$rc")))+"
+
+ls_color() {
+ # Strip the leading reset sequence from the ls output
+ ls -1d --color "$@" | sed -E "s/^$strip([a-z].*)$strip/\4/; s/^$strip//"
+}
+
L=
if [ "$1" = "-L" ]; then
L="$1"
shift
fi
-function ls_color() {
- # Strip the leading reset sequence from the ls output
- ls -1d --color "$@" | sed $'s/^\033\\[0m//'
-}
-
DIR="${1%/*}"
if [ "$DIR" = "$1" ]; then
ls_color "$1"
diff --git a/tests/main.c b/tests/main.c
new file mode 100644
index 0000000..9240e1c
--- /dev/null
+++ b/tests/main.c
@@ -0,0 +1,271 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Entry point for unit tests.
+ */
+
+#include "tests.h"
+
+#include "alloc.h"
+#include "bfstd.h"
+#include "color.h"
+#include "list.h"
+
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+/** Result of the current test. */
+static bool pass;
+
+bool bfs_check_impl(bool result) {
+ pass &= result;
+ return result;
+}
+
+/**
+ * A running test.
+ */
+struct test_proc {
+ /** Linked list links. */
+ struct test_proc *prev, *next;
+
+ /** The PID of this test. */
+ pid_t pid;
+ /** The name of this test. */
+ const char *name;
+};
+
+/**
+ * Global test context.
+ */
+struct test_ctx {
+ /** Number of command line arguments. */
+ int argc;
+ /** The arguments themselves. */
+ char **argv;
+
+ /** Maximum jobs (-j). */
+ int jobs;
+ /** Current jobs. */
+ int running;
+ /** Completed jobs. */
+ int done;
+ /** List of running tests. */
+ struct {
+ struct test_proc *head, *tail;
+ } procs;
+
+ /** Parsed colors. */
+ struct colors *colors;
+ /** Colorized output stream. */
+ CFILE *cout;
+
+ /** Eventual exit status. */
+ int ret;
+};
+
+/** Initialize the test context. */
+static int test_init(struct test_ctx *ctx, int jobs, int argc, char **argv) {
+ ctx->argc = argc;
+ ctx->argv = argv;
+
+ ctx->jobs = jobs;
+ ctx->running = 0;
+ ctx->done = 0;
+ LIST_INIT(&ctx->procs);
+
+ ctx->colors = parse_colors();
+ ctx->cout = cfwrap(stdout, ctx->colors, false);
+ if (!ctx->cout) {
+ ctx->ret = EXIT_FAILURE;
+ return -1;
+ }
+
+ ctx->ret = EXIT_SUCCESS;
+ return 0;
+}
+
+/** Check if a test case is enabled for this run. */
+static bool should_run(const struct test_ctx *ctx, const char *test) {
+ // Run all tests by default
+ if (ctx->argc == 0) {
+ return true;
+ }
+
+ // With args, run only specified tests
+ for (int i = 0; i < ctx->argc; ++i) {
+ if (strcmp(test, ctx->argv[i]) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** Wait for a test to finish. */
+static void wait_test(struct test_ctx *ctx) {
+ int wstatus;
+ pid_t pid = xwaitpid(0, &wstatus, 0);
+ bfs_everify(pid > 0, "xwaitpid()");
+
+ struct test_proc *proc = NULL;
+ for_list (struct test_proc, i, &ctx->procs) {
+ if (i->pid == pid) {
+ proc = i;
+ break;
+ }
+ }
+
+ bfs_verify(proc, "No test_proc for PID %ju", (intmax_t)pid);
+
+ bool passed = false;
+
+ if (WIFEXITED(wstatus)) {
+ int status = WEXITSTATUS(wstatus);
+ if (status == EXIT_SUCCESS) {
+ cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", proc->name);
+ passed = true;
+ } else if (status == EXIT_FAILURE) {
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", proc->name);
+ } else {
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (Exit %d)\n", proc->name, status);
+ }
+ } else {
+ const char *str = NULL;
+ if (WIFSIGNALED(wstatus)) {
+ str = strsignal(WTERMSIG(wstatus));
+ }
+ if (!str) {
+ str = "Unknown";
+ }
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs} (%s)\n", proc->name, str);
+ }
+
+ if (!passed) {
+ ctx->ret = EXIT_FAILURE;
+ }
+
+ --ctx->running;
+ ++ctx->done;
+ LIST_REMOVE(&ctx->procs, proc);
+ free(proc);
+}
+
+/** Unit test function type. */
+typedef void test_fn(void);
+
+/** Run a test if it's enabled. */
+static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
+ if (!should_run(ctx, test)) {
+ return;
+ }
+
+ while (ctx->running >= ctx->jobs) {
+ wait_test(ctx);
+ }
+
+ struct test_proc *proc = ALLOC(struct test_proc);
+ bfs_everify(proc, "alloc()");
+
+ LIST_ITEM_INIT(proc);
+ proc->name = test;
+
+ fflush(NULL);
+ proc->pid = fork();
+ bfs_everify(proc->pid >= 0, "fork()");
+
+ if (proc->pid > 0) {
+ // Parent
+ ++ctx->running;
+ LIST_APPEND(&ctx->procs, proc);
+ return;
+ }
+
+ // Child
+ pass = true;
+ fn();
+ exit(pass ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+/** Finalize the test context. */
+static int test_fini(struct test_ctx *ctx) {
+ while (ctx->running > 0) {
+ wait_test(ctx);
+ }
+
+ if (ctx->cout) {
+ cfclose(ctx->cout);
+ }
+
+ free_colors(ctx->colors);
+
+ return ctx->ret;
+}
+
+int main(int argc, char *argv[]) {
+ // Try to set a UTF-8 locale
+ if (!setlocale(LC_ALL, "C.UTF-8")) {
+ setlocale(LC_ALL, "");
+ }
+
+ // Run tests in UTC
+ if (setenv("TZ", "UTC0", true) != 0) {
+ perror("setenv()");
+ return EXIT_FAILURE;
+ }
+ tzset();
+
+ unsigned int jobs = 0;
+
+ const char *cmd = argc > 0 ? argv[0] : "units";
+ int c;
+ while (c = getopt(argc, argv, ":j:"), c != -1) {
+ switch (c) {
+ case 'j':
+ if (xstrtoui(optarg, NULL, 10, &jobs) != 0) {
+ fprintf(stderr, "%s: Bad job count '%s': %s\n", cmd, optarg, errstr());
+ return EXIT_FAILURE;
+ }
+ break;
+ case ':':
+ fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ case '?':
+ fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (!jobs) {
+ jobs = nproc();
+ }
+
+ if (optind > argc) {
+ optind = argc;
+ }
+
+ struct test_ctx ctx;
+ if (test_init(&ctx, jobs, argc - optind, argv + optind) != 0) {
+ goto done;
+ }
+
+ run_test(&ctx, "alloc", check_alloc);
+ run_test(&ctx, "bfstd", check_bfstd);
+ run_test(&ctx, "bit", check_bit);
+ run_test(&ctx, "ioq", check_ioq);
+ run_test(&ctx, "list", check_list);
+ run_test(&ctx, "sighook", check_sighook);
+ run_test(&ctx, "trie", check_trie);
+ run_test(&ctx, "xspawn", check_xspawn);
+ run_test(&ctx, "xtime", check_xtime);
+
+done:
+ return test_fini(&ctx);
+}
diff --git a/tests/mksock.c b/tests/mksock.c
index d1776b3..f46df96 100644
--- a/tests/mksock.c
+++ b/tests/mksock.c
@@ -1,29 +1,17 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* There's no standard Unix utility that creates a socket file, so this small
* program does the job.
*/
+#include "bfstd.h"
+
#include <errno.h>
-#include <libgen.h>
#include <stdio.h>
-#include <string.h>
#include <stdlib.h>
+#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -32,7 +20,7 @@
* Print an error message.
*/
static void errmsg(const char *cmd, const char *path) {
- fprintf(stderr, "%s: '%s': %s.\n", cmd, path, strerror(errno));
+ fprintf(stderr, "%s: '%s': %s.\n", cmd, path, xstrerror(errno));
}
/**
@@ -41,18 +29,13 @@ static void errmsg(const char *cmd, const char *path) {
* file name is not.
*/
static int chdir_parent(const char *path) {
- char *copy = strdup(path);
- if (!copy) {
+ char *dir = xdirname(path);
+ if (!dir) {
return -1;
}
- const char *dir = dirname(copy);
int ret = chdir(dir);
-
- int error = errno;
- free(copy);
- errno = error;
-
+ free(dir);
return ret;
}
@@ -66,22 +49,21 @@ static int init_sun(struct sockaddr_un *sock, const char *path) {
return -1;
}
- char *copy = strdup(path);
- if (!copy) {
+ char *base = xbasename(path);
+ if (!base) {
return -1;
}
- const char *base = basename(copy);
len = strlen(base);
if (len >= sizeof(sock->sun_path)) {
- free(copy);
+ free(base);
errno = ENAMETOOLONG;
return -1;
}
sock->sun_family = AF_UNIX;
memcpy(sock->sun_path, base, len + 1);
- free(copy);
+ free(base);
return 0;
}
@@ -119,7 +101,7 @@ int main(int argc, char *argv[]) {
ret = EXIT_FAILURE;
}
- if (close(fd) != 0) {
+ if (xclose(fd) != 0) {
errmsg(cmd, path);
ret = EXIT_FAILURE;
}
diff --git a/tests/posix/H.out b/tests/posix/H.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/posix/H.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/posix/H.sh b/tests/posix/H.sh
new file mode 100644
index 0000000..5bae1be
--- /dev/null
+++ b/tests/posix/H.sh
@@ -0,0 +1 @@
+bfs_diff -H links/deeply/nested/dir
diff --git a/tests/posix/HL.out b/tests/posix/HL.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/posix/HL.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/posix/HL.sh b/tests/posix/HL.sh
new file mode 100644
index 0000000..1858982
--- /dev/null
+++ b/tests/posix/HL.sh
@@ -0,0 +1 @@
+bfs_diff -HL links
diff --git a/tests/test_L_samefile_broken.out b/tests/posix/H_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_L_samefile_broken.out
+++ b/tests/posix/H_broken.out
diff --git a/tests/posix/H_broken.sh b/tests/posix/H_broken.sh
new file mode 100644
index 0000000..9ff761c
--- /dev/null
+++ b/tests/posix/H_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links/broken
diff --git a/tests/test_H_loops.out b/tests/posix/H_loops.out
index 1fc8f8f..1fc8f8f 100644
--- a/tests/test_H_loops.out
+++ b/tests/posix/H_loops.out
diff --git a/tests/posix/H_loops.sh b/tests/posix/H_loops.sh
new file mode 100644
index 0000000..90383b8
--- /dev/null
+++ b/tests/posix/H_loops.sh
@@ -0,0 +1 @@
+bfs_diff -H loops/deeply/nested/loop
diff --git a/tests/test_L_samefile_notdir.out b/tests/posix/H_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_L_samefile_notdir.out
+++ b/tests/posix/H_notdir.out
diff --git a/tests/posix/H_notdir.sh b/tests/posix/H_notdir.sh
new file mode 100644
index 0000000..68d7be7
--- /dev/null
+++ b/tests/posix/H_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links/notdir
diff --git a/tests/test_P_slash.out b/tests/posix/H_slash.out
index df7701b..df7701b 100644
--- a/tests/test_P_slash.out
+++ b/tests/posix/H_slash.out
diff --git a/tests/posix/H_slash.sh b/tests/posix/H_slash.sh
new file mode 100644
index 0000000..b44d756
--- /dev/null
+++ b/tests/posix/H_slash.sh
@@ -0,0 +1 @@
+bfs_diff -H links/deeply/nested/dir/
diff --git a/tests/test_path_flag_expr.out b/tests/posix/H_type_l.out
index e67f10b..e67f10b 100644
--- a/tests/test_path_flag_expr.out
+++ b/tests/posix/H_type_l.out
diff --git a/tests/posix/H_type_l.sh b/tests/posix/H_type_l.sh
new file mode 100644
index 0000000..416a53e
--- /dev/null
+++ b/tests/posix/H_type_l.sh
@@ -0,0 +1 @@
+bfs_diff -H links/skip -type l
diff --git a/tests/posix/L.out b/tests/posix/L.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/posix/L.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/posix/L.sh b/tests/posix/L.sh
new file mode 100644
index 0000000..d8aebe6
--- /dev/null
+++ b/tests/posix/L.sh
@@ -0,0 +1 @@
+bfs_diff -L links
diff --git a/tests/posix/LH.out b/tests/posix/LH.out
new file mode 100644
index 0000000..ff635ff
--- /dev/null
+++ b/tests/posix/LH.out
@@ -0,0 +1 @@
+links/deeply/nested/dir
diff --git a/tests/posix/LH.sh b/tests/posix/LH.sh
new file mode 100644
index 0000000..ef1d980
--- /dev/null
+++ b/tests/posix/LH.sh
@@ -0,0 +1 @@
+bfs_diff -LH links/deeply/nested/dir
diff --git a/tests/test_samefile_broken.out b/tests/posix/L_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_samefile_broken.out
+++ b/tests/posix/L_broken.out
diff --git a/tests/posix/L_broken.sh b/tests/posix/L_broken.sh
new file mode 100644
index 0000000..9ff761c
--- /dev/null
+++ b/tests/posix/L_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links/broken
diff --git a/tests/posix/L_depth.out b/tests/posix/L_depth.out
new file mode 100644
index 0000000..ec9e861
--- /dev/null
+++ b/tests/posix/L_depth.out
@@ -0,0 +1,17 @@
+links
+links/broken
+links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
+links/file
+links/hardlink
+links/notdir
+links/skip
+links/skip/broken
+links/skip/dir
+links/skip/file
+links/skip/link
+links/symlink
diff --git a/tests/posix/L_depth.sh b/tests/posix/L_depth.sh
new file mode 100644
index 0000000..59d7ee9
--- /dev/null
+++ b/tests/posix/L_depth.sh
@@ -0,0 +1 @@
+bfs_diff -L links -depth
diff --git a/tests/posix/L_loops.sh b/tests/posix/L_loops.sh
new file mode 100644
index 0000000..01b7efc
--- /dev/null
+++ b/tests/posix/L_loops.sh
@@ -0,0 +1,4 @@
+# POSIX says it's okay to either stop or keep going on seeing a filesystem
+# loop, as long as a diagnostic is printed
+invoke_bfs -L loops >/dev/null 2>"$OUT" && fail
+test -s "$OUT"
diff --git a/tests/posix/L_mount.out b/tests/posix/L_mount.out
new file mode 100644
index 0000000..7ed5f0d
--- /dev/null
+++ b/tests/posix/L_mount.out
@@ -0,0 +1,2 @@
+.
+./foo
diff --git a/tests/posix/L_mount.sh b/tests/posix/L_mount.sh
new file mode 100644
index 0000000..fd8042a
--- /dev/null
+++ b/tests/posix/L_mount.sh
@@ -0,0 +1,13 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+ln -s ../mnt foo/bar
+"$XTOUCH" mnt/baz
+ln -s ../mnt/baz foo/qux
+
+bfs_diff -L . -mount
diff --git a/tests/test_samefile_notdir.out b/tests/posix/L_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_samefile_notdir.out
+++ b/tests/posix/L_notdir.out
diff --git a/tests/posix/L_notdir.sh b/tests/posix/L_notdir.sh
new file mode 100644
index 0000000..68d7be7
--- /dev/null
+++ b/tests/posix/L_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links/notdir
diff --git a/tests/test_L_type_l.out b/tests/posix/L_type_l.out
index 725d398..725d398 100644
--- a/tests/test_L_type_l.out
+++ b/tests/posix/L_type_l.out
diff --git a/tests/posix/L_type_l.sh b/tests/posix/L_type_l.sh
new file mode 100644
index 0000000..ee9e563
--- /dev/null
+++ b/tests/posix/L_type_l.sh
@@ -0,0 +1 @@
+bfs_diff -L links/skip -type l
diff --git a/tests/posix/L_xdev.out b/tests/posix/L_xdev.out
new file mode 100644
index 0000000..788579d
--- /dev/null
+++ b/tests/posix/L_xdev.out
@@ -0,0 +1,5 @@
+.
+./foo
+./foo/bar
+./foo/qux
+./mnt
diff --git a/tests/posix/L_xdev.sh b/tests/posix/L_xdev.sh
new file mode 100644
index 0000000..82d8605
--- /dev/null
+++ b/tests/posix/L_xdev.sh
@@ -0,0 +1,13 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+ln -s ../mnt foo/bar
+"$XTOUCH" mnt/baz
+ln -s ../mnt/baz foo/qux
+
+bfs_diff -L . -xdev
diff --git a/tests/test_and.out b/tests/posix/a.out
index 722962c..722962c 100644
--- a/tests/test_and.out
+++ b/tests/posix/a.out
diff --git a/tests/posix/a.sh b/tests/posix/a.sh
new file mode 100644
index 0000000..7d82d88
--- /dev/null
+++ b/tests/posix/a.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -a -type d
diff --git a/tests/posix/atime.out b/tests/posix/atime.out
new file mode 100644
index 0000000..5ed206b
--- /dev/null
+++ b/tests/posix/atime.out
@@ -0,0 +1,6 @@
+-atime 1: ./yesterday
+-atime +1: ./last_week
+-atime +1: ./two_days_ago
+-atime -1: ./now
+-atime -1: ./one_hour_ago
+-atime -1: ./tomorrow
diff --git a/tests/posix/atime.sh b/tests/posix/atime.sh
new file mode 100644
index 0000000..25dfd7e
--- /dev/null
+++ b/tests/posix/atime.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -at "@$((now - 60 * 60 * 24 * 7))" last_week
+"$XTOUCH" -at "@$((now - 60 * 60 * 49))" two_days_ago
+"$XTOUCH" -at "@$((now - 60 * 60 * 25))" yesterday
+"$XTOUCH" -at "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -at "@$((now))" now
+"$XTOUCH" -at "@$((now + 60 * 60 * 24))" tomorrow
+
+bfs_diff . \! -name . \
+ \( -atime -1 -exec printf -- '-atime -1: %s\n' {} \; -o -prune \) \
+ \( -atime 1 -exec printf -- '-atime 1: %s\n' {} \; -o -prune \) \
+ \( -atime +1 -exec printf -- '-atime +1: %s\n' {} \; -o -prune \)
diff --git a/tests/test_not.out b/tests/posix/bang.out
index 2501b2f..b286454 100644
--- a/tests/test_not.out
+++ b/tests/posix/bang.out
@@ -2,15 +2,15 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo/bar
+basic/l
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/bang.sh b/tests/posix/bang.sh
new file mode 100644
index 0000000..27840cd
--- /dev/null
+++ b/tests/posix/bang.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -name foo
diff --git a/tests/posix/basic.out b/tests/posix/basic.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/basic.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/basic.sh b/tests/posix/basic.sh
new file mode 100644
index 0000000..3d43529
--- /dev/null
+++ b/tests/posix/basic.sh
@@ -0,0 +1 @@
+bfs_diff basic
diff --git a/tests/test_data_flow_and_swap.out b/tests/posix/data_flow_and_swap.out
index 1e72fd9..e604709 100644
--- a/tests/test_data_flow_and_swap.out
+++ b/tests/posix/data_flow_and_swap.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/data_flow_and_swap.sh b/tests/posix/data_flow_and_swap.sh
new file mode 100644
index 0000000..9a141af
--- /dev/null
+++ b/tests/posix/data_flow_and_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -type f -a -type d
diff --git a/tests/posix/data_flow_group.out b/tests/posix/data_flow_group.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/data_flow_group.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/data_flow_group.sh b/tests/posix/data_flow_group.sh
new file mode 100644
index 0000000..453dc3e
--- /dev/null
+++ b/tests/posix/data_flow_group.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \)
diff --git a/tests/test_data_flow_or_swap.out b/tests/posix/data_flow_or_swap.out
index 1e72fd9..e604709 100644
--- a/tests/test_data_flow_or_swap.out
+++ b/tests/posix/data_flow_or_swap.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/data_flow_or_swap.sh b/tests/posix/data_flow_or_swap.sh
new file mode 100644
index 0000000..e8f504b
--- /dev/null
+++ b/tests/posix/data_flow_or_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type d \)
diff --git a/tests/test_printf_w.out b/tests/posix/data_flow_type.out
index e69de29..e69de29 100644
--- a/tests/test_printf_w.out
+++ b/tests/posix/data_flow_type.out
diff --git a/tests/posix/data_flow_type.sh b/tests/posix/data_flow_type.sh
new file mode 100644
index 0000000..33339df
--- /dev/null
+++ b/tests/posix/data_flow_type.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type f \)
diff --git a/tests/posix/data_flow_user.out b/tests/posix/data_flow_user.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/data_flow_user.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/data_flow_user.sh b/tests/posix/data_flow_user.sh
new file mode 100644
index 0000000..44b6e1f
--- /dev/null
+++ b/tests/posix/data_flow_user.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \)
diff --git a/tests/test_de_morgan_and.out b/tests/posix/de_morgan_and.out
index 723790f..7b7afd2 100644
--- a/tests/test_de_morgan_and.out
+++ b/tests/posix/de_morgan_and.out
@@ -2,9 +2,9 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/g/h
basic/l/foo/bar
diff --git a/tests/posix/de_morgan_and.sh b/tests/posix/de_morgan_and.sh
new file mode 100644
index 0000000..d52975e
--- /dev/null
+++ b/tests/posix/de_morgan_and.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -a \! -type f \)
diff --git a/tests/test_de_morgan_not.out b/tests/posix/de_morgan_not.out
index 5916da3..5916da3 100644
--- a/tests/test_de_morgan_not.out
+++ b/tests/posix/de_morgan_not.out
diff --git a/tests/posix/de_morgan_not.sh b/tests/posix/de_morgan_not.sh
new file mode 100644
index 0000000..7393ce0
--- /dev/null
+++ b/tests/posix/de_morgan_not.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name 'foo' -o \! -type f \)
diff --git a/tests/test_de_morgan_or.out b/tests/posix/de_morgan_or.out
index 697f6b8..2a57066 100644
--- a/tests/test_de_morgan_or.out
+++ b/tests/posix/de_morgan_or.out
@@ -2,17 +2,17 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/de_morgan_or.sh b/tests/posix/de_morgan_or.sh
new file mode 100644
index 0000000..378aab2
--- /dev/null
+++ b/tests/posix/de_morgan_or.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -o \! -type f \)
diff --git a/tests/test_deep_strict.out b/tests/posix/deep.out
index c385fce..c385fce 100644
--- a/tests/test_deep_strict.out
+++ b/tests/posix/deep.out
diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh
new file mode 100644
index 0000000..36a88c0
--- /dev/null
+++ b/tests/posix/deep.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/posix/depth.out b/tests/posix/depth.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/depth.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/depth.sh b/tests/posix/depth.sh
new file mode 100644
index 0000000..444eba5
--- /dev/null
+++ b/tests/posix/depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth
diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out
new file mode 100644
index 0000000..c4f8ce4
--- /dev/null
+++ b/tests/posix/depth_error.out
@@ -0,0 +1,4 @@
+inaccessible
+inaccessible/dir
+inaccessible/file
+inaccessible/link
diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh
new file mode 100644
index 0000000..9b29385
--- /dev/null
+++ b/tests/posix/depth_error.sh
@@ -0,0 +1 @@
+! bfs_diff inaccessible -depth
diff --git a/tests/test_depth_slash.out b/tests/posix/depth_slash.out
index 7ce7a82..77526d5 100644
--- a/tests/test_depth_slash.out
+++ b/tests/posix/depth_slash.out
@@ -2,18 +2,18 @@ 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/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/depth_slash.sh b/tests/posix/depth_slash.sh
new file mode 100644
index 0000000..f73e9f1
--- /dev/null
+++ b/tests/posix/depth_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -depth
diff --git a/tests/test_double_negation.out b/tests/posix/double_negation.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_double_negation.out
+++ b/tests/posix/double_negation.out
diff --git a/tests/posix/double_negation.sh b/tests/posix/double_negation.sh
new file mode 100644
index 0000000..eefe464
--- /dev/null
+++ b/tests/posix/double_negation.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \! -name 'foo'
diff --git a/tests/posix/exec.out b/tests/posix/exec.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec.sh b/tests/posix/exec.sh
new file mode 100644
index 0000000..96c897b
--- /dev/null
+++ b/tests/posix/exec.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec echo {} \;
diff --git a/tests/posix/exec_nonexistent.out b/tests/posix/exec_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nonexistent.sh b/tests/posix/exec_nonexistent.sh
new file mode 100644
index 0000000..a9ff052
--- /dev/null
+++ b/tests/posix/exec_nonexistent.sh
@@ -0,0 +1,4 @@
+# Failure to execute the command should lead to an error message and
+# non-zero exit status. See https://unix.stackexchange.com/q/704522/56202
+bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_nopath.out b/tests/posix/exec_nopath.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_nopath.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nopath.sh b/tests/posix/exec_nopath.sh
new file mode 100644
index 0000000..6e05d2e
--- /dev/null
+++ b/tests/posix/exec_nopath.sh
@@ -0,0 +1,7 @@
+(
+ unset PATH
+ invoke_bfs basic -exec echo {} \; >"$OUT"
+)
+
+sort_output
+diff_output
diff --git a/tests/test_exec_plus.out b/tests/posix/exec_plus.out
index f6b423b..f6b423b 100644
--- a/tests/test_exec_plus.out
+++ b/tests/posix/exec_plus.out
diff --git a/tests/posix/exec_plus.sh b/tests/posix/exec_plus.sh
new file mode 100644
index 0000000..56a93f1
--- /dev/null
+++ b/tests/posix/exec_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec "$TESTS/sort-args.sh" {} +
diff --git a/tests/posix/exec_plus_nonexistent.out b/tests/posix/exec_plus_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_nonexistent.sh b/tests/posix/exec_plus_nonexistent.sh
new file mode 100644
index 0000000..24582a3
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -exec "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_plus_nothing.sh b/tests/posix/exec_plus_nothing.sh
new file mode 100644
index 0000000..347722d
--- /dev/null
+++ b/tests/posix/exec_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -exec +
diff --git a/tests/test_exec_plus_semicolon.out b/tests/posix/exec_plus_semicolon.out
index 3805261..f33c48f 100644
--- a/tests/test_exec_plus_semicolon.out
+++ b/tests/posix/exec_plus_semicolon.out
@@ -2,18 +2,18 @@ foo basic bar + baz
foo basic/a bar + baz
foo basic/b bar + baz
foo basic/c bar + baz
+foo basic/c/d bar + baz
foo basic/e bar + baz
+foo basic/e/f bar + baz
foo basic/g bar + baz
+foo basic/g/h bar + baz
foo basic/i bar + baz
foo basic/j bar + baz
-foo basic/k bar + baz
-foo basic/l bar + baz
-foo basic/c/d bar + baz
-foo basic/e/f bar + baz
-foo basic/g/h bar + baz
foo basic/j/foo bar + baz
+foo basic/k bar + baz
foo basic/k/foo bar + baz
-foo basic/l/foo bar + baz
foo basic/k/foo/bar bar + baz
+foo basic/l bar + baz
+foo basic/l/foo bar + baz
foo basic/l/foo/bar bar + baz
foo basic/l/foo/bar/baz bar + baz
diff --git a/tests/posix/exec_plus_semicolon.sh b/tests/posix/exec_plus_semicolon.sh
new file mode 100644
index 0000000..449a3f9
--- /dev/null
+++ b/tests/posix/exec_plus_semicolon.sh
@@ -0,0 +1,5 @@
+# POSIX says:
+# Only a <plus-sign> that immediately follows an argument containing only the two characters "{}"
+# shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be
+# treated as special.
+bfs_diff basic -exec echo foo {} bar + baz \;
diff --git a/tests/posix/exec_plus_status.out b/tests/posix/exec_plus_status.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_status.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_status.sh b/tests/posix/exec_plus_status.sh
new file mode 100644
index 0000000..a814c4e
--- /dev/null
+++ b/tests/posix/exec_plus_status.sh
@@ -0,0 +1,3 @@
+# -exec ... {} + should always return true, but if the command fails, bfs
+# should exit with a non-zero status
+! bfs_diff basic -exec false {} + -print
diff --git a/tests/posix/exec_return.out b/tests/posix/exec_return.out
new file mode 100644
index 0000000..600c93a
--- /dev/null
+++ b/tests/posix/exec_return.out
@@ -0,0 +1,18 @@
+basic
+basic/a
+basic/b
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_return.sh b/tests/posix/exec_return.sh
new file mode 100644
index 0000000..cfa0f5d
--- /dev/null
+++ b/tests/posix/exec_return.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec test {} = basic/c \; -o -print
diff --git a/tests/posix/exec_sigmask.out b/tests/posix/exec_sigmask.out
new file mode 100644
index 0000000..bb646f3
--- /dev/null
+++ b/tests/posix/exec_sigmask.out
@@ -0,0 +1 @@
+SigBlk: 0000000000000000
diff --git a/tests/posix/exec_sigmask.sh b/tests/posix/exec_sigmask.sh
new file mode 100644
index 0000000..2907458
--- /dev/null
+++ b/tests/posix/exec_sigmask.sh
@@ -0,0 +1,16 @@
+# Regression test: restore the signal mask after fork()
+
+cd "$TEST"
+mkfifo p1 p2
+
+{
+ # Get the PID of `sh`
+ read -r pid <p1
+ # Send SIGTERM -- this will hang forever if signals are blocked
+ kill $pid
+} &
+
+# Write the `sh` PID to p1, then hang reading p2 until we're killed
+! invoke_bfs p1 -exec bash -c 'echo $$ >p1 && read -r _ <p2' bash {} + || fail
+
+_wait
diff --git a/tests/posix/exec_substring_plus.out b/tests/posix/exec_substring_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_substring_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_substring_plus.sh b/tests/posix/exec_substring_plus.sh
new file mode 100644
index 0000000..90309b0
--- /dev/null
+++ b/tests/posix/exec_substring_plus.sh
@@ -0,0 +1,14 @@
+# https://pubs.opengroup.org/onlinepubs/9799919799/utilities/find.html
+#
+# Only a <plus-sign> that immediately follows an argument containing only
+# the two characters "{}" shall punctuate the end of the primary expression.
+# Other uses of the <plus-sign> shall not be treated as special.
+# ...
+# If a utility_name or argument string contains the two characters "{}", but
+# not just the two characters "{}", it is implementation-defined whether
+# find replaces those two characters or uses the string without change.
+
+invoke_bfs basic -exec printf '%s %s %s %s\n' {} {}+ +{} + \; | sed 's/ .*//' >"$OUT"
+sort_output
+diff_output
+
diff --git a/tests/posix/exec_ulimit.out b/tests/posix/exec_ulimit.out
new file mode 100644
index 0000000..144169e
--- /dev/null
+++ b/tests/posix/exec_ulimit.out
@@ -0,0 +1,16 @@
+64 deep/0/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/1/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/2/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/3/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/4/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/5/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/6/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/7/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/8/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/9/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/A/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/B/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/C/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/D/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/E/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/F/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/posix/exec_ulimit.sh b/tests/posix/exec_ulimit.sh
new file mode 100644
index 0000000..655fbec
--- /dev/null
+++ b/tests/posix/exec_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -Sn 64
+bfs_diff deep -type f -exec bash -c 'printf "%d %s\n" $(ulimit -Sn) "${1:0:6}/.../${1##*/}"' bash {} \;
diff --git a/tests/posix/extra_paren.sh b/tests/posix/extra_paren.sh
new file mode 100644
index 0000000..d15022f
--- /dev/null
+++ b/tests/posix/extra_paren.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print \)
diff --git a/tests/test_flag_comma.out b/tests/posix/flag_comma.out
index 3574388..3574388 100644
--- a/tests/test_flag_comma.out
+++ b/tests/posix/flag_comma.out
diff --git a/tests/posix/flag_comma.sh b/tests/posix/flag_comma.sh
new file mode 100644
index 0000000..cec87e7
--- /dev/null
+++ b/tests/posix/flag_comma.sh
@@ -0,0 +1,3 @@
+# , is a filename until a non-flag is seen
+cd weirdnames
+bfs_diff -L ',' -print
diff --git a/tests/test_flag_weird_names.out b/tests/posix/flag_weird_names.out
index 73d0ff3..c395659 100644
--- a/tests/test_flag_weird_names.out
+++ b/tests/posix/flag_weird_names.out
@@ -1,28 +1,28 @@
!-
!-
-(-
-(-
-)
-)
-,
-,
--
--
!-/e
!-/e
+(-
+(-
(-/c
(-/c
+)
+)
)/g
)/g
+,
+,
,/f
,/f
+-
+-
-/a
-/a
./!
./!
-./(
-./(
./!/d
./!/d
+./(
+./(
./(/b
./(/b
diff --git a/tests/posix/flag_weird_names.sh b/tests/posix/flag_weird_names.sh
new file mode 100644
index 0000000..f6596e9
--- /dev/null
+++ b/tests/posix/flag_weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/group_id.out b/tests/posix/group_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_id.sh b/tests/posix/group_id.sh
new file mode 100644
index 0000000..2ff7bb3
--- /dev/null
+++ b/tests/posix/group_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -group "$(id -g)"
diff --git a/tests/posix/group_invalid_id.sh b/tests/posix/group_invalid_id.sh
new file mode 100644
index 0000000..1a89747
--- /dev/null
+++ b/tests/posix/group_invalid_id.sh
@@ -0,0 +1 @@
+! invoke_bfs -group 1eW6f5RM9Qi
diff --git a/tests/posix/group_invalid_name.sh b/tests/posix/group_invalid_name.sh
new file mode 100644
index 0000000..a08dc72
--- /dev/null
+++ b/tests/posix/group_invalid_name.sh
@@ -0,0 +1 @@
+! invoke_bfs -group eW6f5RM9Qi
diff --git a/tests/posix/group_name.out b/tests/posix/group_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_name.sh b/tests/posix/group_name.sh
new file mode 100644
index 0000000..36799d9
--- /dev/null
+++ b/tests/posix/group_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -group "$(id -gn)"
diff --git a/tests/posix/group_nogroup.out b/tests/posix/group_nogroup.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_nogroup.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_nogroup.sh b/tests/posix/group_nogroup.sh
new file mode 100644
index 0000000..cbd1ffc
--- /dev/null
+++ b/tests/posix/group_nogroup.sh
@@ -0,0 +1,2 @@
+# Regression test: this was wrongly optimized to -false
+bfs_diff basic -group "$(id -g)" \! -nogroup
diff --git a/tests/posix/group_o_group.out b/tests/posix/group_o_group.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_o_group.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_o_group.sh b/tests/posix/group_o_group.sh
new file mode 100644
index 0000000..60aefc0
--- /dev/null
+++ b/tests/posix/group_o_group.sh
@@ -0,0 +1,3 @@
+# Regression test for
+# https://github.com/tavianator/bfs/issues/155
+bfs_diff basic -group 0 -o -group "$(id -g)"
diff --git a/tests/test_implicit_and.out b/tests/posix/implicit_and.out
index 722962c..722962c 100644
--- a/tests/test_implicit_and.out
+++ b/tests/posix/implicit_and.out
diff --git a/tests/posix/implicit_and.sh b/tests/posix/implicit_and.sh
new file mode 100644
index 0000000..161ab0b
--- /dev/null
+++ b/tests/posix/implicit_and.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -type d
diff --git a/tests/test_name.out b/tests/posix/iname.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_name.out
+++ b/tests/posix/iname.out
diff --git a/tests/posix/iname.sh b/tests/posix/iname.sh
new file mode 100644
index 0000000..a9297ac
--- /dev/null
+++ b/tests/posix/iname.sh
@@ -0,0 +1 @@
+bfs_diff basic -iname '*F*'
diff --git a/tests/posix/incomplete.sh b/tests/posix/incomplete.sh
new file mode 100644
index 0000000..bca5a13
--- /dev/null
+++ b/tests/posix/incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic \(
diff --git a/tests/test_links_plus.out b/tests/posix/links.out
index 996ffc8..996ffc8 100644
--- a/tests/test_links_plus.out
+++ b/tests/posix/links.out
diff --git a/tests/posix/links.sh b/tests/posix/links.sh
new file mode 100644
index 0000000..3d8ad80
--- /dev/null
+++ b/tests/posix/links.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links 2
diff --git a/tests/test_links_minus.out b/tests/posix/links_minus.out
index eda26f1..eda26f1 100644
--- a/tests/test_links_minus.out
+++ b/tests/posix/links_minus.out
diff --git a/tests/posix/links_minus.sh b/tests/posix/links_minus.sh
new file mode 100644
index 0000000..3ee0803
--- /dev/null
+++ b/tests/posix/links_minus.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links -2
diff --git a/tests/test_samefile.out b/tests/posix/links_plus.out
index 996ffc8..996ffc8 100644
--- a/tests/test_samefile.out
+++ b/tests/posix/links_plus.out
diff --git a/tests/posix/links_plus.sh b/tests/posix/links_plus.sh
new file mode 100644
index 0000000..375834b
--- /dev/null
+++ b/tests/posix/links_plus.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links +1
diff --git a/tests/posix/missing_paren.sh b/tests/posix/missing_paren.sh
new file mode 100644
index 0000000..d906fbe
--- /dev/null
+++ b/tests/posix/missing_paren.sh
@@ -0,0 +1 @@
+! invoke_bfs basic \( -print
diff --git a/tests/posix/mount.out b/tests/posix/mount.out
new file mode 100644
index 0000000..b0ad937
--- /dev/null
+++ b/tests/posix/mount.out
@@ -0,0 +1,3 @@
+.
+./foo
+./foo/bar
diff --git a/tests/posix/mount.sh b/tests/posix/mount.sh
new file mode 100644
index 0000000..c9abde5
--- /dev/null
+++ b/tests/posix/mount.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+"$XTOUCH" foo/bar mnt/baz
+
+bfs_diff . -mount
diff --git a/tests/posix/mtime.out b/tests/posix/mtime.out
new file mode 100644
index 0000000..91f0114
--- /dev/null
+++ b/tests/posix/mtime.out
@@ -0,0 +1,6 @@
+-mtime 1: ./yesterday
+-mtime +1: ./last_week
+-mtime +1: ./two_days_ago
+-mtime -1: ./now
+-mtime -1: ./one_hour_ago
+-mtime -1: ./tomorrow
diff --git a/tests/posix/mtime.sh b/tests/posix/mtime.sh
new file mode 100644
index 0000000..8367631
--- /dev/null
+++ b/tests/posix/mtime.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+
+now=$(epoch_time)
+
+"$XTOUCH" -mt "@$((now - 60 * 60 * 24 * 7))" last_week
+"$XTOUCH" -mt "@$((now - 60 * 60 * 49))" two_days_ago
+"$XTOUCH" -mt "@$((now - 60 * 60 * 25))" yesterday
+"$XTOUCH" -mt "@$((now - 60 * 60))" one_hour_ago
+"$XTOUCH" -mt "@$((now))" now
+"$XTOUCH" -mt "@$((now + 60 * 60 * 24))" tomorrow
+
+bfs_diff . \! -name . \
+ \( -mtime -1 -exec printf -- '-mtime -1: %s\n' {} \; -o -prune \) \
+ \( -mtime 1 -exec printf -- '-mtime 1: %s\n' {} \; -o -prune \) \
+ \( -mtime +1 -exec printf -- '-mtime +1: %s\n' {} \; -o -prune \)
diff --git a/tests/test_parens.out b/tests/posix/name.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_parens.out
+++ b/tests/posix/name.out
diff --git a/tests/posix/name.sh b/tests/posix/name.sh
new file mode 100644
index 0000000..a673ad0
--- /dev/null
+++ b/tests/posix/name.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*'
diff --git a/tests/test_quit_before_print.out b/tests/posix/name_backslash.out
index e69de29..e69de29 100644
--- a/tests/test_quit_before_print.out
+++ b/tests/posix/name_backslash.out
diff --git a/tests/posix/name_backslash.sh b/tests/posix/name_backslash.sh
new file mode 100644
index 0000000..ff9b539
--- /dev/null
+++ b/tests/posix/name_backslash.sh
@@ -0,0 +1,2 @@
+# An unescaped \ doesn't match
+bfs_diff weirdnames -name '\'
diff --git a/tests/posix/name_bracket.out b/tests/posix/name_bracket.out
new file mode 100644
index 0000000..5ff3c0c
--- /dev/null
+++ b/tests/posix/name_bracket.out
@@ -0,0 +1 @@
+weirdnames/[
diff --git a/tests/posix/name_bracket.sh b/tests/posix/name_bracket.sh
new file mode 100644
index 0000000..e2f943d
--- /dev/null
+++ b/tests/posix/name_bracket.sh
@@ -0,0 +1,9 @@
+# fnmatch() is broken on some platforms
+case "$UNAME" in
+ Darwin|NetBSD)
+ skip
+ ;;
+esac
+
+# An unclosed [ should be matched literally
+bfs_diff weirdnames -name '['
diff --git a/tests/test_prune.out b/tests/posix/name_character_class.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_prune.out
+++ b/tests/posix/name_character_class.out
diff --git a/tests/posix/name_character_class.sh b/tests/posix/name_character_class.sh
new file mode 100644
index 0000000..ecda190
--- /dev/null
+++ b/tests/posix/name_character_class.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '[e-g][!a-n][!p-z]'
diff --git a/tests/posix/name_double_backslash.out b/tests/posix/name_double_backslash.out
new file mode 100644
index 0000000..45ceda0
--- /dev/null
+++ b/tests/posix/name_double_backslash.out
@@ -0,0 +1 @@
+weirdnames/\
diff --git a/tests/posix/name_double_backslash.sh b/tests/posix/name_double_backslash.sh
new file mode 100644
index 0000000..009553a
--- /dev/null
+++ b/tests/posix/name_double_backslash.sh
@@ -0,0 +1,2 @@
+# An escaped \\ matches
+bfs_diff weirdnames -name '\\'
diff --git a/tests/posix/name_root.out b/tests/posix/name_root.out
new file mode 100644
index 0000000..511198f
--- /dev/null
+++ b/tests/posix/name_root.out
@@ -0,0 +1 @@
+basic/a
diff --git a/tests/posix/name_root.sh b/tests/posix/name_root.sh
new file mode 100644
index 0000000..785861e
--- /dev/null
+++ b/tests/posix/name_root.sh
@@ -0,0 +1 @@
+bfs_diff basic/a -name a
diff --git a/tests/test_quit.out b/tests/posix/name_root_depth.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_quit.out
+++ b/tests/posix/name_root_depth.out
diff --git a/tests/posix/name_root_depth.sh b/tests/posix/name_root_depth.sh
new file mode 100644
index 0000000..dc3b8bb
--- /dev/null
+++ b/tests/posix/name_root_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -name g
diff --git a/tests/test_name_slash.out b/tests/posix/name_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_name_slash.out
+++ b/tests/posix/name_slash.out
diff --git a/tests/posix/name_slash.sh b/tests/posix/name_slash.sh
new file mode 100644
index 0000000..b42b145
--- /dev/null
+++ b/tests/posix/name_slash.sh
@@ -0,0 +1 @@
+bfs_diff / -prune -name /
diff --git a/tests/test_name_slashes.out b/tests/posix/name_slashes.out
index 187b81f..187b81f 100644
--- a/tests/test_name_slashes.out
+++ b/tests/posix/name_slashes.out
diff --git a/tests/posix/name_slashes.sh b/tests/posix/name_slashes.sh
new file mode 100644
index 0000000..45a39d3
--- /dev/null
+++ b/tests/posix/name_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -prune -name /
diff --git a/tests/posix/name_star_star.out b/tests/posix/name_star_star.out
new file mode 100644
index 0000000..a9e5d42
--- /dev/null
+++ b/tests/posix/name_star_star.out
@@ -0,0 +1,4 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/name_star_star.sh b/tests/posix/name_star_star.sh
new file mode 100644
index 0000000..035f635
--- /dev/null
+++ b/tests/posix/name_star_star.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '**f**'
diff --git a/tests/test_name_trailing_slash.out b/tests/posix/name_trailing_slash.out
index daff2f5..daff2f5 100644
--- a/tests/test_name_trailing_slash.out
+++ b/tests/posix/name_trailing_slash.out
diff --git a/tests/posix/name_trailing_slash.sh b/tests/posix/name_trailing_slash.sh
new file mode 100644
index 0000000..ab058d1
--- /dev/null
+++ b/tests/posix/name_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/g/ -name g
diff --git a/tests/test_newerma.out b/tests/posix/newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newerma.out
+++ b/tests/posix/newer.out
diff --git a/tests/posix/newer.sh b/tests/posix/newer.sh
new file mode 100644
index 0000000..860623a
--- /dev/null
+++ b/tests/posix/newer.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/a
diff --git a/tests/posix/newer_broken.out b/tests/posix/newer_broken.out
new file mode 100644
index 0000000..d2dcdd1
--- /dev/null
+++ b/tests/posix/newer_broken.out
@@ -0,0 +1 @@
+times
diff --git a/tests/posix/newer_broken.sh b/tests/posix/newer_broken.sh
new file mode 100644
index 0000000..dccaa73
--- /dev/null
+++ b/tests/posix/newer_broken.sh
@@ -0,0 +1,4 @@
+ln -s nowhere "$TEST/broken"
+"$XTOUCH" -h -t "1991-12-14 00:03" "$TEST/broken"
+
+bfs_diff times -newer "$TEST/broken"
diff --git a/tests/posix/newer_nonexistent.sh b/tests/posix/newer_nonexistent.sh
new file mode 100644
index 0000000..5f2da4b
--- /dev/null
+++ b/tests/posix/newer_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newer times/nonexistent
diff --git a/tests/test_size_big.out b/tests/posix/nogroup.out
index e69de29..e69de29 100644
--- a/tests/test_size_big.out
+++ b/tests/posix/nogroup.out
diff --git a/tests/posix/nogroup.sh b/tests/posix/nogroup.sh
new file mode 100644
index 0000000..60ffd68
--- /dev/null
+++ b/tests/posix/nogroup.sh
@@ -0,0 +1 @@
+bfs_diff basic -nogroup
diff --git a/tests/test_xtype_reorder.out b/tests/posix/nogroup_ulimit.out
index e69de29..e69de29 100644
--- a/tests/test_xtype_reorder.out
+++ b/tests/posix/nogroup_ulimit.out
diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh
new file mode 100644
index 0000000..a39dd1f
--- /dev/null
+++ b/tests/posix/nogroup_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nogroup
diff --git a/tests/test_exclude_depth.out b/tests/posix/not_prune.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_exclude_depth.out
+++ b/tests/posix/not_prune.out
@@ -2,12 +2,12 @@ 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/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/posix/not_prune.sh b/tests/posix/not_prune.sh
new file mode 100644
index 0000000..6d7b092
--- /dev/null
+++ b/tests/posix/not_prune.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name foo -prune \)
diff --git a/tests/posix/nouser.out b/tests/posix/nouser.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/nouser.out
diff --git a/tests/posix/nouser.sh b/tests/posix/nouser.sh
new file mode 100644
index 0000000..e7c48c0
--- /dev/null
+++ b/tests/posix/nouser.sh
@@ -0,0 +1 @@
+bfs_diff basic -nouser
diff --git a/tests/posix/nouser_ulimit.out b/tests/posix/nouser_ulimit.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/nouser_ulimit.out
diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh
new file mode 100644
index 0000000..a94b8c5
--- /dev/null
+++ b/tests/posix/nouser_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nouser
diff --git a/tests/test_or.out b/tests/posix/o.out
index 9a82ee2..1650c4d 100644
--- a/tests/test_or.out
+++ b/tests/posix/o.out
@@ -2,12 +2,12 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/o.sh b/tests/posix/o.sh
new file mode 100644
index 0000000..6dcd442
--- /dev/null
+++ b/tests/posix/o.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -o -type d
diff --git a/tests/posix/ok_plus_nothing.sh b/tests/posix/ok_plus_nothing.sh
new file mode 100644
index 0000000..77c7644
--- /dev/null
+++ b/tests/posix/ok_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -ok +
diff --git a/tests/test_ok_stdin.out b/tests/posix/ok_stdin.out
index 30b57fd..7acf711 100644
--- a/tests/test_ok_stdin.out
+++ b/tests/posix/ok_stdin.out
@@ -1,19 +1,19 @@
-basic? y
basic/a? y
basic/b? y
+basic/c/d? y
basic/c? y
+basic/e/f? y
basic/e? y
+basic/g/h? y
basic/g? y
basic/i? y
-basic/j? y
-basic/k? y
-basic/l? y
-basic/c/d? y
-basic/e/f? y
-basic/g/h? y
basic/j/foo? y
-basic/k/foo? y
-basic/l/foo? y
+basic/j? y
basic/k/foo/bar? y
-basic/l/foo/bar? y
+basic/k/foo? y
+basic/k? y
basic/l/foo/bar/baz? y
+basic/l/foo/bar? y
+basic/l/foo? y
+basic/l? y
+basic? y
diff --git a/tests/posix/ok_stdin.sh b/tests/posix/ok_stdin.sh
new file mode 100644
index 0000000..a190d81
--- /dev/null
+++ b/tests/posix/ok_stdin.sh
@@ -0,0 +1,3 @@
+# -ok should *not* close stdin
+# See https://savannah.gnu.org/bugs/?24561
+yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/posix/or_purity.out b/tests/posix/or_purity.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/or_purity.out
diff --git a/tests/posix/or_purity.sh b/tests/posix/or_purity.sh
new file mode 100644
index 0000000..277b18b
--- /dev/null
+++ b/tests/posix/or_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name '*' -o -print
diff --git a/tests/posix/overlayfs.out b/tests/posix/overlayfs.out
new file mode 100644
index 0000000..b472b56
--- /dev/null
+++ b/tests/posix/overlayfs.out
@@ -0,0 +1,5 @@
+merged
+merged/bar
+merged/baz
+merged/baz/qux
+merged/foo
diff --git a/tests/posix/overlayfs.sh b/tests/posix/overlayfs.sh
new file mode 100644
index 0000000..21ef22f
--- /dev/null
+++ b/tests/posix/overlayfs.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+mkdir -p work merged
+bfs_sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged || skip
+defer bfs_sudo rm -rf work
+defer bfs_sudo umount merged
+
+bfs_diff merged
diff --git a/tests/posix/parens.out b/tests/posix/parens.out
new file mode 100644
index 0000000..a9e5d42
--- /dev/null
+++ b/tests/posix/parens.out
@@ -0,0 +1,4 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/parens.sh b/tests/posix/parens.sh
new file mode 100644
index 0000000..abbb20f
--- /dev/null
+++ b/tests/posix/parens.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name '*f*' \)
diff --git a/tests/posix/path.out b/tests/posix/path.out
new file mode 100644
index 0000000..ae1ae21
--- /dev/null
+++ b/tests/posix/path.out
@@ -0,0 +1,7 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/path.sh b/tests/posix/path.sh
new file mode 100644
index 0000000..04606eb
--- /dev/null
+++ b/tests/posix/path.sh
@@ -0,0 +1 @@
+bfs_diff basic -path 'basic/*f*'
diff --git a/tests/posix/perm_000.out b/tests/posix/perm_000.out
new file mode 100644
index 0000000..9df7f46
--- /dev/null
+++ b/tests/posix/perm_000.out
@@ -0,0 +1 @@
+perms/f---------
diff --git a/tests/posix/perm_000.sh b/tests/posix/perm_000.sh
new file mode 100644
index 0000000..ee25f23
--- /dev/null
+++ b/tests/posix/perm_000.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 000
diff --git a/tests/posix/perm_000_minus.out b/tests/posix/perm_000_minus.out
new file mode 100644
index 0000000..e279684
--- /dev/null
+++ b/tests/posix/perm_000_minus.out
@@ -0,0 +1,29 @@
+perms
+perms/dr-x------
+perms/dr-xr-xr-x
+perms/drwx------
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f---------
+perms/f--x------
+perms/f--x--x--x
+perms/f-w-------
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx------
+perms/f-wx--x--x
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/fr--------
+perms/fr--r--r--
+perms/fr-x------
+perms/fr-xr-xr-x
+perms/frw-------
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-----
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_000_minus.sh b/tests/posix/perm_000_minus.sh
new file mode 100644
index 0000000..5027b91
--- /dev/null
+++ b/tests/posix/perm_000_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -000
diff --git a/tests/posix/perm_222.out b/tests/posix/perm_222.out
new file mode 100644
index 0000000..bdc5590
--- /dev/null
+++ b/tests/posix/perm_222.out
@@ -0,0 +1 @@
+perms/f-w--w--w-
diff --git a/tests/posix/perm_222.sh b/tests/posix/perm_222.sh
new file mode 100644
index 0000000..40f5804
--- /dev/null
+++ b/tests/posix/perm_222.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 222
diff --git a/tests/posix/perm_222_minus.out b/tests/posix/perm_222_minus.out
new file mode 100644
index 0000000..342b285
--- /dev/null
+++ b/tests/posix/perm_222_minus.out
@@ -0,0 +1,5 @@
+perms/drwxrwxrwx
+perms/f-w--w--w-
+perms/f-wx-wx-wx
+perms/frw-rw-rw-
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_222_minus.sh b/tests/posix/perm_222_minus.sh
new file mode 100644
index 0000000..4e7ad5a
--- /dev/null
+++ b/tests/posix/perm_222_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -222
diff --git a/tests/posix/perm_644.out b/tests/posix/perm_644.out
new file mode 100644
index 0000000..9f77ce6
--- /dev/null
+++ b/tests/posix/perm_644.out
@@ -0,0 +1 @@
+perms/frw-r--r--
diff --git a/tests/posix/perm_644.sh b/tests/posix/perm_644.sh
new file mode 100644
index 0000000..9a4f41d
--- /dev/null
+++ b/tests/posix/perm_644.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 644
diff --git a/tests/posix/perm_644_minus.out b/tests/posix/perm_644_minus.out
new file mode 100644
index 0000000..84f69f5
--- /dev/null
+++ b/tests/posix/perm_644_minus.out
@@ -0,0 +1,10 @@
+perms
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_644_minus.sh b/tests/posix/perm_644_minus.sh
new file mode 100644
index 0000000..6464f84
--- /dev/null
+++ b/tests/posix/perm_644_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -644
diff --git a/tests/posix/perm_leading_plus_symbolic_minus.out b/tests/posix/perm_leading_plus_symbolic_minus.out
new file mode 100644
index 0000000..38d0e1c
--- /dev/null
+++ b/tests/posix/perm_leading_plus_symbolic_minus.out
@@ -0,0 +1,7 @@
+perms
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_leading_plus_symbolic_minus.sh b/tests/posix/perm_leading_plus_symbolic_minus.sh
new file mode 100644
index 0000000..60389c0
--- /dev/null
+++ b/tests/posix/perm_leading_plus_symbolic_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -+rwx
diff --git a/tests/posix/perm_leading_plus_umask.out b/tests/posix/perm_leading_plus_umask.out
new file mode 100644
index 0000000..6ed4b7f
--- /dev/null
+++ b/tests/posix/perm_leading_plus_umask.out
@@ -0,0 +1,10 @@
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/f-w--w----
+perms/f-w--w--w-
+perms/f-wx-wx--x
+perms/f-wx-wx-wx
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_leading_plus_umask.sh b/tests/posix/perm_leading_plus_umask.sh
new file mode 100644
index 0000000..948b4ad
--- /dev/null
+++ b/tests/posix/perm_leading_plus_umask.sh
@@ -0,0 +1,3 @@
+# Test for https://www.austingroupbugs.net/view.php?id=1392
+umask 002
+bfs_diff perms -perm -+w
diff --git a/tests/test_perm_setid.out b/tests/posix/perm_setid.out
index 865a74e..865a74e 100644
--- a/tests/test_perm_setid.out
+++ b/tests/posix/perm_setid.out
diff --git a/tests/posix/perm_setid.sh b/tests/posix/perm_setid.sh
new file mode 100644
index 0000000..3b98647
--- /dev/null
+++ b/tests/posix/perm_setid.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -perm -u+s -o -perm -g+s
diff --git a/tests/test_perm_sticky.out b/tests/posix/perm_sticky.out
index c07eb61..c07eb61 100644
--- a/tests/test_perm_sticky.out
+++ b/tests/posix/perm_sticky.out
diff --git a/tests/posix/perm_sticky.sh b/tests/posix/perm_sticky.sh
new file mode 100644
index 0000000..6bdf8e9
--- /dev/null
+++ b/tests/posix/perm_sticky.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -perm -a+t
diff --git a/tests/posix/perm_symbolic.out b/tests/posix/perm_symbolic.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/perm_symbolic.out
diff --git a/tests/posix/perm_symbolic.sh b/tests/posix/perm_symbolic.sh
new file mode 100644
index 0000000..5cfddb6
--- /dev/null
+++ b/tests/posix/perm_symbolic.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm a+r,u=wX,g+wX-w
diff --git a/tests/posix/perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out
new file mode 100644
index 0000000..84f69f5
--- /dev/null
+++ b/tests/posix/perm_symbolic_minus.out
@@ -0,0 +1,10 @@
+perms
+perms/drwxr-xr-x
+perms/drwxrwxr-x
+perms/drwxrwxrwx
+perms/frw-r--r--
+perms/frw-rw-r--
+perms/frw-rw-rw-
+perms/frwxr-xr-x
+perms/frwxrwxr-x
+perms/frwxrwxrwx
diff --git a/tests/posix/perm_symbolic_minus.sh b/tests/posix/perm_symbolic_minus.sh
new file mode 100644
index 0000000..b6ba3a5
--- /dev/null
+++ b/tests/posix/perm_symbolic_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -a+r,u=wX,g+wX-w
diff --git a/tests/posix/permcopy.out b/tests/posix/permcopy.out
new file mode 100644
index 0000000..9f77ce6
--- /dev/null
+++ b/tests/posix/permcopy.out
@@ -0,0 +1 @@
+perms/frw-r--r--
diff --git a/tests/posix/permcopy.sh b/tests/posix/permcopy.sh
new file mode 100644
index 0000000..3c85cce
--- /dev/null
+++ b/tests/posix/permcopy.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm u+rw,g+u-w,o=g
diff --git a/tests/test_print0.out b/tests/posix/print0.out
index 1347444..1347444 100644
--- a/tests/test_print0.out
+++ b/tests/posix/print0.out
Binary files differ
diff --git a/tests/posix/print0.sh b/tests/posix/print0.sh
new file mode 100644
index 0000000..b916172
--- /dev/null
+++ b/tests/posix/print0.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a basic/b -print0 >"$OUT"
+diff_output
diff --git a/tests/posix/prune.out b/tests/posix/prune.out
new file mode 100644
index 0000000..e9d47b1
--- /dev/null
+++ b/tests/posix/prune.out
@@ -0,0 +1,3 @@
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/prune.sh b/tests/posix/prune.sh
new file mode 100644
index 0000000..b48ab48
--- /dev/null
+++ b/tests/posix/prune.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune
diff --git a/tests/posix/prune_error.out b/tests/posix/prune_error.out
new file mode 100644
index 0000000..436c48e
--- /dev/null
+++ b/tests/posix/prune_error.out
@@ -0,0 +1 @@
+inaccessible
diff --git a/tests/posix/prune_error.sh b/tests/posix/prune_error.sh
new file mode 100644
index 0000000..07a2523
--- /dev/null
+++ b/tests/posix/prune_error.sh
@@ -0,0 +1 @@
+! bfs_diff -L inaccessible -path '*/*' -prune -o -print
diff --git a/tests/posix/prune_file.out b/tests/posix/prune_file.out
new file mode 100644
index 0000000..7575ae4
--- /dev/null
+++ b/tests/posix/prune_file.out
@@ -0,0 +1,10 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/e
+basic/g
+basic/i
+basic/j
+basic/k
+basic/l
diff --git a/tests/posix/prune_file.sh b/tests/posix/prune_file.sh
new file mode 100644
index 0000000..29a3a33
--- /dev/null
+++ b/tests/posix/prune_file.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -name '?' -prune
diff --git a/tests/test_exclude_name.out b/tests/posix/prune_or_print.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_exclude_name.out
+++ b/tests/posix/prune_or_print.out
@@ -2,12 +2,12 @@ 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/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/posix/prune_or_print.sh b/tests/posix/prune_or_print.sh
new file mode 100644
index 0000000..85b97fd
--- /dev/null
+++ b/tests/posix/prune_or_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune -o -print
diff --git a/tests/posix/readdir_error.sh b/tests/posix/readdir_error.sh
new file mode 100644
index 0000000..82fcd17
--- /dev/null
+++ b/tests/posix/readdir_error.sh
@@ -0,0 +1,37 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkfifo hang pid wait running
+
+(
+ # Create a zombie process
+ cat hang >/dev/null &
+ # Write the PID to pid
+ echo $! >pid
+ # Don't wait on the zombie process
+ exec cat wait hang >running
+) &
+
+# Kill the parent cat on exit
+defer kill -9 %1
+
+# Read the child PID
+read -r pid <pid
+
+# Make sure the parent cat is running before we kill the child, because bash
+# will wait() on its children
+echo >wait &
+read -r _ <running
+
+# Turn the child into a zombie
+kill -9 "$pid"
+
+# Wait until it's really a zombie
+state=R
+while [ "$state" != "Z" ]; do
+ read -r _ _ state _ <"/proc/$pid/stat"
+done
+
+# On Linux, open(/proc/$pid/net) will succeed but readdir() will fail
+test -r "/proc/$pid/net" || skip
+! invoke_bfs "/proc/$pid/net" >/dev/null
diff --git a/tests/posix/root_order.out b/tests/posix/root_order.out
new file mode 100644
index 0000000..ea94276
--- /dev/null
+++ b/tests/posix/root_order.out
@@ -0,0 +1,4 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
diff --git a/tests/posix/root_order.sh b/tests/posix/root_order.sh
new file mode 100644
index 0000000..86adf20
--- /dev/null
+++ b/tests/posix/root_order.sh
@@ -0,0 +1,6 @@
+# Root paths must be processed in order
+# https://www.austingroupbugs.net/view.php?id=1859
+
+# -size forces a stat(), which we don't want to be async
+invoke_bfs basic/{a,b,c/d,e/f} -size -1000 >"$OUT"
+diff_output
diff --git a/tests/test_size.out b/tests/posix/size.out
index eeabbd7..eeabbd7 100644
--- a/tests/test_size.out
+++ b/tests/posix/size.out
diff --git a/tests/posix/size.sh b/tests/posix/size.sh
new file mode 100644
index 0000000..1e7528a
--- /dev/null
+++ b/tests/posix/size.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 0
diff --git a/tests/test_size_bytes.out b/tests/posix/size_bytes.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_bytes.out
+++ b/tests/posix/size_bytes.out
diff --git a/tests/posix/size_bytes.sh b/tests/posix/size_bytes.sh
new file mode 100644
index 0000000..6a68321
--- /dev/null
+++ b/tests/posix/size_bytes.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0c
diff --git a/tests/test_size_plus.out b/tests/posix/size_plus.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_plus.out
+++ b/tests/posix/size_plus.out
diff --git a/tests/posix/size_plus.sh b/tests/posix/size_plus.sh
new file mode 100644
index 0000000..01853d5
--- /dev/null
+++ b/tests/posix/size_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0
diff --git a/tests/posix/type_bind_mount.out b/tests/posix/type_bind_mount.out
new file mode 100644
index 0000000..2f06c47
--- /dev/null
+++ b/tests/posix/type_bind_mount.out
@@ -0,0 +1 @@
+./null
diff --git a/tests/posix/type_bind_mount.sh b/tests/posix/type_bind_mount.sh
new file mode 100644
index 0000000..97b7305
--- /dev/null
+++ b/tests/posix/type_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -type c
diff --git a/tests/test_type_d.out b/tests/posix/type_d.out
index 1e72fd9..e604709 100644
--- a/tests/test_type_d.out
+++ b/tests/posix/type_d.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/type_d.sh b/tests/posix/type_d.sh
new file mode 100644
index 0000000..8d06b73
--- /dev/null
+++ b/tests/posix/type_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -type d
diff --git a/tests/posix/type_f.out b/tests/posix/type_f.out
new file mode 100644
index 0000000..6218a0c
--- /dev/null
+++ b/tests/posix/type_f.out
@@ -0,0 +1,7 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
+basic/j/foo
+basic/k/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/type_f.sh b/tests/posix/type_f.sh
new file mode 100644
index 0000000..1fd0c8c
--- /dev/null
+++ b/tests/posix/type_f.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f
diff --git a/tests/test_type_l.out b/tests/posix/type_l.out
index f2c8b19..f2c8b19 100644
--- a/tests/test_type_l.out
+++ b/tests/posix/type_l.out
diff --git a/tests/posix/type_l.sh b/tests/posix/type_l.sh
new file mode 100644
index 0000000..457f74d
--- /dev/null
+++ b/tests/posix/type_l.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l
diff --git a/tests/posix/unionfs.out b/tests/posix/unionfs.out
new file mode 100644
index 0000000..28c4ec1
--- /dev/null
+++ b/tests/posix/unionfs.out
@@ -0,0 +1,10 @@
+.
+./lower
+./lower/bar
+./lower/baz
+./lower/foo
+./upper
+./upper/bar
+./upper/baz
+./upper/baz/qux
+./upper/foo
diff --git a/tests/posix/unionfs.sh b/tests/posix/unionfs.sh
new file mode 100644
index 0000000..94d3929
--- /dev/null
+++ b/tests/posix/unionfs.sh
@@ -0,0 +1,9 @@
+[[ "$UNAME" == *BSD* ]] || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+bfs_sudo mount -t unionfs -o below lower upper || skip
+defer bfs_sudo umount upper
+
+bfs_diff .
diff --git a/tests/posix/user_id.out b/tests/posix/user_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_id.sh b/tests/posix/user_id.sh
new file mode 100644
index 0000000..c3e4b31
--- /dev/null
+++ b/tests/posix/user_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -user "$(id -u)"
diff --git a/tests/posix/user_invalid_id.sh b/tests/posix/user_invalid_id.sh
new file mode 100644
index 0000000..c378f7e
--- /dev/null
+++ b/tests/posix/user_invalid_id.sh
@@ -0,0 +1 @@
+! invoke_bfs -user 1eW6f5RM9Qi
diff --git a/tests/posix/user_invalid_name.sh b/tests/posix/user_invalid_name.sh
new file mode 100644
index 0000000..bbf3031
--- /dev/null
+++ b/tests/posix/user_invalid_name.sh
@@ -0,0 +1 @@
+! invoke_bfs -user eW6f5RM9Qi
diff --git a/tests/posix/user_name.out b/tests/posix/user_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_name.sh b/tests/posix/user_name.sh
new file mode 100644
index 0000000..8599249
--- /dev/null
+++ b/tests/posix/user_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -user "$(id -un)"
diff --git a/tests/posix/user_nouser.out b/tests/posix/user_nouser.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_nouser.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_nouser.sh b/tests/posix/user_nouser.sh
new file mode 100644
index 0000000..e72bd45
--- /dev/null
+++ b/tests/posix/user_nouser.sh
@@ -0,0 +1,2 @@
+# Regression test: this was wrongly optimized to -false
+bfs_diff basic -user "$(id -u)" \! -nouser
diff --git a/tests/posix/user_o_user.out b/tests/posix/user_o_user.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_o_user.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_o_user.sh b/tests/posix/user_o_user.sh
new file mode 100644
index 0000000..7c143ae
--- /dev/null
+++ b/tests/posix/user_o_user.sh
@@ -0,0 +1,3 @@
+# Regression test for
+# https://github.com/tavianator/bfs/issues/155
+bfs_diff basic -user 0 -o -user "$(id -u)"
diff --git a/tests/test_weird_names.out b/tests/posix/weird_names.out
index 73d0ff3..c395659 100644
--- a/tests/test_weird_names.out
+++ b/tests/posix/weird_names.out
@@ -1,28 +1,28 @@
!-
!-
-(-
-(-
-)
-)
-,
-,
--
--
!-/e
!-/e
+(-
+(-
(-/c
(-/c
+)
+)
)/g
)/g
+,
+,
,/f
,/f
+-
+-
-/a
-/a
./!
./!
-./(
-./(
./!/d
./!/d
+./(
+./(
./(/b
./(/b
diff --git a/tests/posix/weird_names.sh b/tests/posix/weird_names.sh
new file mode 100644
index 0000000..8a9a8cd
--- /dev/null
+++ b/tests/posix/weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/xdev.out b/tests/posix/xdev.out
new file mode 100644
index 0000000..6253434
--- /dev/null
+++ b/tests/posix/xdev.out
@@ -0,0 +1,4 @@
+.
+./foo
+./foo/bar
+./mnt
diff --git a/tests/posix/xdev.sh b/tests/posix/xdev.sh
new file mode 100644
index 0000000..c59c5c8
--- /dev/null
+++ b/tests/posix/xdev.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+"$XTOUCH" foo/bar mnt/baz
+
+bfs_diff . -xdev
diff --git a/tests/ptyx.c b/tests/ptyx.c
new file mode 100644
index 0000000..59292df
--- /dev/null
+++ b/tests/ptyx.c
@@ -0,0 +1,252 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Execute a command in a pseudo-terminal.
+ *
+ * $ ptyx [-w WIDTH] [-h HEIGHT] [--] COMMAND [ARGS...]
+ */
+
+#include "bfs.h"
+#include "bfstd.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+
+#if __has_include(<stropts.h>)
+# include <stropts.h>
+#endif
+
+#if __sun
+/**
+ * Push a STREAMS module, if it's not already there.
+ *
+ * See https://www.illumos.org/issues/9042.
+ */
+static int i_push(int fd, const char *name) {
+ int ret = ioctl(fd, I_FIND, name);
+ if (ret < 0) {
+ return ret;
+ } else if (ret == 0) {
+ return ioctl(fd, I_PUSH, name);
+ } else {
+ return 0;
+ }
+}
+#endif
+
+int main(int argc, char *argv[]) {
+ const char *cmd = argc > 0 ? argv[0] : "ptyx";
+
+/** Report an error message and exit. */
+#define die(...) die_(__VA_ARGS__, )
+
+#define die_(format, ...) \
+ do { \
+ fprintf(stderr, "%s: " format "%s", cmd, __VA_ARGS__ "\n"); \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+/** Report an error code and exit. */
+#define edie(...) edie_(__VA_ARGS__, )
+
+#define edie_(format, ...) \
+ do { \
+ fprintf(stderr, "%s: " format ": %s\n", cmd, __VA_ARGS__ errstr()); \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+ unsigned short width = 0;
+ unsigned short height = 0;
+
+ // Parse the command line
+ int c;
+ while (c = getopt(argc, argv, "+:w:h:"), c != -1) {
+ switch (c) {
+ case 'w':
+ if (xstrtous(optarg, NULL, 10, &width) != 0) {
+ edie("Bad width '%s'", optarg);
+ }
+ break;
+ case 'h':
+ if (xstrtous(optarg, NULL, 10, &height) != 0) {
+ edie("Bad height '%s'", optarg);
+ }
+ break;
+ case ':':
+ die("Missing argument to -%c", optopt);
+ case '?':
+ die("Unrecognized option -%c", optopt);
+ }
+ }
+
+ if (optind >= argc) {
+ die("Missing command");
+ }
+ char **args = argv + optind;
+
+ // Create a new pty, and set it up
+ int ptm = posix_openpt(O_RDWR | O_NOCTTY);
+ if (ptm < 0) {
+ edie("posix_openpt()");
+ }
+ if (grantpt(ptm) != 0) {
+ edie("grantpt()");
+ }
+ if (unlockpt(ptm) != 0) {
+ edie("unlockpt()");
+ }
+
+ // Get the subsidiary device path
+ char *name = ptsname(ptm);
+ if (!name) {
+ edie("ptsname()");
+ }
+
+ // Open the subsidiary device
+ int pts = open(name, O_RDWR | O_NOCTTY);
+ if (pts < 0) {
+ edie("%s", name);
+ }
+
+#if __sun
+ // On Solaris/illumos, a pty doesn't behave like a terminal until we
+ // push some STREAMS modules (see ptm(4D), ptem(4M), ldterm(4M)).
+ if (i_push(pts, "ptem") != 0) {
+ die("ioctl(I_PUSH, ptem)");
+ }
+ if (i_push(pts, "ldterm") != 0) {
+ die("ioctl(I_PUSH, ldterm)");
+ }
+#endif
+
+ // A new pty starts at 0x0, which is not very useful. Instead, grab the
+ // default size from the current controlling terminal, if possible.
+ if (!width || !height) {
+ int tty = open_cterm(O_RDONLY | O_CLOEXEC);
+ if (tty >= 0) {
+ struct winsize ws;
+ if (xtcgetwinsize(tty, &ws) != 0) {
+ edie("tcgetwinsize()");
+ }
+ if (!width) {
+ width = ws.ws_col;
+ }
+ if (!height) {
+ height = ws.ws_row;
+ }
+ xclose(tty);
+ }
+ }
+ if (!width) {
+ width = 80;
+ }
+ if (!height) {
+ height = 24;
+ }
+
+ // Update the pty size
+ struct winsize ws;
+ if (xtcgetwinsize(pts, &ws) != 0) {
+ edie("tcgetwinsize()");
+ }
+ ws.ws_col = width;
+ ws.ws_row = height;
+ if (xtcsetwinsize(pts, &ws) != 0) {
+ edie("tcsetwinsize()");
+ }
+
+ // Set custom terminal attributes
+ struct termios attrs;
+ if (tcgetattr(pts, &attrs) != 0) {
+ edie("tcgetattr()");
+ }
+ attrs.c_oflag &= ~OPOST; // Don't convert \n to \r\n
+ if (tcsetattr(pts, TCSANOW, &attrs) != 0) {
+ edie("tcsetattr()");
+ }
+
+ pid_t pid = fork();
+ if (pid < 0) {
+ edie("fork()");
+ } else if (pid == 0) {
+ // Child
+ close(ptm);
+
+ // Make ourselves a session leader so we can have our own
+ // controlling terminal
+ if (setsid() < 0) {
+ edie("setsid()");
+ }
+
+#ifdef TIOCSCTTY
+ // Set the pty as the controlling terminal
+ if (ioctl(pts, TIOCSCTTY, 0) != 0) {
+ edie("ioctl(TIOCSCTTY)");
+ }
+#endif
+
+ // Redirect std{in,out,err} to the pty
+ if (dup2(pts, STDIN_FILENO) < 0
+ || dup2(pts, STDOUT_FILENO) < 0
+ || dup2(pts, STDERR_FILENO) < 0) {
+ edie("dup2()");
+ }
+ if (pts > STDERR_FILENO) {
+ xclose(pts);
+ }
+
+ // Run the requested command
+ execvp(args[0], args);
+ edie("execvp(): %s", args[0]);
+ }
+
+ // Parent
+ xclose(pts);
+
+ // Read output from the pty and copy it to stdout
+ char buf[1024];
+ while (true) {
+ ssize_t len = read(ptm, buf, sizeof(buf));
+ if (len > 0) {
+ if (xwrite(STDOUT_FILENO, buf, len) < 0) {
+ edie("write()");
+ }
+ } else if (len == 0) {
+ break;
+ } else if (errno == EINTR) {
+ continue;
+ } else if (errno == EIO) {
+ // Linux reports EIO rather than EOF when pts is closed
+ break;
+ } else {
+ die("read()");
+ }
+ }
+
+ xclose(ptm);
+
+ int wstatus;
+ if (xwaitpid(pid, &wstatus, 0) < 0) {
+ edie("waitpid()");
+ }
+
+ if (WIFEXITED(wstatus)) {
+ return WEXITSTATUS(wstatus);
+ } else if (WIFSIGNALED(wstatus)) {
+ int sig = WTERMSIG(wstatus);
+ fprintf(stderr, "%s: %s: %s\n", cmd, args[0], strsignal(sig));
+ return 128 + sig;
+ } else {
+ return 128;
+ }
+}
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100644
index 0000000..3ed2a9c
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,453 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Running test cases
+
+# ERR trap for tests
+debug_err() {
+ local ret=$? line func file
+ callers | while read -r line func file; do
+ if [ "$func" = source ]; then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ break
+ fi
+ done
+}
+
+# Source a test
+source_test() (
+ set -eE
+ trap debug_err ERR
+
+ if ((${#MAKE[@]})); then
+ # Close the jobserver pipes
+ exec {READY_PIPE}<&- {DONE_PIPE}>&-
+ fi
+
+ cd "$TMP"
+ source "$@"
+)
+
+# Run a test
+run_test() {
+ if ((VERBOSE_ERRORS)); then
+ source_test "$1"
+ else
+ source_test "$1" 2>"$TMP/$TEST.err"
+ fi
+ ret=$?
+
+ if ((${#MAKE[@]})); then
+ # Write one byte to the done pipe
+ printf . >&$DONE_PIPE
+ fi
+
+ case $ret in
+ 0)
+ if ((VERBOSE_TESTS)); then
+ color printf "${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ $EX_SKIP)
+ if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then
+ color printf "${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ *)
+ if ((!VERBOSE_ERRORS)); then
+ cat "$TMP/$TEST.err" >&2
+ fi
+ color printf "${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
+ ;;
+ esac
+
+ return $ret
+}
+
+# Count the tests running in the background
+BG=0
+
+# Run a test in the background
+bg_test() {
+ run_test "$1" &
+ ((++BG))
+}
+
+# Reap a finished background test
+reap_test() {
+ ((BG--))
+
+ case "$1" in
+ 0)
+ ((++passed))
+ ;;
+ $EX_SKIP)
+ ((++skipped))
+ ;;
+ *)
+ ((++failed))
+ ;;
+ esac
+}
+
+# Wait for a background test to finish
+wait_test() {
+ local pid line ret
+
+ while :; do
+ line=$((LINENO + 1))
+ _wait -n -ppid
+ ret=$?
+
+ if [ "${pid:-}" ]; then
+ break
+ else
+ debug "${BASH_SOURCE[0]}" $line "${RED}error $ret${RST}" >&$DUPERR
+ exit 1
+ fi
+ done
+
+ reap_test $ret
+}
+
+# Wait until we're ready to run another test
+wait_ready() {
+ if ((${#MAKE[@]})); then
+ # We'd like to parse the output of jobs -n, but we can't run it in a
+ # subshell or we won't get the right output
+ jobs -n >"$TMP/jobs"
+
+ local job status ret rest
+ while read -r job status ret rest; do
+ case "$status" in
+ Done)
+ reap_test 0
+ ;;
+ Exit)
+ reap_test $ret
+ ;;
+ esac
+ done <"$TMP/jobs"
+
+ # Read one byte from the ready pipe
+ read -r -N1 -u$READY_PIPE
+ elif ((BG >= JOBS)); then
+ wait_test
+ fi
+}
+
+# Run make as a co-process to use its job control
+comake() {
+ coproc {
+ # We can't just use std{in,out}, due to
+ # https://www.gnu.org/software/make/manual/html_node/Parallel-Input.html
+ exec {DONE_PIPE}<&0 {READY_PIPE}>&1
+ exec "${MAKE[@]}" -s \
+ -f "$TESTS/tests.mk" \
+ DONE=$DONE_PIPE \
+ READY=$READY_PIPE \
+ "${!TEST_CASES[@]}" \
+ </dev/null >/dev/null
+ }
+
+ # coproc pipes aren't inherited by subshells, so dup them
+ exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]}
+}
+
+# Print the current test progress
+progress() {
+ if [ "${BAR:-}" ]; then
+ print_bar "$(printf "$@")"
+ elif ((VERBOSE_TESTS)); then
+ color printf "$@"
+ fi
+}
+
+# Run all the tests
+run_tests() {
+ passed=0
+ failed=0
+ skipped=0
+ ran=0
+ total=${#TEST_CASES[@]}
+
+ TEST_FMT="${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n"
+
+ if ((${#MAKE[@]})); then
+ comake
+ fi
+
+ # Turn off set -e (but turn it back on in run_test)
+ set +e
+
+ if ((COLOR_STDOUT && !VERBOSE_TESTS)); then
+ show_bar
+ fi
+
+ for TEST in "${TEST_CASES[@]}"; do
+ wait_ready
+ if ((STOP && failed > 0)); then
+ break
+ fi
+
+ percent=$((100 * ran / total))
+ progress "${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" $percent "$TEST"
+
+ mkdir -p "$TMP/$TEST"
+ OUT="$TMP/$TEST.out"
+
+ bg_test "$TESTS/$TEST.sh"
+ ((++ran))
+ done
+
+ while ((BG > 0)); do
+ wait_test
+ done
+
+ if [ "${BAR:-}" ]; then
+ hide_bar
+ fi
+
+ if ((passed > 0)); then
+ color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total
+ fi
+ if ((skipped > 0)); then
+ color printf "${CYN}[SKIP]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $skipped $total
+ fi
+ if ((failed > 0)); then
+ color printf "${RED}[FAIL]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $failed $total
+ exit 1
+ fi
+}
+
+## Utilities for the tests themselves
+
+# Default return value for failed tests
+EX_FAIL=1
+
+# Fail the current test
+fail() {
+ exit $EX_FAIL
+}
+
+# Return value when a test is skipped
+EX_SKIP=77
+
+# Skip the current test
+skip() {
+ if ((VERBOSE_SKIPPED)); then
+ caller | {
+ read -r line file
+ debug "$file" $line "" >&$DUPOUT
+ }
+ fi
+
+ exit $EX_SKIP
+}
+
+# Run a command and check its exit status
+check_exit() {
+ local expected="$1"
+ local actual=0
+ shift
+ "$@" || actual=$?
+ ((actual == expected))
+}
+
+# Run a command with sudo
+bfs_sudo() {
+ if ((${#SUDO[@]})); then
+ "${SUDO[@]}" "$@"
+ else
+ return 1
+ fi
+}
+
+# Get the inode number of a file
+inum() {
+ ls -id "$@" | awk '{ print $1 }'
+}
+
+# Set an ACL on a file
+set_acl() {
+ case "$UNAME" in
+ Darwin)
+ chmod +a "$(id -un) allow read,write" "$1"
+ ;;
+ FreeBSD)
+ if (($(getconf ACL_NFS4 "$1") > 0)); then
+ setfacl -m "u:$(id -un):rw::allow" "$1"
+ else
+ setfacl -m "u:$(id -un):rw" "$1"
+ fi
+ ;;
+ *)
+ setfacl -m "u:$(id -un):rw" "$1"
+ ;;
+ esac
+}
+
+# Print a bfs invocation for --verbose=commands
+bfs_verbose() {
+ if ((VERBOSE_COMMANDS)); then
+ (
+ # Close some fds to make room for the pipe,
+ # even with extremely low ulimit -n
+ exec >&- {DUPERR}>&-
+ exec >&$DUPOUT {DUPOUT}>&-
+ color bfs_verbose_impl "$@"
+ )
+ fi
+}
+
+bfs_verbose_impl() {
+ printf "${GRN}%q${RST}" "${BFS[0]}"
+ if ((${#BFS[@]} > 1)); then
+ printf " ${GRN}%q${RST}" "${BFS[@]:1}"
+ fi
+
+ local expr_started=0 color
+ for arg; do
+ case "$arg" in
+ -[A-Z]*|-[dsxf]|-j*)
+ color="${CYN}"
+ ;;
+ \(|!|-[ao]|-and|-or|-not|-exclude)
+ expr_started=1
+ color="${RED}"
+ ;;
+ \)|,)
+ if ((expr_started)); then
+ color="${RED}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ -?*)
+ expr_started=1
+ color="${BLU}"
+ ;;
+ *)
+ if ((expr_started)); then
+ color="${BLD}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ esac
+ printf " ${color}%q${RST}" "$arg"
+ done
+
+ printf '\n'
+}
+
+# Run the bfs we're testing
+invoke_bfs() {
+ bfs_verbose "$@"
+
+ local ret=0
+ # Close the logging fds
+ "${BFS[@]}" "$@" {DUPOUT}>&- {DUPERR}>&- || ret=$?
+
+ # Allow bfs to fail, but not crash
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+# Run bfs with a pseudo-terminal attached
+bfs_pty() {
+ bfs_verbose "$@"
+
+ local ret=0
+ "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$?
+
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+# Create a directory tree with xattrs in scratch
+make_xattrs() {
+ cd "$TEST"
+
+ "$XTOUCH" normal xattr xattr_2
+ ln -s xattr link
+ ln -s normal xattr_link
+
+ case "$UNAME" in
+ Darwin)
+ xattr -w bfs_test true xattr \
+ && xattr -w bfs_test_2 true xattr_2 \
+ && xattr -s -w bfs_test true xattr_link
+ ;;
+ FreeBSD)
+ setextattr user bfs_test true xattr \
+ && setextattr user bfs_test_2 true xattr_2 \
+ && setextattr -h user bfs_test true xattr_link
+ ;;
+ *)
+ # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
+ # namespace, which is writable by root and readable by others
+ bfs_sudo setfattr -n security.bfs_test xattr \
+ && bfs_sudo setfattr -n security.bfs_test_2 xattr_2 \
+ && bfs_sudo setfattr -h -n security.bfs_test xattr_link
+ ;;
+ esac
+}
+
+# Get the Unix epoch time in seconds
+epoch_time() {
+ if [ "${EPOCHSECONDS:-}" ]; then
+ # Added in bash 5
+ printf '%d' "$EPOCHSECONDS"
+ else
+ # https://stackoverflow.com/a/12746260/502399
+ awk 'BEGIN { srand(); print srand(); }'
+ fi
+}
+
+## Snapshot testing
+
+# Return value when a difference is detected
+EX_DIFF=20
+
+# Detect colored diff support
+if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null &>/dev/null; then
+ DIFF="diff --color=always"
+else
+ DIFF="diff"
+fi
+
+# Sort the output file
+sort_output() {
+ sort -o "$OUT" "$OUT"
+}
+
+# Diff against the expected output
+diff_output() {
+ local GOLD="$TESTS/$TEST.out"
+
+ if ((UPDATE)); then
+ cp "$OUT" "$GOLD"
+ elif ! cmp -s "$GOLD" "$OUT"; then
+ $DIFF -u "$GOLD" "$OUT" >&$DUPERR
+ fi
+}
+
+# Run bfs, and diff it against the expected output
+bfs_diff() {
+ local ret=0
+ invoke_bfs "$@" >"$OUT" || ret=$?
+
+ sort_output
+ diff_output || exit $EX_DIFF
+
+ return $ret
+}
diff --git a/tests/sighook.c b/tests/sighook.c
new file mode 100644
index 0000000..82e0ae5
--- /dev/null
+++ b/tests/sighook.c
@@ -0,0 +1,228 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "atomic.h"
+#include "bfstd.h"
+#include "sighook.h"
+#include "thread.h"
+#include "xtime.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/** Counts SIGALRM deliveries. */
+static atomic size_t count = 0;
+
+/** SIGALRM handler. */
+static void alrm_hook(int sig, siginfo_t *info, void *arg) {
+ fetch_add(&count, 1, relaxed);
+}
+
+/** SH_ONESHOT counter. */
+static atomic size_t shots = 0;
+
+/** SH_ONESHOT hook. */
+static void alrm_oneshot(int sig, siginfo_t *info, void *arg) {
+ fetch_add(&shots, 1, relaxed);
+}
+
+/** Keeps the background thread alive. */
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
+static bool done = false;
+
+/** Background thread that receives signals. */
+static void *hook_thread(void *ptr) {
+ mutex_lock(&mutex);
+ while (!done) {
+ cond_wait(&cond, &mutex);
+ }
+ mutex_unlock(&mutex);
+ return NULL;
+}
+
+/** Block a signal in this thread. */
+static int block_signal(int sig, sigset_t *old) {
+ sigset_t set;
+ if (sigemptyset(&set) != 0) {
+ return -1;
+ }
+ if (sigaddset(&set, sig) != 0) {
+ return -1;
+ }
+
+ errno = pthread_sigmask(SIG_BLOCK, &set, old);
+ if (errno != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Tests for sighook(). */
+static void check_hooks(void) {
+ struct sighook *hook = NULL;
+ struct sighook *oneshot = NULL;
+
+ hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ if (!bfs_echeck(hook, "sighook(SIGALRM)")) {
+ return;
+ }
+
+ // Create a background thread to receive SIGALRM
+ pthread_t thread;
+ if (!bfs_echeck(thread_create(&thread, NULL, hook_thread, NULL) == 0)) {
+ goto unhook;
+ }
+
+ // Block SIGALRM in this thread so the handler runs concurrently with
+ // sighook()/sigunhook()
+ sigset_t mask;
+ if (!bfs_echeck(block_signal(SIGALRM, &mask) == 0)) {
+ goto unthread;
+ }
+
+ // Check that we can unregister and re-register a hook
+ sigunhook(hook);
+ hook = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ if (!bfs_echeck(hook, "sighook(SIGALRM)")) {
+ goto unblock;
+ }
+
+ // Test SH_ONESHOT
+ oneshot = sighook(SIGALRM, alrm_oneshot, NULL, SH_ONESHOT);
+ if (!bfs_echeck(oneshot, "sighook(SH_ONESHOT)")) {
+ goto unblock;
+ }
+
+ // Create a timer that sends SIGALRM every 100 microseconds
+ const struct timespec ival = { .tv_nsec = 100 * 1000 };
+ struct timer *timer = xtimer_start(&ival);
+ if (!bfs_echeck(timer, "xtimer_start()")) {
+ goto unblock;
+ }
+
+ // Rapidly register/unregister SIGALRM hooks
+ size_t alarms;
+ while (alarms = load(&count, relaxed), alarms < 1000) {
+ size_t nshots = load(&shots, relaxed);
+ bfs_check(nshots <= 1);
+ if (alarms > 1) {
+ bfs_check(nshots == 1);
+ }
+ if (alarms >= 500) {
+ sigunhook(oneshot);
+ oneshot = NULL;
+ }
+
+ struct sighook *next = sighook(SIGALRM, alrm_hook, NULL, SH_CONTINUE);
+ if (!bfs_echeck(next, "sighook(SIGALRM)")) {
+ break;
+ }
+
+ sigunhook(hook);
+ hook = next;
+ }
+
+ // Stop the timer
+ xtimer_stop(timer);
+unblock:
+ // Restore the old signal mask
+ errno = pthread_sigmask(SIG_SETMASK, &mask, NULL);
+ bfs_echeck(errno == 0, "pthread_sigmask()");
+unthread:
+ // Quit the background thread
+ mutex_lock(&mutex);
+ done = true;
+ mutex_unlock(&mutex);
+ cond_signal(&cond);
+ thread_join(thread, NULL);
+unhook:
+ // Unregister the SIGALRM hooks
+ sigunhook(oneshot);
+ sigunhook(hook);
+}
+
+/** atsigexit() hook. */
+static void exit_hook(int sig, siginfo_t *info, void *arg) {
+ // Write the signal that's killing us to the pipe
+ int *pipes = arg;
+ if (xwrite(pipes[1], &sig, sizeof(sig)) != sizeof(sig)) {
+ abort();
+ }
+}
+
+/** Tests for atsigexit(). */
+static void check_sigexit(int sig) {
+ // To wait for the child to call atsigexit()
+ int ready[2];
+ bfs_everify(pipe(ready) == 0);
+
+ // Written in the atsigexit() handler
+ int killed[2];
+ bfs_everify(pipe(killed) == 0);
+
+ pid_t pid;
+ bfs_everify((pid = fork()) >= 0);
+
+ if (pid > 0) {
+ // Parent
+ xclose(ready[1]);
+ xclose(killed[1]);
+
+ // Wait for the child to call atsigexit()
+ char c;
+ bfs_everify(xread(ready[0], &c, 1) == 1);
+
+ // Kill the child with the signal
+ bfs_everify(kill(pid, sig) == 0);
+
+ // Check that the child died to the right signal
+ int wstatus;
+ if (bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)) {
+ bfs_check(WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == sig);
+ }
+
+ // Check that the signal hook wrote the signal number to the pipe
+ int hsig;
+ if (bfs_echeck(xread(killed[0], &hsig, sizeof(hsig)) == sizeof(hsig))) {
+ bfs_check(hsig == sig);
+ }
+ } else {
+ // Child
+ xclose(ready[0]);
+ xclose(killed[0]);
+
+ // exit_hook() will write to killed[1]
+ bfs_everify(atsigexit(exit_hook, killed) != NULL);
+
+ // Tell the parent we're ready
+ bfs_everify(xwrite(ready[1], "A", 1) == 1);
+
+ // Wait until we're killed
+ const struct timespec dur = { .tv_nsec = 1 };
+ while (true) {
+ nanosleep(&dur, NULL);
+ }
+ }
+}
+
+void check_sighook(void) {
+ check_hooks();
+
+ check_sigexit(SIGINT);
+ check_sigexit(SIGQUIT);
+ check_sigexit(SIGPIPE);
+
+ // macOS cannot distinguish between sync and async SIG{BUS,ILL,SEGV}
+#if !__APPLE__
+ check_sigexit(SIGSEGV);
+#endif
+}
diff --git a/tests/sort-args.sh b/tests/sort-args.sh
index f801d3b..227cc1a 100755
--- a/tests/sort-args.sh
+++ b/tests/sort-args.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
-args=($({ for arg; do echo "$arg"; done } | sort))
+IFS=$'\n' read -rd '' -a args < <(printf '%s\n' "$@" | sort)
echo "${args[@]}"
diff --git a/tests/stddirs.sh b/tests/stddirs.sh
new file mode 100644
index 0000000..1569fee
--- /dev/null
+++ b/tests/stddirs.sh
@@ -0,0 +1,181 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Standard directory trees for tests
+
+# Creates a simple file+directory structure for tests
+make_basic() {
+ "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
+ "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
+ echo baz >"$1/l/foo/bar/baz"
+}
+
+# Creates a file+directory structure with various permissions for tests
+make_perms() {
+ "$XTOUCH" -p -M000 "$1/f---------"
+ "$XTOUCH" -p -M111 "$1/f--x--x--x"
+ "$XTOUCH" -p -M222 "$1/f-w--w--w-"
+ "$XTOUCH" -p -M333 "$1/f-wx-wx-wx"
+ "$XTOUCH" -p -M444 "$1/fr--r--r--"
+ "$XTOUCH" -p -M555 "$1/fr-xr-xr-x" "$1/dr-xr-xr-x/"
+ "$XTOUCH" -p -M666 "$1/frw-rw-rw-"
+ "$XTOUCH" -p -M777 "$1/frwxrwxrwx" "$1/drwxrwxrwx/"
+
+ "$XTOUCH" -p -M220 "$1/f-w--w----"
+ "$XTOUCH" -p -M331 "$1/f-wx-wx--x"
+ "$XTOUCH" -p -M664 "$1/frw-rw-r--"
+ "$XTOUCH" -p -M775 "$1/frwxrwxr-x" "$1/drwxrwxr-x/"
+
+ "$XTOUCH" -p -M311 "$1/f-wx--x--x"
+ "$XTOUCH" -p -M644 "$1/frw-r--r--"
+ "$XTOUCH" -p -M755 "$1/frwxr-xr-x" "$1/drwxr-xr-x/"
+
+ "$XTOUCH" -p -M100 "$1/f--x------"
+ "$XTOUCH" -p -M200 "$1/f-w-------"
+ "$XTOUCH" -p -M300 "$1/f-wx------"
+ "$XTOUCH" -p -M400 "$1/fr--------"
+ "$XTOUCH" -p -M500 "$1/fr-x------" "$1/dr-x------/"
+ "$XTOUCH" -p -M600 "$1/frw-------"
+ "$XTOUCH" -p -M700 "$1/frwxr-----" "$1/drwx------/"
+}
+
+# Creates a file+directory structure with various symbolic and hard links
+make_links() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln "$1/file" "$1/hardlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
+ ln -s file "$1/deeply/nested/link"
+ ln -s nowhere "$1/deeply/nested/broken"
+ ln -s deeply/nested "$1/skip"
+}
+
+# Creates a file+directory structure with symbolic link loops
+make_loops() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ ln -s loop "$1/loop"
+ mkdir -p "$1/deeply/nested/dir"
+ ln -s ../../deeply "$1/deeply/nested/loop"
+ ln -s deeply/nested/loop/nested "$1/skip"
+}
+
+# Creates a file+directory structure with inaccessible files
+make_inaccessible() {
+ "$XTOUCH" -p -M000 "$1/file" "$1/dir/"
+ ln -s dir/file "$1/link"
+}
+
+# Creates a file+directory structure with varying timestamps
+make_times() {
+ "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
+ "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
+ "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
+ ln -s a "$1/l"
+ "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
+ "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
+}
+
+# Creates a file+directory structure with various weird file/directory names
+make_weirdnames() {
+ "$XTOUCH" -p "$1/-/a"
+ "$XTOUCH" -p "$1/(/b"
+ "$XTOUCH" -p "$1/(-/c"
+ "$XTOUCH" -p "$1/!/d"
+ "$XTOUCH" -p "$1/!-/e"
+ "$XTOUCH" -p "$1/,/f"
+ "$XTOUCH" -p "$1/)/g"
+ "$XTOUCH" -p "$1/.../h"
+ "$XTOUCH" -p "$1/\\/i"
+ "$XTOUCH" -p "$1/ /j"
+ "$XTOUCH" -p "$1/[/k"
+ "$XTOUCH" -p "$1/{/l"
+ "$XTOUCH" -p "$1/*/m"
+ "$XTOUCH" -p "$1/"$'\n/n'
+}
+
+# Creates a very deep directory structure for testing PATH_MAX handling
+make_deep() {
+ mkdir -p "$1"
+
+ # $name will be 255 characters, aka _XOPEN_NAME_MAX
+ local name="0123456789ABCDEF"
+ name="$name$name$name$name"
+ name="$name$name$name$name"
+ name="${name:0:255}"
+
+ # 4 * 4 * 256 == 4096 >= PATH_MAX
+ local path="$name/$name/$name/$name"
+ path="$path/$path/$path/$path"
+
+ "$XTOUCH" -p "$1"/{{0..9},A,B,C,D,E,F}/"$path/$name"
+}
+
+# Creates a directory structure with many different types, and therefore colors
+make_rainbow() {
+ "$XTOUCH" -p "$1/file.txt"
+ "$XTOUCH" -p "$1/file.dat"
+ "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
+ "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
+ "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
+ ln -s file.txt "$1/link.txt"
+ "$XTOUCH" -p "$1/mh1"
+ ln "$1/mh1" "$1/mh2"
+ mkfifo "$1/pipe"
+ # TODO: block
+ ln -s /dev/null "$1/chardev_link"
+ ln -s nowhere "$1/broken"
+ "$MKSOCK" "$1/socket"
+ "$XTOUCH" -p "$1"/s{u,g,ug}id
+ chmod 06644 "$1"/sugid
+ chmod 04644 "$1"/suid
+ chmod 02644 "$1"/sgid
+ mkdir "$1/ow" "$1"/sticky{,_ow}
+ chmod o+w "$1"/*ow
+ chmod +t "$1"/sticky*
+ "$XTOUCH" -p "$1"/exec.sh
+ chmod +x "$1"/exec.sh
+ "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
+}
+
+# Create all standard directory trees
+make_stddirs() {
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
+
+ if ((CLEAN)); then
+ defer clean_stddirs
+ else
+ color printf "Test files saved to ${BLD}%s${RST}\n" "$TMP"
+ fi
+
+ chown "$(id -u):$(id -g)" "$TMP"
+
+ make_basic "$TMP/basic"
+ make_perms "$TMP/perms"
+ make_links "$TMP/links"
+ make_loops "$TMP/loops"
+ make_inaccessible "$TMP/inaccessible"
+ make_times "$TMP/times"
+ make_weirdnames "$TMP/weirdnames"
+ make_deep "$TMP/deep"
+ make_rainbow "$TMP/rainbow"
+}
+
+# Clean up temporary directories on exit
+clean_stddirs() {
+ # Don't force rm to deal with long paths
+ for dir in "$TMP"/deep/*/*; do
+ if [ -d "$dir" ]; then
+ (cd "$dir" && rm -rf *)
+ fi
+ done
+
+ chmod -R +rwX "$TMP"
+ rm -rf "$TMP"
+}
diff --git a/tests/test_L_acl.out b/tests/test_L_acl.out
deleted file mode 100644
index 1dae00a..0000000
--- a/tests/test_L_acl.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/acl
-scratch/link
diff --git a/tests/test_L_capable.out b/tests/test_L_capable.out
deleted file mode 100644
index e5ba3c7..0000000
--- a/tests/test_L_capable.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/capable
-scratch/link
diff --git a/tests/test_L_delete.out b/tests/test_L_delete.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_L_delete.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_L_mount.out b/tests/test_L_mount.out
deleted file mode 100644
index 2c3c590..0000000
--- a/tests/test_L_mount.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
-scratch/foo/qux
diff --git a/tests/test_L_xattr.out b/tests/test_L_xattr.out
deleted file mode 100644
index 12fac95..0000000
--- a/tests/test_L_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/link
-scratch/xattr
-scratch/xattr_2
diff --git a/tests/test_L_xattrname.out b/tests/test_L_xattrname.out
deleted file mode 100644
index 4dc4836..0000000
--- a/tests/test_L_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/xattr
diff --git a/tests/test_L_xdev.out b/tests/test_L_xdev.out
deleted file mode 100644
index 2c3c590..0000000
--- a/tests/test_L_xdev.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
-scratch/foo/qux
diff --git a/tests/test_S_bfs.out b/tests/test_S_bfs.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_S_bfs.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_S_ids.out b/tests/test_S_ids.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_S_ids.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_acl.out b/tests/test_acl.out
deleted file mode 100644
index ddf8446..0000000
--- a/tests/test_acl.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/acl
diff --git a/tests/test_basic.out b/tests/test_basic.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_basic.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_capable.out b/tests/test_capable.out
deleted file mode 100644
index 78b5bd9..0000000
--- a/tests/test_capable.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/capable
diff --git a/tests/test_closed_stdin.out b/tests/test_closed_stdin.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_closed_stdin.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_color_ls.out b/tests/test_color_ls.out
deleted file mode 100644
index b08d894..0000000
--- a/tests/test_color_ls.out
+++ /dev/null
@@ -1,12 +0,0 @@
-scratch/foo/bar
-scratch/foo/bar
-/__bfs__/nowhere
-/__bfs__/nowhere
-foo/bar/baz/qux
-foo/bar/baz/qux
-foo/bar/nowhere
-foo/bar/nowhere
-foo/bar/nowhere/nothing
-foo/bar/nowhere/nothing
-foo/bar/baz
-foo/bar/baz
diff --git a/tests/test_color_mi.out b/tests/test_color_mi.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_mi.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_nul.out b/tests/test_color_nul.out
deleted file mode 100644
index c328f82..0000000
--- a/tests/test_color_nul.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_color_or0_mi.out b/tests/test_color_or0_mi.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_or0_mi.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_or0_mi0.out b/tests/test_color_or0_mi0.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_or0_mi0.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_st0_tw0_ow0.out b/tests/test_color_st0_tw0_ow0.out
deleted file mode 100644
index 2b86fe4..0000000
--- a/tests/test_color_st0_tw0_ow0.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sgid
-rainbow/pipe
-rainbow/sugid
-rainbow/suid
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_star.out b/tests/test_color_star.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_star.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_d_path.out b/tests/test_d_path.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_d_path.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_data_flow_group.out b/tests/test_data_flow_group.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_group.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_data_flow_hidden.out b/tests/test_data_flow_hidden.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_hidden.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_data_flow_sparse.out b/tests/test_data_flow_sparse.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_sparse.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_data_flow_user.out b/tests/test_data_flow_user.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_user.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_daystart.out b/tests/test_daystart.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_daystart.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_daystart_twice.out b/tests/test_daystart_twice.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_daystart_twice.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_delete.out b/tests/test_delete.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_delete_many.out b/tests/test_delete_many.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete_many.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_depth.out b/tests/test_depth.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_depth.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_depth_error.out b/tests/test_depth_error.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_depth_error.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_depth_overflow.out b/tests/test_depth_overflow.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_depth_overflow.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_empty_special.out b/tests/test_empty_special.out
deleted file mode 100644
index 3927f2b..0000000
--- a/tests/test_empty_special.out
+++ /dev/null
@@ -1,14 +0,0 @@
-rainbow/exec.sh
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/ow
-rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
-rainbow/sticky
-rainbow/sticky_ow
-rainbow/sugid
-rainbow/suid
diff --git a/tests/test_exec.out b/tests/test_exec.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_exec.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_exec_plus_status.out b/tests/test_exec_plus_status.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_exec_plus_status.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_executable.out b/tests/test_executable.out
deleted file mode 100644
index 49c1b21..0000000
--- a/tests/test_executable.out
+++ /dev/null
@@ -1,4 +0,0 @@
-perms
-perms/rwx
-perms/rx
-perms/wx
diff --git a/tests/test_flags.out b/tests/test_flags.out
deleted file mode 100644
index 11998ed..0000000
--- a/tests/test_flags.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/bar
diff --git a/tests/test_fstype.out b/tests/test_fstype.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_fstype.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid.out b/tests/test_gid.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid_minus.out b/tests/test_gid_minus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_minus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid_minus_plus.out b/tests/test_gid_minus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_minus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid_name.out b/tests/test_gid_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid_plus.out b/tests/test_gid_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_gid_plus_plus.out b/tests/test_gid_plus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_plus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_group_id.out b/tests/test_group_id.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_id.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_group_name.out b/tests/test_group_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_group_nogroup.out b/tests/test_group_nogroup.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_nogroup.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_inum_bind_mount.out b/tests/test_inum_bind_mount.out
deleted file mode 100644
index a520de3..0000000
--- a/tests/test_inum_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/bar
-scratch/foo
diff --git a/tests/test_inum_mount.out b/tests/test_inum_mount.out
deleted file mode 100644
index 99c7511..0000000
--- a/tests/test_inum_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/mnt
diff --git a/tests/test_mount.out b/tests/test_mount.out
deleted file mode 100644
index 005bdcf..0000000
--- a/tests/test_mount.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
diff --git a/tests/test_path_d.out b/tests/test_path_d.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_path_d.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_perm_000.out b/tests/test_perm_000.out
deleted file mode 100644
index 5fd30bc..0000000
--- a/tests/test_perm_000.out
+++ /dev/null
@@ -1 +0,0 @@
-perms/0
diff --git a/tests/test_perm_000_minus.out b/tests/test_perm_000_minus.out
deleted file mode 100644
index d7494b8..0000000
--- a/tests/test_perm_000_minus.out
+++ /dev/null
@@ -1,8 +0,0 @@
-perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_000_plus.out b/tests/test_perm_000_plus.out
deleted file mode 100644
index d7494b8..0000000
--- a/tests/test_perm_000_plus.out
+++ /dev/null
@@ -1,8 +0,0 @@
-perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_000_slash.out b/tests/test_perm_000_slash.out
deleted file mode 100644
index d7494b8..0000000
--- a/tests/test_perm_000_slash.out
+++ /dev/null
@@ -1,8 +0,0 @@
-perms
-perms/0
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_222.out b/tests/test_perm_222.out
deleted file mode 100644
index 1690e43..0000000
--- a/tests/test_perm_222.out
+++ /dev/null
@@ -1 +0,0 @@
-perms/w
diff --git a/tests/test_perm_222_minus.out b/tests/test_perm_222_minus.out
deleted file mode 100644
index 1690e43..0000000
--- a/tests/test_perm_222_minus.out
+++ /dev/null
@@ -1 +0,0 @@
-perms/w
diff --git a/tests/test_perm_222_plus.out b/tests/test_perm_222_plus.out
deleted file mode 100644
index 9a5b95a..0000000
--- a/tests/test_perm_222_plus.out
+++ /dev/null
@@ -1,5 +0,0 @@
-perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
diff --git a/tests/test_perm_222_slash.out b/tests/test_perm_222_slash.out
deleted file mode 100644
index 9a5b95a..0000000
--- a/tests/test_perm_222_slash.out
+++ /dev/null
@@ -1,5 +0,0 @@
-perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
diff --git a/tests/test_perm_644.out b/tests/test_perm_644.out
deleted file mode 100644
index 4e64e49..0000000
--- a/tests/test_perm_644.out
+++ /dev/null
@@ -1 +0,0 @@
-perms/rw
diff --git a/tests/test_perm_644_minus.out b/tests/test_perm_644_minus.out
deleted file mode 100644
index 2e2576b..0000000
--- a/tests/test_perm_644_minus.out
+++ /dev/null
@@ -1,3 +0,0 @@
-perms
-perms/rw
-perms/rwx
diff --git a/tests/test_perm_644_plus.out b/tests/test_perm_644_plus.out
deleted file mode 100644
index 7e5ae98..0000000
--- a/tests/test_perm_644_plus.out
+++ /dev/null
@@ -1,7 +0,0 @@
-perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_644_slash.out b/tests/test_perm_644_slash.out
deleted file mode 100644
index 7e5ae98..0000000
--- a/tests/test_perm_644_slash.out
+++ /dev/null
@@ -1,7 +0,0 @@
-perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_leading_plus_symbolic_slash.out b/tests/test_perm_leading_plus_symbolic_slash.out
deleted file mode 100644
index 7e5ae98..0000000
--- a/tests/test_perm_leading_plus_symbolic_slash.out
+++ /dev/null
@@ -1,7 +0,0 @@
-perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_perm_symbolic_minus.out b/tests/test_perm_symbolic_minus.out
deleted file mode 100644
index 2e2576b..0000000
--- a/tests/test_perm_symbolic_minus.out
+++ /dev/null
@@ -1,3 +0,0 @@
-perms
-perms/rw
-perms/rwx
diff --git a/tests/test_perm_symbolic_slash.out b/tests/test_perm_symbolic_slash.out
deleted file mode 100644
index 7e5ae98..0000000
--- a/tests/test_perm_symbolic_slash.out
+++ /dev/null
@@ -1,7 +0,0 @@
-perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
-perms/w
-perms/wx
diff --git a/tests/test_permcopy.out b/tests/test_permcopy.out
deleted file mode 100644
index 4e64e49..0000000
--- a/tests/test_permcopy.out
+++ /dev/null
@@ -1 +0,0 @@
-perms/rw
diff --git a/tests/test_printf_Y_error.out b/tests/test_printf_Y_error.out
deleted file mode 100644
index 00d6ee7..0000000
--- a/tests/test_printf_Y_error.out
+++ /dev/null
@@ -1,3 +0,0 @@
-(scratch) () d d
-(scratch/foo) () d d
-(scratch/bar) (foo/bar) l ?
diff --git a/tests/test_printf_nul.out b/tests/test_printf_nul.out
deleted file mode 100644
index 6833fdd..0000000
--- a/tests/test_printf_nul.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_readable.out b/tests/test_readable.out
deleted file mode 100644
index 386feba..0000000
--- a/tests/test_readable.out
+++ /dev/null
@@ -1,5 +0,0 @@
-perms
-perms/r
-perms/rw
-perms/rwx
-perms/rx
diff --git a/tests/test_rm.out b/tests/test_rm.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_rm.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_stderr_fails_silently.out b/tests/test_stderr_fails_silently.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_stderr_fails_silently.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_true.out b/tests/test_true.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_true.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_type_bind_mount.out b/tests/test_type_bind_mount.out
deleted file mode 100644
index 6435159..0000000
--- a/tests/test_type_bind_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/null
diff --git a/tests/test_uid.out b/tests/test_uid.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_uid_minus.out b/tests/test_uid_minus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_minus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_uid_minus_plus.out b/tests/test_uid_minus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_minus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_uid_name.out b/tests/test_uid_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_uid_plus.out b/tests/test_uid_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_uid_plus_plus.out b/tests/test_uid_plus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_plus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_unique_depth.out b/tests/test_unique_depth.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_unique_depth.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_user_id.out b/tests/test_user_id.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_id.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_user_name.out b/tests/test_user_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_user_nouser.out b/tests/test_user_nouser.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_nouser.out
+++ /dev/null
@@ -1,19 +0,0 @@
-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/test_writable.out b/tests/test_writable.out
deleted file mode 100644
index 9a5b95a..0000000
--- a/tests/test_writable.out
+++ /dev/null
@@ -1,5 +0,0 @@
-perms
-perms/rw
-perms/rwx
-perms/w
-perms/wx
diff --git a/tests/test_xattr.out b/tests/test_xattr.out
deleted file mode 100644
index 109e7c9..0000000
--- a/tests/test_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/xattr
-scratch/xattr_2
-scratch/xattr_link
diff --git a/tests/test_xattrname.out b/tests/test_xattrname.out
deleted file mode 100644
index 0285ac1..0000000
--- a/tests/test_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/xattr
-scratch/xattr_link
diff --git a/tests/test_xdev.out b/tests/test_xdev.out
deleted file mode 100644
index 005bdcf..0000000
--- a/tests/test_xdev.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
diff --git a/tests/test_xtype_bind_mount.out b/tests/test_xtype_bind_mount.out
deleted file mode 100644
index 16804ea..0000000
--- a/tests/test_xtype_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/null
diff --git a/tests/tests.h b/tests/tests.h
new file mode 100644
index 0000000..d395c7c
--- /dev/null
+++ b/tests/tests.h
@@ -0,0 +1,74 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Unit tests.
+ */
+
+#ifndef BFS_TESTS_H
+#define BFS_TESTS_H
+
+#include "bfstd.h"
+#include "diag.h"
+
+/** Memory allocation tests. */
+void check_alloc(void);
+
+/** Standard library wrapper tests. */
+void check_bfstd(void);
+
+/** Bit manipulation tests. */
+void check_bit(void);
+
+/** I/O queue tests. */
+void check_ioq(void);
+
+/** Linked list tests. */
+void check_list(void);
+
+/** Signal hook tests. */
+void check_sighook(void);
+
+/** Trie tests. */
+void check_trie(void);
+
+/** Process spawning tests. */
+void check_xspawn(void);
+
+/** Time tests. */
+void check_xtime(void);
+
+/** Record a single check and return the result. */
+bool bfs_check_impl(bool result);
+
+/**
+ * Check a condition, logging a message on failure but continuing.
+ */
+#define bfs_check(...) \
+ bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", )
+
+#define bfs_check_(str, cond, format, ...) \
+ bfs_check_impl((cond) || (bfs_check__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false))
+
+#define bfs_check__(format, ...) \
+ bfs_diagf(sizeof(format) > 1 \
+ ? BFS_DIAG_FORMAT_("%s" format "%s") \
+ : BFS_DIAG_FORMAT_("Check failed: `%s`"), \
+ BFS_DIAG_ARGS_(__VA_ARGS__))
+
+/**
+ * Check a condition, logging the current error string on failure.
+ */
+#define bfs_echeck(...) \
+ bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", )
+
+#define bfs_echeck_(str, cond, format, ...) \
+ bfs_check_impl((cond) || (bfs_echeck__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false))
+
+#define bfs_echeck__(format, ...) \
+ bfs_diagf(sizeof(format) > 1 \
+ ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \
+ : BFS_DIAG_FORMAT_("Check failed: `%s`: %s"), \
+ BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), ))
+
+#endif // BFS_TESTS_H
diff --git a/tests/tests.mk b/tests/tests.mk
new file mode 100644
index 0000000..035ca79
--- /dev/null
+++ b/tests/tests.mk
@@ -0,0 +1,13 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that exposes make's job control to tests.sh
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# Turn off implicit rules
+.SUFFIXES:
+
+.DEFAULT::
+ bash -c 'printf . >&$(READY) && read -r -N1 -u$(DONE)'
diff --git a/tests/tests.sh b/tests/tests.sh
new file mode 100755
index 0000000..3890243
--- /dev/null
+++ b/tests/tests.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+set -euP
+umask 022
+
+TESTS="$(dirname -- "${BASH_SOURCE[0]}")"
+. "$TESTS/util.sh"
+. "$TESTS/color.sh"
+. "$TESTS/stddirs.sh"
+. "$TESTS/getopts.sh"
+. "$TESTS/run.sh"
+
+stdenv
+drop_root "$@"
+parse_args "$@"
+make_stddirs
+run_tests
diff --git a/tests/trie.c b/tests/trie.c
index 70e2414..59bde40 100644
--- a/tests/trie.c
+++ b/tests/trie.c
@@ -1,11 +1,16 @@
-#undef NDEBUG
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "bfs.h"
+#include "diag.h"
+#include "trie.h"
-#include "../trie.h"
-#include <assert.h>
#include <stdlib.h>
#include <string.h>
-const char *keys[] = {
+static const char *keys[] = {
"foo",
"bar",
"baz",
@@ -15,9 +20,11 @@ const char *keys[] = {
"quuuux",
"pre",
- "pref",
"prefi",
+ "pref",
"prefix",
+ "p",
+ "pRefix",
"AAAA",
"AADD",
@@ -32,16 +39,20 @@ const char *keys[] = {
">>>>>>",
">>><<<",
">>>",
+
+ "AAAAAAA",
+ "AAAAAAAB",
+ "AAAAAAAa",
};
-const size_t nkeys = sizeof(keys) / sizeof(keys[0]);
+static const size_t nkeys = countof(keys);
-int main(void) {
+void check_trie(void) {
struct trie trie;
trie_init(&trie);
for (size_t i = 0; i < nkeys; ++i) {
- assert(!trie_find_str(&trie, keys[i]));
+ bfs_check(!trie_find_str(&trie, keys[i]));
const char *prefix = NULL;
for (size_t j = 0; j < i; ++j) {
@@ -54,27 +65,39 @@ int main(void) {
struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]);
if (prefix) {
- assert(leaf);
- assert(strcmp(prefix, leaf->key) == 0);
+ bfs_verify(leaf);
+ bfs_check(strcmp(prefix, leaf->key) == 0);
} else {
- assert(!leaf);
+ bfs_check(!leaf);
}
leaf = trie_insert_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ bfs_check(strcmp(keys[i], leaf->key) == 0);
+ bfs_check(leaf->length == strlen(keys[i]) + 1);
+ }
+
+ {
+ size_t i = 0;
+ for_trie (leaf, &trie) {
+ bfs_check(leaf == trie_find_str(&trie, keys[i]));
+ bfs_check(leaf == trie_insert_str(&trie, keys[i]));
+ bfs_check(!leaf->prev || leaf->prev->next == leaf);
+ bfs_check(!leaf->next || leaf->next->prev == leaf);
+ ++i;
+ }
+ bfs_check(i == nkeys);
}
for (size_t i = 0; i < nkeys; ++i) {
struct trie_leaf *leaf = trie_find_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ bfs_check(strcmp(keys[i], leaf->key) == 0);
+ bfs_check(leaf->length == strlen(keys[i]) + 1);
trie_remove(&trie, leaf);
leaf = trie_find_str(&trie, keys[i]);
- assert(!leaf);
+ bfs_check(!leaf);
const char *postfix = NULL;
for (size_t j = i + 1; j < nkeys; ++j) {
@@ -87,31 +110,34 @@ int main(void) {
leaf = trie_find_postfix(&trie, keys[i]);
if (postfix) {
- assert(leaf);
- assert(strcmp(postfix, leaf->key) == 0);
+ bfs_verify(leaf);
+ bfs_check(strcmp(postfix, leaf->key) == 0);
} else {
- assert(!leaf);
+ bfs_check(!leaf);
}
}
- // This tests the "jump" node handline on 32-bit platforms
+ for_trie (leaf, &trie) {
+ bfs_check(false, "trie should be empty");
+ }
+
+ // This tests the "jump" node handling on 32-bit platforms
size_t longsize = 1 << 20;
char *longstr = malloc(longsize);
- assert(longstr);
+ bfs_verify(longstr);
memset(longstr, 0xAC, longsize);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr + longsize/2, 0xAB, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr + longsize / 2, 0xAB, longsize / 2);
+ bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr, 0xAA, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr, 0xAA, longsize / 2);
+ bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ bfs_check(trie_insert_mem(&trie, longstr, longsize));
free(longstr);
trie_destroy(&trie);
- return EXIT_SUCCESS;
}
diff --git a/tests/util.sh b/tests/util.sh
new file mode 100644
index 0000000..1718a1a
--- /dev/null
+++ b/tests/util.sh
@@ -0,0 +1,217 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Utility functions
+
+# Portable realpath(1)
+_realpath() (
+ cd "$(dirname -- "$1")"
+ echo "$PWD/$(basename -- "$1")"
+)
+
+# Globals
+ROOT=$(_realpath "$(dirname -- "$TESTS")")
+TESTS="$ROOT/tests"
+BIN="$ROOT/bin"
+MKSOCK="$BIN/tests/mksock"
+PTYX="$BIN/tests/ptyx"
+XTOUCH="$BIN/tests/xtouch"
+UNAME=$(uname)
+
+# Standardize the environment
+stdenv() {
+ export LC_ALL=C
+ export TZ=UTC0
+
+ local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0"
+ export ASAN_OPTIONS="$SAN_OPTIONS"
+ export LSAN_OPTIONS="$SAN_OPTIONS"
+ export MSAN_OPTIONS="$SAN_OPTIONS"
+ export TSAN_OPTIONS="$SAN_OPTIONS"
+ export UBSAN_OPTIONS="$SAN_OPTIONS"
+
+ export LS_COLORS=""
+ unset BFS_COLORS
+
+ if [ "$UNAME" = Darwin ]; then
+ # ASan on macOS likes to report
+ #
+ # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
+ #
+ # to syslog, which as a side effect opens a socket which might take the
+ # place of one of the standard streams if the process is launched with
+ # it closed. This environment variable avoids the message.
+ export MallocNanoZone=0
+ fi
+
+ # Count the inherited FDs
+ if [ -d /proc/self/fd ]; then
+ local fds=/proc/self/fd
+ else
+ local fds=/dev/fd
+ fi
+ # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting
+ # internal bash fds that are not exposed to spawned processes
+ NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l)
+ NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3))
+
+ # Close stdin so bfs doesn't think we're interactive
+ # dup() the standard fds for logging even when redirected
+ exec </dev/null {DUPOUT}>&1 {DUPERR}>&2
+
+ # Get the ttyname
+ if [ -t $DUPOUT ]; then
+ TTY=$(tty <&$DUPOUT)
+ elif [ -t $DUPERR ]; then
+ TTY=$(tty <&$DUPERR)
+ else
+ TTY=
+ fi
+}
+
+# Drop root privileges or bail
+drop_root() {
+ if command -v capsh &>/dev/null; then
+ if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
+ if [ -n "${BFS_TRIED_DROP:-}" ]; then
+ color cat >&2 <<EOF
+${RED}error:${RST} Failed to drop capabilities.
+EOF
+
+ exit 1
+ fi
+
+ color cat >&2 <<EOF
+${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
+${BLD}cap_dac_read_search${RST}.
+
+EOF
+
+ BFS_TRIED_DROP=y exec capsh \
+ --drop=cap_dac_override,cap_dac_read_search \
+ --caps=cap_dac_override,cap_dac_read_search-eip \
+ -- "$0" "$@"
+ fi
+ elif ((EUID == 0)); then
+ UNLESS=
+ if [ "$UNAME" = "Linux" ]; then
+ UNLESS=" unless ${GRN}capsh${RST} is installed"
+ fi
+
+ color cat >&2 <<EOF
+${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
+will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
+EOF
+ exit 1
+ fi
+}
+
+## Debugging
+
+# Get the bash call stack
+callers() {
+ local frame=0
+ while caller $frame; do
+ ((++frame))
+ done
+}
+
+# Print a message including path, line number, and command
+debug() {
+ local file="$1"
+ local line="$2"
+ local msg="$3"
+ local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
+ file="${file/#*\/tests\//tests/}"
+
+ color printf "${BLD}%s:%d:${RST} %s\n %s\n" "$file" "$line" "$msg" "$cmd"
+}
+
+## Deferred cleanup
+
+# Quote a command safely for eval
+quote() {
+ printf '%q' "$1"
+ shift
+ if (($# > 0)); then
+ printf ' %q' "$@"
+ fi
+}
+
+DEFER_LEVEL=-1
+
+# Run a command when this (sub)shell exits
+defer() {
+ # Check if the EXIT trap is already set
+ if ((DEFER_LEVEL != BASH_SUBSHELL)); then
+ DEFER_LEVEL=$BASH_SUBSHELL
+ DEFER_CMDS=()
+ DEFER_LINES=()
+ DEFER_FILES=()
+ trap pop_defers EXIT
+ fi
+
+ DEFER_CMDS+=("$(quote "$@")")
+
+ local line file
+ read -r line file < <(caller)
+ DEFER_LINES+=("$line")
+ DEFER_FILES+=("$file")
+}
+
+# Pop a single command from the defer stack and run it
+pop_defer() {
+ local cmd="${DEFER_CMDS[-1]}"
+ local file="${DEFER_FILES[-1]}"
+ local line="${DEFER_LINES[-1]}"
+ unset "DEFER_CMDS[-1]"
+ unset "DEFER_FILES[-1]"
+ unset "DEFER_LINES[-1]"
+
+ local ret=0
+ eval "$cmd" || ret=$?
+
+ if ((ret != 0)); then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ fi
+
+ return $ret
+}
+
+# Run all deferred commands
+pop_defers() {
+ local ret=0
+
+ while ((${#DEFER_CMDS[@]} > 0)); do
+ pop_defer || ret=$?
+ done
+
+ return $ret
+}
+
+## Parallelism
+
+# Get the number of processors
+_nproc() {
+ {
+ nproc \
+ || sysctl -n hw.ncpu \
+ || getconf _NPROCESSORS_ONLN \
+ || echo 1
+ } 2>/dev/null
+}
+
+# Run wait, looping if interrupted
+_wait() {
+ local ret=130
+
+ # "If wait is interrupted by a signal, the return status will be greater than 128"
+ while ((ret > 128)); do
+ ret=0
+ wait "$@" || ret=$?
+ done
+
+ return $ret
+}
diff --git a/tests/xspawn.c b/tests/xspawn.c
new file mode 100644
index 0000000..6864192
--- /dev/null
+++ b/tests/xspawn.c
@@ -0,0 +1,220 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "alloc.h"
+#include "bfstd.h"
+#include "dstring.h"
+#include "xspawn.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+
+/** Duplicate the current environment. */
+static char **envdup(void) {
+ extern char **environ;
+
+ char **envp = NULL;
+ size_t envc = 0;
+
+ for (char **var = environ; ; ++var) {
+ char *copy = NULL;
+ if (*var) {
+ copy = strdup(*var);
+ if (!copy) {
+ goto fail;
+ }
+ }
+
+ char **dest = RESERVE(char *, &envp, &envc);
+ if (!dest) {
+ free(copy);
+ goto fail;
+ }
+ *dest = copy;
+
+ if (!*var) {
+ break;
+ }
+ }
+
+ return envp;
+
+fail:
+ for (size_t i = 0; i < envc; ++i) {
+ free(envp[i]);
+ }
+ free(envp);
+ return NULL;
+}
+
+/** Add an entry to $PATH. */
+static int add_path(const char *entry, char **old_path) {
+ int ret = -1;
+ const char *new_path = NULL;
+
+ *old_path = getenv("PATH");
+ if (*old_path) {
+ *old_path = strdup(*old_path);
+ if (!*old_path) {
+ goto done;
+ }
+
+ new_path = dstrprintf("%s:%s", entry, *old_path);
+ if (!new_path) {
+ goto done;
+ }
+ } else {
+ new_path = entry;
+ }
+
+ ret = setenv("PATH", new_path, true);
+
+done:
+ if (new_path && new_path != entry) {
+ dstrfree((dchar *)new_path);
+ }
+
+ if (ret != 0) {
+ free(*old_path);
+ *old_path = NULL;
+ }
+
+ return ret;
+}
+
+/** Undo add_path(). */
+static int reset_path(char *old_path) {
+ int ret;
+
+ if (old_path) {
+ ret = setenv("PATH", old_path, true);
+ free(old_path);
+ } else {
+ ret = unsetenv("PATH");
+ }
+
+ return ret;
+}
+
+/** Spawn the test binary and check for success. */
+static void check_spawnee(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ pid_t pid = bfs_spawn(exe, ctx, argv, envp);
+ if (!bfs_echeck(pid >= 0, "bfs_spawn('%s')", exe)) {
+ return;
+ }
+
+ int wstatus;
+ bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)
+ && bfs_check(WIFEXITED(wstatus));
+ if (exited) {
+ int wexit = WEXITSTATUS(wstatus);
+ bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit);
+ }
+}
+
+/** Check that we resolve executables in $PATH correctly. */
+static void check_use_path(bool use_posix) {
+ struct bfs_spawn spawn;
+ if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) {
+ return;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ bool init = bfs_echeck(bfs_spawn_addopen(&spawn, 10, "bin", O_RDONLY | O_DIRECTORY, 0) == 0)
+ && bfs_echeck(bfs_spawn_adddup2(&spawn, 10, 11) == 0)
+ && bfs_echeck(bfs_spawn_addclose(&spawn, 10) == 0)
+ && bfs_echeck(bfs_spawn_addfchdir(&spawn, 11) == 0)
+ && bfs_echeck(bfs_spawn_addclose(&spawn, 11) == 0);
+ if (!init) {
+ goto destroy;
+ }
+
+ // Check that $PATH is resolved in the parent's environment
+ char **envp = envdup();
+ if (!bfs_echeck(envp, "envdup()")) {
+ goto destroy;
+ }
+
+ // Check that $PATH is resolved after the file actions
+ char *old_path;
+ if (!bfs_echeck(add_path("tests", &old_path) == 0)) {
+ goto env;
+ }
+
+ char *argv[] = {"xspawnee", old_path, NULL};
+ check_spawnee("xspawnee", &spawn, argv, envp);
+ check_spawnee("tests/xspawnee", &spawn, argv, envp);
+
+ bfs_echeck(reset_path(old_path) == 0);
+env:
+ for (char **var = envp; *var; ++var) {
+ free(*var);
+ }
+ free(envp);
+destroy:
+ bfs_echeck(bfs_spawn_destroy(&spawn) == 0);
+}
+
+/** Check path resolution of non-existent executables. */
+static void check_enoent(bool use_posix) {
+ struct bfs_spawn spawn;
+ if (!bfs_echeck(bfs_spawn_init(&spawn) == 0)) {
+ return;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ char *argv[] = {"eW6f5RM9Qi", NULL};
+ pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL);
+ bfs_echeck(pid < 0 && errno == ENOENT, "bfs_spawn()");
+
+ bfs_echeck(bfs_spawn_destroy(&spawn) == 0);
+}
+
+static void check_resolve(void) {
+ char *exe;
+
+ exe = bfs_spawn_resolve("sh");
+ bfs_echeck(exe, "bfs_spawn_resolve('sh')");
+ free(exe);
+
+ exe = bfs_spawn_resolve("/bin/sh");
+ bfs_echeck(exe && strcmp(exe, "/bin/sh") == 0);
+ free(exe);
+
+ exe = bfs_spawn_resolve("bin/tests/xspawnee");
+ bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ free(exe);
+
+ bfs_echeck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+
+ bfs_echeck(!bfs_spawn_resolve("bin/eW6f5RM9Qi") && errno == ENOENT);
+
+ char *old_path;
+ if (bfs_echeck(add_path("bin/tests", &old_path) == 0)) {
+ exe = bfs_spawn_resolve("xspawnee");
+ bfs_echeck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ free(exe);
+ bfs_echeck(reset_path(old_path) == 0);
+ }
+}
+
+void check_xspawn(void) {
+ check_use_path(true);
+ check_use_path(false);
+
+ check_enoent(true);
+ check_enoent(false);
+
+ check_resolve();
+}
diff --git a/tests/xspawnee.c b/tests/xspawnee.c
new file mode 100644
index 0000000..b0a76ca
--- /dev/null
+++ b/tests/xspawnee.c
@@ -0,0 +1,17 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+#include <string.h>
+
+/** Child binary for bfs_spawn() tests. */
+int main(int argc, char *argv[]) {
+ if (argc >= 2) {
+ const char *path = getenv("PATH");
+ if (!path || strcmp(path, argv[1]) != 0) {
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/xtime.c b/tests/xtime.c
new file mode 100644
index 0000000..c890a1e
--- /dev/null
+++ b/tests/xtime.c
@@ -0,0 +1,187 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "tests.h"
+
+#include "bfs.h"
+#include "diag.h"
+#include "xtime.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <time.h>
+
+static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
+ return tma->tm_year == tmb->tm_year
+ && tma->tm_mon == tmb->tm_mon
+ && tma->tm_mday == tmb->tm_mday
+ && tma->tm_hour == tmb->tm_hour
+ && tma->tm_min == tmb->tm_min
+ && tma->tm_sec == tmb->tm_sec
+ && tma->tm_wday == tmb->tm_wday
+ && tma->tm_yday == tmb->tm_yday
+ && tma->tm_isdst == tmb->tm_isdst;
+}
+
+/** Check one xgetdate() result. */
+static bool check_one_xgetdate(const char *str, int error, time_t expected) {
+ struct timespec ts;
+ int ret = xgetdate(str, &ts);
+
+ if (error) {
+ return bfs_echeck(ret == -1 && errno == error, "xgetdate('%s')", str);
+ } else {
+ return bfs_echeck(ret == 0, "xgetdate('%s')", str)
+ && bfs_check(ts.tv_sec == expected && ts.tv_nsec == 0,
+ "xgetdate('%s'): %jd.%09jd != %jd",
+ str, (intmax_t)ts.tv_sec, (intmax_t)ts.tv_nsec, (intmax_t)expected);
+ }
+}
+
+/** xgetdate() tests. */
+static void check_xgetdate(void) {
+ check_one_xgetdate("", EINVAL, 0);
+ check_one_xgetdate("????", EINVAL, 0);
+ check_one_xgetdate("1991", EINVAL, 0);
+ check_one_xgetdate("1991-??", EINVAL, 0);
+ check_one_xgetdate("1991-12", EINVAL, 0);
+ check_one_xgetdate("1991-12-", EINVAL, 0);
+ check_one_xgetdate("1991-12-??", EINVAL, 0);
+ check_one_xgetdate("1991-12-14", 0, 692668800);
+ check_one_xgetdate("1991-12-14-", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T??", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T10", 0, 692704800);
+ check_one_xgetdate("1991-12-14T10:??", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T10:11", 0, 692705460);
+ check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472);
+ check_one_xgetdate("1991-12-14T10Z", 0, 692704800);
+ check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460);
+ check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472);
+ check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0);
+ check_one_xgetdate("1991-12-14T03-07", 0, 692704800);
+ check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460);
+ check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472);
+ check_one_xgetdate("19911214 031112-0700", 0, 692705472);;
+}
+
+#define TM_FORMAT "%04d-%02d-%02d %02d:%02d:%02d (%d/7, %d/365%s)"
+
+#define TM_PRINTF(tm) \
+ (1900 + (tm).tm_year), (tm).tm_mon, (tm).tm_mday, \
+ (tm).tm_hour, (tm).tm_min, (tm).tm_sec, \
+ ((tm).tm_wday + 1), ((tm).tm_yday + 1), \
+ ((tm).tm_isdst ? ((tm).tm_isdst < 0 ? ", DST?" : ", DST") : "")
+
+/** Check one xmktime() result. */
+static bool check_one_xmktime(time_t expected) {
+ struct tm tm;
+ if (!localtime_r(&expected, &tm)) {
+ bfs_ediag("localtime_r(%jd)", (intmax_t)expected);
+ return false;
+ }
+
+ time_t actual;
+ return bfs_echeck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm))
+ && bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected);
+}
+
+/** xmktime() tests. */
+static void check_xmktime(void) {
+ for (time_t time = -10; time <= 10; ++time) {
+ check_one_xmktime(time);
+ }
+
+ // Attempt to trigger overflow (but don't test for it, since it's not mandatory)
+ struct tm tm = {
+ .tm_year = INT_MAX,
+ .tm_mon = INT_MAX,
+ .tm_mday = INT_MAX,
+ .tm_hour = INT_MAX,
+ .tm_min = INT_MAX,
+ .tm_sec = INT_MAX,
+ .tm_isdst = -1,
+ };
+ time_t time;
+ xmktime(&tm, &time);
+}
+
+/** Check one xtimegm() result. */
+static void check_one_xtimegm(const struct tm *tm) {
+ struct tm tma = *tm, tmb = *tm;
+ time_t ta, tb;
+ ta = mktime(&tma);
+ if (xtimegm(&tmb, &tb) != 0) {
+ tb = -1;
+ }
+
+ bool pass = true;
+ pass &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb);
+ if (ta != -1) {
+ pass &= bfs_check(tm_equal(&tma, &tmb));
+ }
+
+ if (!pass) {
+ bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma));
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+}
+
+#if !BFS_HAS_TIMEGM
+/** Check an overflowing xtimegm() call. */
+static void check_xtimegm_overflow(const struct tm *tm) {
+ struct tm copy = *tm;
+ time_t time = 123;
+
+ bool pass = true;
+ pass &= bfs_check(xtimegm(&copy, &time) == -1 && errno == EOVERFLOW);
+ pass &= bfs_check(tm_equal(&copy, tm));
+ pass &= bfs_check(time == 123);
+
+ if (!pass) {
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+}
+#endif
+
+/** xtimegm() tests. */
+static void check_xtimegm(void) {
+ struct tm tm = {
+ .tm_isdst = -1,
+ };
+
+#if BFS_HAS_TIMEGM
+ // Check that xtimegm(-1) isn't an error
+ for (time_t time = -10; time <= 10; ++time) {
+ if (bfs_check(gmtime_r(&time, &tm), "gmtime_r(%jd)", (intmax_t)time)) {
+ check_one_xtimegm(&tm);
+ }
+ }
+#else
+ // Check equivalence with mktime()
+ for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
+ for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
+ for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
+ for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
+ for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
+ for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
+ check_one_xtimegm(&tm);
+ }
+
+ // Check integer overflow cases
+ check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+#endif // !BFS_HAS_TIMEGM
+}
+
+void check_xtime(void) {
+ check_xgetdate();
+ check_xmktime();
+ check_xtimegm();
+}
diff --git a/tests/xtimegm.c b/tests/xtimegm.c
deleted file mode 100644
index 2fc7fba..0000000
--- a/tests/xtimegm.c
+++ /dev/null
@@ -1,91 +0,0 @@
-#include "../time.h"
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <time.h>
-
-static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
- if (tma->tm_year != tmb->tm_year) {
- return false;
- }
- if (tma->tm_mon != tmb->tm_mon) {
- return false;
- }
- if (tma->tm_mday != tmb->tm_mday) {
- return false;
- }
- if (tma->tm_hour != tmb->tm_hour) {
- return false;
- }
- if (tma->tm_min != tmb->tm_min) {
- return false;
- }
- if (tma->tm_sec != tmb->tm_sec) {
- return false;
- }
- if (tma->tm_wday != tmb->tm_wday) {
- return false;
- }
- if (tma->tm_yday != tmb->tm_yday) {
- return false;
- }
- if (tma->tm_isdst != tmb->tm_isdst) {
- return false;
- }
-
- return true;
-}
-
-static void tm_print(FILE *file, const struct tm *tm) {
- fprintf(file, "Y%d M%d D%d h%d m%d s%d wd%d yd%d%s\n",
- tm->tm_year, tm->tm_mon, tm->tm_mday,
- tm->tm_hour, tm->tm_min, tm->tm_sec,
- tm->tm_wday, tm->tm_yday,
- tm->tm_isdst ? (tm->tm_isdst < 0 ? " (DST?)" : " (DST)") : "");
-}
-
-int main(void) {
- if (setenv("TZ", "UTC0", true) != 0) {
- perror("setenv()");
- return EXIT_FAILURE;
- }
-
- struct tm tm = {
- .tm_isdst = -1,
- };
-
- for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
- for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
- for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
- for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
- for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
- for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
- struct tm tma = tm, tmb = tm;
- time_t ta, tb;
- ta = mktime(&tma);
- if (xtimegm(&tmb, &tb) != 0) {
- tb = -1;
- }
-
- bool fail = false;
- if (ta != tb) {
- printf("Mismatch: %jd != %jd\n", (intmax_t)ta, (intmax_t)tb);
- fail = true;
- }
- if (ta != -1 && !tm_equal(&tma, &tmb)) {
- printf("mktime(): ");
- tm_print(stdout, &tma);
- printf("xtimegm(): ");
- tm_print(stdout, &tmb);
- fail = true;
- }
- if (fail) {
- printf("Input: ");
- tm_print(stdout, &tm);
- return EXIT_FAILURE;
- }
- }
-
- return EXIT_SUCCESS;
-}
diff --git a/tests/xtouch.c b/tests/xtouch.c
new file mode 100644
index 0000000..f33c573
--- /dev/null
+++ b/tests/xtouch.c
@@ -0,0 +1,279 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "bfstd.h"
+#include "sanity.h"
+#include "xtime.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+/** Parsed xtouch arguments. */
+struct args {
+ /** Simple flags. */
+ enum {
+ /** Don't create nonexistent files (-c). */
+ NO_CREATE = 1 << 0,
+ /** Don't follow symlinks (-h). */
+ NO_FOLLOW = 1 << 1,
+ /** Create any missing parent directories (-p). */
+ CREATE_PARENTS = 1 << 2,
+ } flags;
+
+ /** Timestamps (-r|-t|-d). */
+ struct timespec times[2];
+
+ /** File creation mode (-M; default 0666 & ~umask). */
+ mode_t fmode;
+ /** Directory creation mode (-M; default 0777 & ~umask). */
+ mode_t dmode;
+ /** Parent directory creation mode (0777 & ~umask). */
+ mode_t pmode;
+};
+
+/** Open (and maybe create) a single directory. */
+static int open_dir(const struct args *args, int dfd, const char *path) {
+ int ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
+
+ if (ret < 0 && errno == ENOENT && (args->flags & CREATE_PARENTS)) {
+ if (mkdirat(dfd, path, args->pmode) == 0 || errno == EEXIST) {
+ ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
+ }
+ }
+
+ return ret;
+}
+
+/** Open (and maybe create) the parent directory of the path. */
+static int open_parent(const struct args *args, const char **path) {
+ size_t max = xbaseoff(*path);
+ if (max == 0) {
+ return AT_FDCWD;
+ }
+
+ char *dir = strndup(*path, max);
+ if (!dir) {
+ return -1;
+ }
+
+ // Optimistically try the whole path first
+ int dfd = open_dir(args, AT_FDCWD, dir);
+ if (dfd >= 0) {
+ goto done;
+ }
+
+ if (errno == ENOENT) {
+ if (!(args->flags & CREATE_PARENTS)) {
+ goto err;
+ }
+ } else if (!errno_is_like(ENAMETOOLONG)) {
+ goto err;
+ }
+
+ // Open the parents one-at-a-time
+ dfd = AT_FDCWD;
+ char *cur = dir;
+ while (*cur) {
+ char *next = cur;
+ next += strcspn(next, "/");
+ next += strspn(next, "/");
+
+ char c = *next;
+ *next = '\0';
+
+ int parent = dfd;
+ dfd = open_dir(args, parent, cur);
+ if (parent >= 0) {
+ close_quietly(parent);
+ }
+ if (dfd < 0) {
+ goto err;
+ }
+
+ *next = c;
+ cur = next;
+ }
+
+done:
+ *path += max;
+err:
+ free(dir);
+ return dfd;
+}
+
+/** Compute flags for fstatat()/utimensat(). */
+static int at_flags(const struct args *args) {
+ if (args->flags & NO_FOLLOW) {
+ return AT_SYMLINK_NOFOLLOW;
+ } else {
+ return 0;
+ }
+}
+
+/** Touch one path. */
+static int xtouch(const struct args *args, const char *path) {
+ int dfd = open_parent(args, &path);
+ if (dfd < 0 && dfd != (int)AT_FDCWD) {
+ return -1;
+ }
+
+ int ret = utimensat(dfd, path, args->times, at_flags(args));
+ if (ret == 0 || errno != ENOENT) {
+ goto done;
+ }
+
+ if (args->flags & NO_CREATE) {
+ ret = 0;
+ goto done;
+ }
+
+ size_t len = strlen(path);
+ if (len > 0 && path[len - 1] == '/') {
+ if (mkdirat(dfd, path, args->dmode) == 0) {
+ ret = utimensat(dfd, path, args->times, at_flags(args));
+ }
+ } else {
+ int fd = openat(dfd, path, O_WRONLY | O_CREAT, args->fmode);
+ if (fd >= 0) {
+ if (futimens(fd, args->times) == 0) {
+ ret = xclose(fd);
+ } else {
+ close_quietly(fd);
+ }
+ }
+ }
+
+done:
+ if (dfd >= 0) {
+ close_quietly(dfd);
+ }
+ return ret;
+}
+
+int main(int argc, char *argv[]) {
+ tzset();
+
+ mode_t mask = umask(0);
+
+ struct args args = {
+ .flags = 0,
+ .times = {
+ { .tv_nsec = UTIME_OMIT },
+ { .tv_nsec = UTIME_OMIT },
+ },
+ .fmode = 0666 & ~mask,
+ .dmode = 0777 & ~mask,
+ .pmode = 0777 & ~mask,
+ };
+
+ bool atime = false, mtime = false;
+ const char *darg = NULL;
+ const char *marg = NULL;
+ const char *rarg = NULL;
+
+ const char *cmd = argc > 0 ? argv[0] : "xtouch";
+ int c;
+ while (c = getopt(argc, argv, ":M:acd:hmpr:t:"), c != -1) {
+ switch (c) {
+ case 'M':
+ marg = optarg;
+ break;
+ case 'a':
+ atime = true;
+ break;
+ case 'c':
+ args.flags |= NO_CREATE;
+ break;
+ case 'd':
+ case 't':
+ darg = optarg;
+ break;
+ case 'h':
+ args.flags |= NO_FOLLOW;
+ break;
+ case 'm':
+ mtime = true;
+ break;
+ case 'p':
+ args.flags |= CREATE_PARENTS;
+ break;
+ case 'r':
+ rarg = optarg;
+ break;
+ case ':':
+ fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ case '?':
+ fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (marg) {
+ unsigned int mode;
+ if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) {
+ args.fmode = args.dmode = mode;
+ } else {
+ fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg);
+ return EXIT_FAILURE;
+ }
+ }
+
+ struct timespec times[2];
+
+ if (rarg) {
+ struct stat buf;
+ if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) {
+ fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno));
+ return EXIT_FAILURE;
+ }
+ times[0] = ST_ATIM(buf);
+ times[1] = ST_MTIM(buf);
+ } else if (darg) {
+ if (xgetdate(darg, &times[0]) != 0) {
+ fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno));
+ return EXIT_FAILURE;
+ }
+ times[1] = times[0];
+ } else {
+ // Don't use UTIME_NOW, so that multiple paths all get the same timestamp
+ if (clock_gettime(CLOCK_REALTIME, &times[0]) != 0) {
+ perror("clock_gettime()");
+ return EXIT_FAILURE;
+ }
+ times[1] = times[0];
+ }
+
+ if (!atime && !mtime) {
+ atime = true;
+ mtime = true;
+ }
+ if (atime) {
+ args.times[0] = times[0];
+ }
+ if (mtime) {
+ args.times[1] = times[1];
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "%s: No files to touch\n", cmd);
+ return EXIT_FAILURE;
+ }
+
+ int ret = EXIT_SUCCESS;
+ for (; optind < argc; ++optind) {
+ const char *path = argv[optind];
+ if (xtouch(&args, path) != 0) {
+ fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno));
+ ret = EXIT_FAILURE;
+ }
+ }
+ return ret;
+}
diff --git a/time.c b/time.c
deleted file mode 100644
index c7331b5..0000000
--- a/time.c
+++ /dev/null
@@ -1,323 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "time.h"
-#include <errno.h>
-#include <limits.h>
-#include <stdbool.h>
-#include <stdlib.h>
-#include <time.h>
-
-/** Whether tzset() has been called. */
-static bool tz_is_set = false;
-
-int xlocaltime(const time_t *timep, struct tm *result) {
- // Should be called before localtime_r() according to POSIX.1-2004
- if (!tz_is_set) {
- tzset();
- tz_is_set = true;
- }
-
- if (localtime_r(timep, result)) {
- return 0;
- } else {
- return -1;
- }
-}
-
-int xgmtime(const time_t *timep, struct tm *result) {
- // Should be called before gmtime_r() according to POSIX.1-2004
- if (!tz_is_set) {
- tzset();
- tz_is_set = true;
- }
-
- if (gmtime_r(timep, result)) {
- return 0;
- } else {
- return -1;
- }
-}
-
-int xmktime(struct tm *tm, time_t *timep) {
- *timep = mktime(tm);
-
- if (*timep == -1) {
- int error = errno;
-
- struct tm tmp;
- if (xlocaltime(timep, &tmp) != 0) {
- return -1;
- }
-
- if (tm->tm_year != tmp.tm_year || tm->tm_yday != tmp.tm_yday
- || tm->tm_hour != tmp.tm_hour || tm->tm_min != tmp.tm_min || tm->tm_sec != tmp.tm_sec) {
- errno = error;
- return -1;
- }
- }
-
- return 0;
-}
-
-static int safe_add(int *value, int delta) {
- if (*value >= 0) {
- if (delta > INT_MAX - *value) {
- return -1;
- }
- } else {
- if (delta < INT_MIN - *value) {
- return -1;
- }
- }
-
- *value += delta;
- return 0;
-}
-
-static int floor_div(int n, int d) {
- int a = n < 0;
- return (n + a)/d - a;
-}
-
-static int wrap(int *value, int max, int *next) {
- int carry = floor_div(*value, max);
- *value -= carry * max;
- return safe_add(next, carry);
-}
-
-static int month_length(int year, int month) {
- static const int month_lengths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
- int ret = month_lengths[month];
- if (month == 1 && year%4 == 0 && (year%100 != 0 || (year + 300)%400 == 0)) {
- ++ret;
- }
- return ret;
-}
-
-int xtimegm(struct tm *tm, time_t *timep) {
- tm->tm_isdst = 0;
-
- if (wrap(&tm->tm_sec, 60, &tm->tm_min) != 0) {
- goto overflow;
- }
- if (wrap(&tm->tm_min, 60, &tm->tm_hour) != 0) {
- goto overflow;
- }
- if (wrap(&tm->tm_hour, 24, &tm->tm_mday) != 0) {
- goto overflow;
- }
-
- // In order to wrap the days of the month, we first need to know what
- // month it is
- if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) {
- goto overflow;
- }
-
- if (tm->tm_mday < 1) {
- do {
- --tm->tm_mon;
- if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) {
- goto overflow;
- }
-
- tm->tm_mday += month_length(tm->tm_year, tm->tm_mon);
- } while (tm->tm_mday < 1);
- } else {
- while (true) {
- int days = month_length(tm->tm_year, tm->tm_mon);
- if (tm->tm_mday <= days) {
- break;
- }
-
- tm->tm_mday -= days;
- ++tm->tm_mon;
- if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) {
- goto overflow;
- }
- }
- }
-
- tm->tm_yday = 0;
- for (int i = 0; i < tm->tm_mon; ++i) {
- tm->tm_yday += month_length(tm->tm_year, i);
- }
- tm->tm_yday += tm->tm_mday - 1;
-
- int leap_days;
- // Compute floor((year - 69)/4) - floor((year - 1)/100) + floor((year + 299)/400) without overflows
- if (tm->tm_year >= 0) {
- leap_days = floor_div(tm->tm_year - 69, 4) - floor_div(tm->tm_year - 1, 100) + floor_div(tm->tm_year - 101, 400) + 1;
- } else {
- leap_days = floor_div(tm->tm_year + 3, 4) - floor_div(tm->tm_year + 99, 100) + floor_div(tm->tm_year + 299, 400) - 17;
- }
-
- long long epoch_days = 365LL*(tm->tm_year - 70) + leap_days + tm->tm_yday;
- tm->tm_wday = (epoch_days + 4)%7;
- if (tm->tm_wday < 0) {
- tm->tm_wday += 7;
- }
-
- long long epoch_time = tm->tm_sec + 60*(tm->tm_min + 60*(tm->tm_hour + 24*epoch_days));
- *timep = (time_t)epoch_time;
- if ((long long)*timep != epoch_time) {
- goto overflow;
- }
- return 0;
-
-overflow:
- errno = EOVERFLOW;
- return -1;
-}
-
-/** Parse some digits from a timestamp. */
-static int parse_timestamp_part(const char **str, size_t n, int *result) {
- char buf[n + 1];
- for (size_t i = 0; i < n; ++i, ++*str) {
- char c = **str;
- if (c < '0' || c > '9') {
- return -1;
- }
- buf[i] = c;
- }
- buf[n] = '\0';
-
- *result = atoi(buf);
- return 0;
-}
-
-int parse_timestamp(const char *str, struct timespec *result) {
- struct tm tm = {
- .tm_isdst = -1,
- };
-
- int tz_hour = 0;
- int tz_min = 0;
- bool tz_negative = false;
- bool local = true;
-
- // YYYY
- if (parse_timestamp_part(&str, 4, &tm.tm_year) != 0) {
- goto invalid;
- }
- tm.tm_year -= 1900;
-
- // MM
- if (*str == '-') {
- ++str;
- }
- if (parse_timestamp_part(&str, 2, &tm.tm_mon) != 0) {
- goto invalid;
- }
- tm.tm_mon -= 1;
-
- // DD
- if (*str == '-') {
- ++str;
- }
- if (parse_timestamp_part(&str, 2, &tm.tm_mday) != 0) {
- goto invalid;
- }
-
- if (!*str) {
- goto end;
- } else if (*str == 'T') {
- ++str;
- }
-
- // hh
- if (parse_timestamp_part(&str, 2, &tm.tm_hour) != 0) {
- goto invalid;
- }
-
- // mm
- if (!*str) {
- goto end;
- } else if (*str == ':') {
- ++str;
- }
- if (parse_timestamp_part(&str, 2, &tm.tm_min) != 0) {
- goto invalid;
- }
-
- // ss
- if (!*str) {
- goto end;
- } else if (*str == ':') {
- ++str;
- }
- if (parse_timestamp_part(&str, 2, &tm.tm_sec) != 0) {
- goto invalid;
- }
-
- if (!*str) {
- goto end;
- } else if (*str == 'Z') {
- local = false;
- ++str;
- } else if (*str == '+' || *str == '-') {
- local = false;
- tz_negative = *str == '-';
- ++str;
-
- // hh
- if (parse_timestamp_part(&str, 2, &tz_hour) != 0) {
- goto invalid;
- }
-
- // mm
- if (!*str) {
- goto end;
- } else if (*str == ':') {
- ++str;
- }
- if (parse_timestamp_part(&str, 2, &tz_min) != 0) {
- goto invalid;
- }
- } else {
- goto invalid;
- }
-
- if (*str) {
- goto invalid;
- }
-
-end:
- if (local) {
- if (xmktime(&tm, &result->tv_sec) != 0) {
- goto error;
- }
- } else {
- if (xtimegm(&tm, &result->tv_sec) != 0) {
- goto error;
- }
-
- int offset = 60*tz_hour + tz_min;
- if (tz_negative) {
- result->tv_sec -= offset;
- } else {
- result->tv_sec += offset;
- }
- }
-
- result->tv_nsec = 0;
- return 0;
-
-invalid:
- errno = EINVAL;
-error:
- return -1;
-}
diff --git a/time.h b/time.h
deleted file mode 100644
index 0f9adb4..0000000
--- a/time.h
+++ /dev/null
@@ -1,86 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Date/time handling.
- */
-
-#ifndef BFS_TIME_H
-#define BFS_TIME_H
-
-#include <time.h>
-
-/**
- * localtime_r() wrapper that calls tzset() first.
- *
- * @param[in] timep
- * The time_t to convert.
- * @param[out] result
- * Buffer to hold the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xlocaltime(const time_t *timep, struct tm *result);
-
-/**
- * gmtime_r() wrapper that calls tzset() first.
- *
- * @param[in] timep
- * The time_t to convert.
- * @param[out] result
- * Buffer to hold the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xgmtime(const time_t *timep, struct tm *result);
-
-/**
- * mktime() wrapper that reports errors more reliably.
- *
- * @param[in,out] tm
- * The struct tm to convert.
- * @param[out] timep
- * A pointer to the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xmktime(struct tm *tm, time_t *timep);
-
-/**
- * A portable timegm(), the inverse of gmtime().
- *
- * @param[in,out] tm
- * The struct tm to convert.
- * @param[out] timep
- * A pointer to the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xtimegm(struct tm *tm, time_t *timep);
-
-/**
- * Parse an ISO 8601-style timestamp.
- *
- * @param[in] str
- * The string to parse.
- * @param[out] result
- * A pointer to the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int parse_timestamp(const char *str, struct timespec *result);
-
-#endif // BFS_TIME_H
diff --git a/trie.c b/trie.c
deleted file mode 100644
index 6489b0c..0000000
--- a/trie.c
+++ /dev/null
@@ -1,693 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * This is an implementation of a "qp trie," as documented at
- * https://dotat.at/prog/qp/README.html
- *
- * An uncompressed trie over the dataset {AAAA, AADD, ABCD, DDAA, DDDD} would
- * look like
- *
- * A A A A
- * *--->*--->*--->*--->$
- * | | | D D
- * | | +--->*--->$
- * | | B C D
- * | +--->*--->*--->$
- * | D D A A
- * +--->*--->*--->*--->$
- * | D D
- * +--->*--->$
- *
- * A compressed (PATRICIA) trie collapses internal nodes that have only a single
- * child, like this:
- *
- * A A AA
- * *--->*--->*---->$
- * | | | DD
- * | | +---->$
- * | | BCD
- * | +----->$
- * | DD AA
- * +---->*---->$
- * | DD
- * +---->$
- *
- * The nodes can be compressed further by dropping the actual compressed
- * sequences from the nodes, storing it only in the leaves. This is the
- * technique applied in QP tries, and the crit-bit trees that inspired them
- * (https://cr.yp.to/critbit.html). Only the index to test, and the values to
- * branch on, need to be stored in each node.
- *
- * A A A
- * 0--->1--->2--->AAAA
- * | | | D
- * | | +--->AADD
- * | | B
- * | +--->ABCD
- * | D A
- * +--->2--->DDAA
- * | D
- * +--->DDDD
- *
- * Nodes are represented very compactly. Rather than a dense array of children,
- * a sparse array of only the non-NULL children directly follows the node in
- * memory. A bitmap is used to track which children exist; the index of a child
- * i is found by counting the number of bits below bit i that are set. A tag
- * bit is used to tell pointers to internal nodes apart from pointers to leaves.
- *
- * This implementation tests a whole nibble (half byte/hex digit) at every
- * branch, so the bitmap takes up 16 bits. The remainder of a machine word is
- * used to hold the offset, which severely constrains its range on 32-bit
- * platforms. As a workaround, we store relative instead of absolute offsets,
- * and insert intermediate singleton "jump" nodes when necessary.
- */
-
-#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <limits.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-
-#if CHAR_BIT != 8
-# error "This trie implementation assumes 8-bit bytes."
-#endif
-
-/** Number of bits for the sparse array bitmap, aka the range of a nibble. */
-#define BITMAP_BITS 16
-/** The number of remaining bits in a word, to hold the offset. */
-#define OFFSET_BITS (sizeof(size_t)*CHAR_BIT - BITMAP_BITS)
-/** The highest representable offset (only 64k on a 32-bit architecture). */
-#define OFFSET_MAX (((size_t)1 << OFFSET_BITS) - 1)
-
-/**
- * An internal node of the trie.
- */
-struct trie_node {
- /**
- * A bitmap that hold which indices exist in the sparse children array.
- * Bit i will be set if a child exists at logical index i, and its index
- * into the array will be popcount(bitmap & ((1 << i) - 1)).
- */
- size_t bitmap : BITMAP_BITS;
-
- /**
- * The offset into the key in nibbles. This is relative to the parent
- * node, to support offsets larger than OFFSET_MAX.
- */
- size_t offset : OFFSET_BITS;
-
- /**
- * Flexible array of children. Each pointer uses the lowest bit as a
- * tag to distinguish internal nodes from leaves. This is safe as long
- * as all dynamic allocations are aligned to more than a single byte.
- */
- uintptr_t children[];
-};
-
-/** Check if an encoded pointer is to a leaf. */
-static bool trie_is_leaf(uintptr_t ptr) {
- return ptr & 1;
-}
-
-/** Decode a pointer to a leaf. */
-static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) {
- assert(trie_is_leaf(ptr));
- return (struct trie_leaf *)(ptr ^ 1);
-}
-
-/** Encode a pointer to a leaf. */
-static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) {
- uintptr_t ptr = (uintptr_t)leaf ^ 1;
- assert(trie_is_leaf(ptr));
- return ptr;
-}
-
-/** Decode a pointer to an internal node. */
-static struct trie_node *trie_decode_node(uintptr_t ptr) {
- assert(!trie_is_leaf(ptr));
- return (struct trie_node *)ptr;
-}
-
-/** Encode a pointer to an internal node. */
-static uintptr_t trie_encode_node(const struct trie_node *node) {
- uintptr_t ptr = (uintptr_t)node;
- assert(!trie_is_leaf(ptr));
- return ptr;
-}
-
-void trie_init(struct trie *trie) {
- trie->root = 0;
-}
-
-/** Compute the popcount (Hamming weight) of a bitmap. */
-static unsigned int trie_popcount(unsigned int n) {
-#if __POPCNT__
- // Use the x86 instruction if we have it. Otherwise, GCC generates a
- // library call, so use the below implementation instead.
- return __builtin_popcount(n);
-#else
- // See https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation
- n -= (n >> 1) & 0x5555;
- n = (n & 0x3333) + ((n >> 2) & 0x3333);
- n = (n + (n >> 4)) & 0x0F0F;
- n = (n + (n >> 8)) & 0xFF;
- return n;
-#endif
-}
-
-/** Extract the nibble at a certain offset from a byte sequence. */
-static unsigned char trie_key_nibble(const void *key, size_t offset) {
- const unsigned char *bytes = key;
- size_t byte = offset >> 1;
-
- // A branchless version of
- // if (offset & 1) {
- // return bytes[byte] >> 4;
- // } else {
- // return bytes[byte] & 0xF;
- // }
- unsigned int shift = (offset & 1) << 2;
- return (bytes[byte] >> shift) & 0xF;
-}
-
-/**
- * Finds a leaf in the trie that matches the key at every branch. If the key
- * exists in the trie, the representative will match the searched key. But
- * since only branch points are tested, it can be different from the key. In
- * that case, the first mismatch between the key and the representative will be
- * the depth at which to make a new branch to insert the key.
- */
-static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) {
- uintptr_t ptr = trie->root;
- if (!ptr) {
- return NULL;
- }
-
- size_t offset = 0;
- while (!trie_is_leaf(ptr)) {
- struct trie_node *node = trie_decode_node(ptr);
- offset += node->offset;
-
- unsigned int index = 0;
- if ((offset >> 1) < length) {
- unsigned char nibble = trie_key_nibble(key, offset);
- unsigned int bit = 1U << nibble;
- if (node->bitmap & bit) {
- index = trie_popcount(node->bitmap & (bit - 1));
- }
- }
- ptr = node->children[index];
- }
-
- return trie_decode_leaf(ptr);
-}
-
-struct trie_leaf *trie_first_leaf(const struct trie *trie) {
- return trie_representative(trie, NULL, 0);
-}
-
-struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) {
- return trie_find_mem(trie, key, strlen(key) + 1);
-}
-
-struct trie_leaf *trie_find_mem(const struct trie *trie, const void *key, size_t length) {
- struct trie_leaf *rep = trie_representative(trie, key, length);
- if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) {
- return rep;
- } else {
- return NULL;
- }
-}
-
-struct trie_leaf *trie_find_postfix(const struct trie *trie, const char *key) {
- size_t length = strlen(key);
- struct trie_leaf *rep = trie_representative(trie, key, length + 1);
- if (rep && rep->length >= length && memcmp(rep->key, key, length) == 0) {
- return rep;
- } else {
- return NULL;
- }
-}
-
-/**
- * Find a leaf that may end at the current node.
- */
-static struct trie_leaf *trie_terminal_leaf(const struct trie_node *node) {
- // Finding a terminating NUL byte may take two nibbles
- for (int i = 0; i < 2; ++i) {
- if (!(node->bitmap & 1)) {
- break;
- }
-
- uintptr_t ptr = node->children[0];
- if (trie_is_leaf(ptr)) {
- return trie_decode_leaf(ptr);
- } else {
- node = trie_decode_node(ptr);
- }
- }
-
- return NULL;
-}
-
-/** Check if a leaf is a prefix of a search key. */
-static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *key, size_t length) {
- if (leaf && leaf->length <= length) {
- return memcmp(key + skip, leaf->key + skip, leaf->length - skip - 1) == 0;
- } else {
- return false;
- }
-}
-
-struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
- uintptr_t ptr = trie->root;
- if (!ptr) {
- return NULL;
- }
-
- struct trie_leaf *best = NULL;
- size_t skip = 0;
- size_t length = strlen(key) + 1;
-
- size_t offset = 0;
- while (!trie_is_leaf(ptr)) {
- struct trie_node *node = trie_decode_node(ptr);
- offset += node->offset;
- if ((offset >> 1) >= length) {
- return best;
- }
-
- struct trie_leaf *leaf = trie_terminal_leaf(node);
- if (trie_check_prefix(leaf, skip, key, length)) {
- best = leaf;
- skip = offset >> 1;
- }
-
- unsigned char nibble = trie_key_nibble(key, offset);
- unsigned int bit = 1U << nibble;
- if (node->bitmap & bit) {
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
- ptr = node->children[index];
- } else {
- return best;
- }
- }
-
- struct trie_leaf *leaf = trie_decode_leaf(ptr);
- if (trie_check_prefix(leaf, skip, key, length)) {
- best = leaf;
- }
-
- return best;
-}
-
-/** Create a new leaf, holding a copy of the given key. */
-static struct trie_leaf *new_trie_leaf(const void *key, size_t length) {
- struct trie_leaf *leaf = malloc(BFS_FLEX_SIZEOF(struct trie_leaf, key, length));
- if (leaf) {
- leaf->value = NULL;
- leaf->length = length;
- memcpy(leaf->key, key, length);
- }
- return leaf;
-}
-
-/** Compute the size of a trie node with a certain number of children. */
-static size_t trie_node_size(unsigned int size) {
- // Empty nodes aren't supported
- assert(size > 0);
- // Node size must be a power of two
- assert((size & (size - 1)) == 0);
-
- return BFS_FLEX_SIZEOF(struct trie_node, children, size);
-}
-
-/** Find the offset of the first nibble that differs between two keys. */
-static size_t trie_key_mismatch(const void *key1, const void *key2, size_t length) {
- const unsigned char *bytes1 = key1;
- const unsigned char *bytes2 = key2;
- size_t i = 0;
- size_t offset = 0;
- const size_t chunk = sizeof(size_t);
-
- for (; i + chunk <= length; i += chunk) {
- if (memcmp(bytes1 + i, bytes2 + i, chunk) != 0) {
- break;
- }
- }
-
- for (; i < length; ++i) {
- unsigned char b1 = bytes1[i], b2 = bytes2[i];
- if (b1 != b2) {
- offset = (b1 & 0xF) == (b2 & 0xF);
- break;
- }
- }
-
- offset |= i << 1;
- return offset;
-}
-
-/**
- * Insert a key into a node. The node must not have a child in that position
- * already. Effectively takes a subtrie like this:
- *
- * ptr
- * |
- * v X
- * *--->...
- * | Z
- * +--->...
- *
- * and transforms it to:
- *
- * ptr
- * |
- * v X
- * *--->...
- * | Y
- * +--->key
- * | Z
- * +--->...
- */
-static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_t length, size_t offset) {
- struct trie_node *node = trie_decode_node(*ptr);
- unsigned int size = trie_popcount(node->bitmap);
-
- // Double the capacity every power of two
- if ((size & (size - 1)) == 0) {
- node = realloc(node, trie_node_size(2*size));
- if (!node) {
- return NULL;
- }
- *ptr = trie_encode_node(node);
- }
-
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (!leaf) {
- return NULL;
- }
-
- unsigned char nibble = trie_key_nibble(key, offset);
- unsigned int bit = 1U << nibble;
-
- // The child must not already be present
- assert(!(node->bitmap & bit));
- node->bitmap |= bit;
-
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
- uintptr_t *child = node->children + index;
- if (index < size) {
- memmove(child + 1, child, (size - index)*sizeof(*child));
- }
- *child = trie_encode_leaf(leaf);
- return leaf;
-}
-
-/**
- * When the current offset exceeds OFFSET_MAX, insert "jump" nodes that bridge
- * the gap. This function takes a subtrie like this:
- *
- * ptr
- * |
- * v
- * *--->rep
- *
- * and changes it to:
- *
- * ptr ret
- * | |
- * v v
- * *--->*--->rep
- *
- * so that a new key can be inserted like:
- *
- * ptr ret
- * | |
- * v v X
- * *--->*--->rep
- * | Y
- * +--->key
- */
-static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) {
- // We only ever need to jump to leaf nodes, since internal nodes are
- // guaranteed to be within OFFSET_MAX anyway
- assert(trie_is_leaf(*ptr));
-
- struct trie_node *node = malloc(trie_node_size(1));
- if (!node) {
- return NULL;
- }
-
- *offset += OFFSET_MAX;
- node->offset = OFFSET_MAX;
-
- unsigned char nibble = trie_key_nibble(key, *offset);
- node->bitmap = 1 << nibble;
-
- node->children[0] = *ptr;
- *ptr = trie_encode_node(node);
- return node->children;
-}
-
-/**
- * Split a node in the trie. Changes a subtrie like this:
- *
- * ptr
- * |
- * v
- * *...>--->rep
- *
- * into this:
- *
- * ptr
- * |
- * v X
- * *--->*...>--->rep
- * | Y
- * +--->key
- */
-static struct trie_leaf *trie_split(uintptr_t *ptr, const void *key, size_t length, struct trie_leaf *rep, size_t offset, size_t mismatch) {
- unsigned char key_nibble = trie_key_nibble(key, mismatch);
- unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch);
- assert(key_nibble != rep_nibble);
-
- struct trie_node *node = malloc(trie_node_size(2));
- if (!node) {
- return NULL;
- }
-
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (!leaf) {
- free(node);
- return NULL;
- }
-
- node->bitmap = (1 << key_nibble) | (1 << rep_nibble);
-
- size_t delta = mismatch - offset;
- if (!trie_is_leaf(*ptr)) {
- struct trie_node *child = trie_decode_node(*ptr);
- child->offset -= delta;
- }
- node->offset = delta;
-
- unsigned int key_index = key_nibble > rep_nibble;
- node->children[key_index] = trie_encode_leaf(leaf);
- node->children[key_index ^ 1] = *ptr;
- *ptr = trie_encode_node(node);
- return leaf;
-}
-
-struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) {
- return trie_insert_mem(trie, key, strlen(key) + 1);
-}
-
-struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) {
- struct trie_leaf *rep = trie_representative(trie, key, length);
- if (!rep) {
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (leaf) {
- trie->root = trie_encode_leaf(leaf);
- }
- return leaf;
- }
-
- size_t limit = length < rep->length ? length : rep->length;
- size_t mismatch = trie_key_mismatch(key, rep->key, limit);
- if ((mismatch >> 1) >= length) {
- return rep;
- }
-
- size_t offset = 0;
- uintptr_t *ptr = &trie->root;
- while (!trie_is_leaf(*ptr)) {
- struct trie_node *node = trie_decode_node(*ptr);
- if (offset + node->offset > mismatch) {
- break;
- }
- offset += node->offset;
-
- unsigned char nibble = trie_key_nibble(key, offset);
- unsigned int bit = 1U << nibble;
- if (node->bitmap & bit) {
- assert(offset < mismatch);
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
- ptr = node->children + index;
- } else {
- assert(offset == mismatch);
- return trie_node_insert(ptr, key, length, offset);
- }
- }
-
- while (mismatch - offset > OFFSET_MAX) {
- ptr = trie_jump(ptr, key, &offset);
- if (!ptr) {
- return NULL;
- }
- }
-
- return trie_split(ptr, key, length, rep, offset, mismatch);
-}
-
-/** Free a chain of singleton nodes. */
-static void trie_free_singletons(uintptr_t ptr) {
- while (!trie_is_leaf(ptr)) {
- struct trie_node *node = trie_decode_node(ptr);
-
- // Make sure the bitmap is a power of two, i.e. it has just one child
- assert((node->bitmap & (node->bitmap - 1)) == 0);
-
- ptr = node->children[0];
- free(node);
- }
-
- free(trie_decode_leaf(ptr));
-}
-
-/**
- * Try to collapse a two-child node like:
- *
- * parent child
- * | |
- * v v
- * *----->*----->*----->leaf
- * |
- * +----->other
- *
- * into
- *
- * parent
- * |
- * v
- * other
- */
-static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) {
- uintptr_t other = parent_node->children[child_index ^ 1];
- if (!trie_is_leaf(other)) {
- struct trie_node *other_node = trie_decode_node(other);
- if (other_node->offset + parent_node->offset <= OFFSET_MAX) {
- other_node->offset += parent_node->offset;
- } else {
- return -1;
- }
- }
-
- *parent = other;
- free(parent_node);
- return 0;
-}
-
-void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
- uintptr_t *child = &trie->root;
- uintptr_t *parent = NULL;
- unsigned int child_bit = 0, child_index = 0;
- size_t offset = 0;
- while (!trie_is_leaf(*child)) {
- struct trie_node *node = trie_decode_node(*child);
- offset += node->offset;
- assert((offset >> 1) < leaf->length);
-
- unsigned char nibble = trie_key_nibble(leaf->key, offset);
- unsigned int bit = 1U << nibble;
- unsigned int bitmap = node->bitmap;
- assert(bitmap & bit);
- unsigned int index = trie_popcount(bitmap & (bit - 1));
-
- // Advance the parent pointer, unless this node had only one child
- if (bitmap & (bitmap - 1)) {
- parent = child;
- child_bit = bit;
- child_index = index;
- }
-
- child = node->children + index;
- }
-
- assert(trie_decode_leaf(*child) == leaf);
-
- if (!parent) {
- trie_free_singletons(trie->root);
- trie->root = 0;
- return;
- }
-
- struct trie_node *node = trie_decode_node(*parent);
- child = node->children + child_index;
- trie_free_singletons(*child);
-
- node->bitmap ^= child_bit;
- unsigned int parent_size = trie_popcount(node->bitmap);
- assert(parent_size > 0);
- if (parent_size == 1 && trie_collapse_node(parent, node, child_index) == 0) {
- return;
- }
-
- if (child_index < parent_size) {
- memmove(child, child + 1, (parent_size - child_index)*sizeof(*child));
- }
-
- if ((parent_size & (parent_size - 1)) == 0) {
- node = realloc(node, trie_node_size(parent_size));
- if (node) {
- *parent = trie_encode_node(node);
- }
- }
-}
-
-/** Free an encoded pointer to a node. */
-static void free_trie_ptr(uintptr_t ptr) {
- if (trie_is_leaf(ptr)) {
- free(trie_decode_leaf(ptr));
- } else {
- struct trie_node *node = trie_decode_node(ptr);
- size_t size = trie_popcount(node->bitmap);
- for (size_t i = 0; i < size; ++i) {
- free_trie_ptr(node->children[i]);
- }
- free(node);
- }
-}
-
-void trie_destroy(struct trie *trie) {
- if (trie->root) {
- free_trie_ptr(trie->root);
- }
-}
diff --git a/typo.h b/typo.h
deleted file mode 100644
index 0347aae..0000000
--- a/typo.h
+++ /dev/null
@@ -1,31 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#ifndef BFS_TYPO_H
-#define BFS_TYPO_H
-
-/**
- * Find the "typo" distance between two strings.
- *
- * @param actual
- * The actual string typed by the user.
- * @param expected
- * The expected valid string.
- * @return The distance between the two strings.
- */
-int typo_distance(const char *actual, const char *expected);
-
-#endif // BFS_TYPO_H
diff --git a/util.c b/util.c
deleted file mode 100644
index d913b7d..0000000
--- a/util.c
+++ /dev/null
@@ -1,428 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "util.h"
-#include "dstring.h"
-#include <errno.h>
-#include <fcntl.h>
-#include <langinfo.h>
-#include <nl_types.h>
-#include <regex.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
-#endif
-
-#if BFS_HAS_SYS_SYSMACROS
-# include <sys/sysmacros.h>
-#elif BFS_HAS_SYS_MKDEV
-# include <sys/mkdev.h>
-#endif
-
-#if BFS_HAS_UTIL
-# include <util.h>
-#endif
-
-char *xreadlinkat(int fd, const char *path, size_t size) {
- ssize_t len;
- char *name = NULL;
-
- if (size == 0) {
- size = 64;
- } else {
- ++size; // NUL terminator
- }
-
- while (true) {
- char *new_name = realloc(name, size);
- if (!new_name) {
- goto error;
- }
- name = new_name;
-
- len = readlinkat(fd, path, name, size);
- if (len < 0) {
- goto error;
- } else if ((size_t)len >= size) {
- size *= 2;
- } else {
- break;
- }
- }
-
- name[len] = '\0';
- return name;
-
-error:
- free(name);
- return NULL;
-}
-
-int dup_cloexec(int fd) {
-#ifdef F_DUPFD_CLOEXEC
- return fcntl(fd, F_DUPFD_CLOEXEC, 0);
-#else
- int ret = dup(fd);
- if (ret < 0) {
- return -1;
- }
-
- if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) {
- close(ret);
- return -1;
- }
-
- return ret;
-#endif
-}
-
-int pipe_cloexec(int pipefd[2]) {
-#if __linux__ || (BSD && !__APPLE__)
- return pipe2(pipefd, O_CLOEXEC);
-#else
- if (pipe(pipefd) != 0) {
- return -1;
- }
-
- if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) {
- int error = errno;
- close(pipefd[1]);
- close(pipefd[0]);
- errno = error;
- return -1;
- }
-
- return 0;
-#endif
-}
-
-char *xregerror(int err, const regex_t *regex) {
- size_t len = regerror(err, regex, NULL, 0);
- char *str = malloc(len);
- if (str) {
- regerror(err, regex, str, len);
- }
- return str;
-}
-
-/** Get the single character describing the given file type. */
-static char type_char(mode_t mode) {
- switch (mode & S_IFMT) {
- case S_IFREG:
- return '-';
- case S_IFBLK:
- return 'b';
- case S_IFCHR:
- return 'c';
- case S_IFDIR:
- return 'd';
- case S_IFLNK:
- return 'l';
- case S_IFIFO:
- return 'p';
- case S_IFSOCK:
- return 's';
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return 'D';
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return 'P';
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return 'w';
-#endif
- }
-
- return '?';
-}
-
-void xstrmode(mode_t mode, char str[11]) {
- strcpy(str, "----------");
-
- str[0] = type_char(mode);
-
- if (mode & 00400) {
- str[1] = 'r';
- }
- if (mode & 00200) {
- str[2] = 'w';
- }
- if ((mode & 04100) == 04000) {
- str[3] = 'S';
- } else if (mode & 04000) {
- str[3] = 's';
- } else if (mode & 00100) {
- str[3] = 'x';
- }
-
- if (mode & 00040) {
- str[4] = 'r';
- }
- if (mode & 00020) {
- str[5] = 'w';
- }
- if ((mode & 02010) == 02000) {
- str[6] = 'S';
- } else if (mode & 02000) {
- str[6] = 's';
- } else if (mode & 00010) {
- str[6] = 'x';
- }
-
- if (mode & 00004) {
- str[7] = 'r';
- }
- if (mode & 00002) {
- str[8] = 'w';
- }
- if ((mode & 01001) == 01000) {
- str[9] = 'T';
- } else if (mode & 01000) {
- str[9] = 't';
- } else if (mode & 00001) {
- str[9] = 'x';
- }
-}
-
-const char *xbasename(const char *path) {
- const char *i;
-
- // Skip trailing slashes
- for (i = path + strlen(path); i > path && i[-1] == '/'; --i);
-
- // Find the beginning of the name
- for (; i > path && i[-1] != '/'; --i);
-
- // Skip leading slashes
- for (; i[0] == '/' && i[1]; ++i);
-
- return i;
-}
-
-int xfaccessat(int fd, const char *path, int amode) {
- int ret = faccessat(fd, path, amode, 0);
-
-#ifdef AT_EACCESS
- // Some platforms, like Hurd, only support AT_EACCESS. Other platforms,
- // like Android, don't support AT_EACCESS at all.
- if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) {
- ret = faccessat(fd, path, amode, AT_EACCESS);
- }
-#endif
-
- return ret;
-}
-
-int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
-#if BSD && !__GNU__
- char *str_arg = (char *)*str;
- unsigned long set_arg = 0;
- unsigned long clear_arg = 0;
-
-#if __NetBSD__
- int ret = string_to_flags(&str_arg, &set_arg, &clear_arg);
-#else
- int ret = strtofflags(&str_arg, &set_arg, &clear_arg);
-#endif
-
- *str = str_arg;
- *set = set_arg;
- *clear = clear_arg;
-
- if (ret != 0) {
- errno = EINVAL;
- }
- return ret;
-#else // !BSD
- errno = ENOTSUP;
- return -1;
-#endif
-}
-
-bool is_nonexistence_error(int error) {
- return error == ENOENT || errno == ENOTDIR;
-}
-
-/** Compile and execute a regular expression for xrpmatch(). */
-static int xrpregex(nl_item item, const char *response) {
- const char *pattern = nl_langinfo(item);
- if (!pattern) {
- return REG_BADPAT;
- }
-
- regex_t regex;
- int ret = regcomp(&regex, pattern, REG_EXTENDED);
- if (ret != 0) {
- return ret;
- }
-
- ret = regexec(&regex, response, 0, NULL, 0);
- regfree(&regex);
- return ret;
-}
-
-/** Check if a response is affirmative or negative. */
-static int xrpmatch(const char *response) {
- int ret = xrpregex(NOEXPR, response);
- if (ret == 0) {
- return 0;
- } else if (ret != REG_NOMATCH) {
- return -1;
- }
-
- ret = xrpregex(YESEXPR, response);
- if (ret == 0) {
- return 1;
- } else if (ret != REG_NOMATCH) {
- return -1;
- }
-
- // Failsafe: always handle y/n
- char c = response[0];
- if (c == 'n' || c == 'N') {
- return 0;
- } else if (c == 'y' || c == 'Y') {
- return 1;
- } else {
- return -1;
- }
-}
-
-int ynprompt() {
- fflush(stderr);
-
- char *line = xgetdelim(stdin, '\n');
- int ret = line ? xrpmatch(line) : -1;
- free(line);
- return ret;
-}
-
-dev_t bfs_makedev(int ma, int mi) {
-#ifdef makedev
- return makedev(ma, mi);
-#else
- return (ma << 8) | mi;
-#endif
-}
-
-int bfs_major(dev_t dev) {
-#ifdef major
- return major(dev);
-#else
- return dev >> 8;
-#endif
-}
-
-int bfs_minor(dev_t dev) {
-#ifdef minor
- return minor(dev);
-#else
- return dev & 0xFF;
-#endif
-}
-
-size_t xread(int fd, void *buf, size_t nbytes) {
- size_t count = 0;
-
- while (count < nbytes) {
- ssize_t ret = read(fd, (char *)buf + count, nbytes - count);
- if (ret < 0) {
- if (errno == EINTR) {
- continue;
- } else {
- break;
- }
- } else if (ret == 0) {
- // EOF
- errno = 0;
- break;
- } else {
- count += ret;
- }
- }
-
- return count;
-}
-
-size_t xwrite(int fd, const void *buf, size_t nbytes) {
- size_t count = 0;
-
- while (count < nbytes) {
- ssize_t ret = write(fd, (const char *)buf + count, nbytes - count);
- if (ret < 0) {
- if (errno == EINTR) {
- continue;
- } else {
- break;
- }
- } else if (ret == 0) {
- // EOF?
- errno = 0;
- break;
- } else {
- count += ret;
- }
- }
-
- return count;
-}
-
-char *xconfstr(int name) {
- size_t len = confstr(name, NULL, 0);
- if (len == 0) {
- return NULL;
- }
-
- char *str = malloc(len);
- if (!str) {
- return NULL;
- }
-
- if (confstr(name, str, len) != len) {
- free(str);
- return NULL;
- }
-
- return str;
-}
-
-char *xgetdelim(FILE *file, char delim) {
- char *chunk = NULL;
- size_t n = 0;
- ssize_t len = getdelim(&chunk, &n, delim, file);
- if (len >= 0) {
- if (chunk[len] == delim) {
- chunk[len] = '\0';
- }
- return chunk;
- } else {
- free(chunk);
- if (!ferror(file)) {
- errno = 0;
- }
- return NULL;
- }
-}
diff --git a/util.h b/util.h
deleted file mode 100644
index be38db5..0000000
--- a/util.h
+++ /dev/null
@@ -1,291 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Assorted utilities that don't belong anywhere else.
- */
-
-#ifndef BFS_UTIL_H
-#define BFS_UTIL_H
-
-#include <fcntl.h>
-#include <fnmatch.h>
-#include <regex.h>
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <sys/types.h>
-
-// Some portability concerns
-
-#ifdef __has_feature
-# define BFS_HAS_FEATURE(feature, fallback) __has_feature(feature)
-#else
-# define BFS_HAS_FEATURE(feature, fallback) fallback
-#endif
-
-#ifdef __has_include
-# define BFS_HAS_INCLUDE(header, fallback) __has_include(header)
-#else
-# define BFS_HAS_INCLUDE(header, fallback) fallback
-#endif
-
-#ifdef __has_c_attribute
-# define BFS_HAS_C_ATTRIBUTE(attr) __has_c_attribute(attr)
-#else
-# define BFS_HAS_C_ATTRIBUTE(attr) false
-#endif
-
-#if __GNUC__ && defined(__has_attribute)
-# define BFS_HAS_GNU_ATTRIBUTE(attr) __has_attribute(attr)
-#else
-# define BFS_HAS_GNU_ATTRIBUTE(attr) false
-#endif
-
-#ifndef BFS_HAS_MNTENT
-# define BFS_HAS_MNTENT BFS_HAS_INCLUDE(<mntent.h>, __GLIBC__)
-#endif
-
-#ifndef BFS_HAS_SYS_ACL
-# define BFS_HAS_SYS_ACL BFS_HAS_INCLUDE(<sys/acl.h>, true)
-#endif
-
-#ifndef BFS_HAS_SYS_CAPABILITY
-# define BFS_HAS_SYS_CAPABILITY BFS_HAS_INCLUDE(<sys/capability.h>, __linux__)
-#endif
-
-#ifndef BFS_HAS_SYS_EXTATTR
-# define BFS_HAS_SYS_EXTATTR BFS_HAS_INCLUDE(<sys/extattr.h>, __FreeBSD__)
-#endif
-
-#ifndef BFS_HAS_SYS_MKDEV
-# define BFS_HAS_SYS_MKDEV BFS_HAS_INCLUDE(<sys/mkdev.h>, false)
-#endif
-
-#ifndef BFS_HAS_SYS_PARAM
-# define BFS_HAS_SYS_PARAM BFS_HAS_INCLUDE(<sys/param.h>, true)
-#endif
-
-#ifndef BFS_HAS_SYS_SYSMACROS
-# define BFS_HAS_SYS_SYSMACROS BFS_HAS_INCLUDE(<sys/sysmacros.h>, __GLIBC__)
-#endif
-
-#ifndef BFS_HAS_SYS_XATTR
-# define BFS_HAS_SYS_XATTR BFS_HAS_INCLUDE(<sys/xattr.h>, __linux__)
-#endif
-
-#ifndef BFS_HAS_UTIL
-# define BFS_HAS_UTIL BFS_HAS_INCLUDE(<util.h>, __NetBSD__)
-#endif
-
-#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE)
-# define FNM_CASEFOLD FNM_IGNORECASE
-#endif
-
-#ifndef O_DIRECTORY
-# define O_DIRECTORY 0
-#endif
-
-#if BFS_HAS_C_ATTRIBUTE(fallthrough)
-# define BFS_FALLTHROUGH [[fallthrough]]
-#elif BFS_HAS_GNU_ATTRIBUTE(fallthrough)
-# define BFS_FALLTHROUGH __attribute__((fallthrough))
-#else
-# define BFS_FALLTHROUGH ((void)0)
-#endif
-
-/**
- * Adds compiler warnings for bad printf()-style function calls, if supported.
- */
-#if BFS_HAS_GNU_ATTRIBUTE(format)
-# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args)))
-#else
-# define BFS_FORMATTER(fmt, args)
-#endif
-
-// Lower bound on BFS_FLEX_SIZEOF()
-#define BFS_FLEX_LB(type, member, length) (offsetof(type, member) + sizeof(((type *)NULL)->member[0]) * (length))
-
-// Maximum macro for BFS_FLEX_SIZE()
-#define BFS_FLEX_MAX(a, b) ((a) > (b) ? (a) : (b))
-
-/**
- * Computes the size of a struct containing a flexible array member of the given
- * length.
- *
- * @param type
- * The type of the struct containing the flexible array.
- * @param member
- * The name of the flexible array member.
- * @param length
- * The length of the flexible array.
- */
-#define BFS_FLEX_SIZEOF(type, member, length) \
- (sizeof(type) <= BFS_FLEX_LB(type, member, 0) \
- ? BFS_FLEX_LB(type, member, length) \
- : BFS_FLEX_MAX(sizeof(type), BFS_FLEX_LB(type, member, length)))
-
-/**
- * readlinkat() wrapper that dynamically allocates the result.
- *
- * @param fd
- * The base directory descriptor.
- * @param path
- * The path to the link, relative to fd.
- * @param size
- * An estimate for the size of the link name (pass 0 if unknown).
- * @return The target of the link, allocated with malloc(), or NULL on failure.
- */
-char *xreadlinkat(int fd, const char *path, size_t size);
-
-/**
- * Like dup(), but set the FD_CLOEXEC flag.
- *
- * @param fd
- * The file descriptor to duplicate.
- * @return A duplicated file descriptor, or -1 on failure.
- */
-int dup_cloexec(int fd);
-
-/**
- * Like pipe(), but set the FD_CLOEXEC flag.
- *
- * @param pipefd
- * The array to hold the two file descriptors.
- * @return 0 on success, -1 on failure.
- */
-int pipe_cloexec(int pipefd[2]);
-
-/**
- * Dynamically allocate a regex error message.
- *
- * @param err
- * The error code to stringify.
- * @param regex
- * The (partially) compiled regex.
- * @return A human-readable description of the error, allocated with malloc().
- */
-char *xregerror(int err, const regex_t *regex);
-
-/**
- * Format a mode like ls -l (e.g. -rw-r--r--).
- *
- * @param mode
- * The mode to format.
- * @param str
- * The string to hold the formatted mode.
- */
-void xstrmode(mode_t mode, char str[11]);
-
-/**
- * basename() variant that doesn't modify the input.
- *
- * @param path
- * The path in question.
- * @return A pointer into path at the base name offset.
- */
-const char *xbasename(const char *path);
-
-/**
- * Wrapper for faccessat() that handles some portability issues.
- */
-int xfaccessat(int fd, const char *path, int amode);
-
-/**
- * Portability wrapper for strtofflags().
- *
- * @param str
- * The string to parse. The pointee will be advanced to the first
- * invalid position on error.
- * @param set
- * The flags that are set in the string.
- * @param clear
- * The flags that are cleared in the string.
- * @return
- * 0 on success, -1 on failure.
- */
-int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear);
-
-/**
- * Return whether an error code is due to a path not existing.
- */
-bool is_nonexistence_error(int error);
-
-/**
- * Process a yes/no prompt.
- *
- * @return 1 for yes, 0 for no, and -1 for unknown.
- */
-int ynprompt(void);
-
-/**
- * Portable version of makedev().
- */
-dev_t bfs_makedev(int ma, int mi);
-
-/**
- * Portable version of major().
- */
-int bfs_major(dev_t dev);
-
-/**
- * Portable version of minor().
- */
-int bfs_minor(dev_t dev);
-
-/**
- * A safe version of read() that handles interrupted system calls and partial
- * reads.
- *
- * @return
- * The number of bytes read. A value != nbytes indicates an error
- * (errno != 0) or end of file (errno == 0).
- */
-size_t xread(int fd, void *buf, size_t nbytes);
-
-/**
- * A safe version of write() that handles interrupted system calls and partial
- * writes.
- *
- * @return
- The number of bytes written. A value != nbytes indicates an error.
- */
-size_t xwrite(int fd, const void *buf, size_t nbytes);
-
-/**
- * Wrapper for confstr() that allocates with malloc().
- *
- * @param name
- * The ID of the confstr to look up.
- * @return
- * The value of the confstr, or NULL on failure.
- */
-char *xconfstr(int name);
-
-/**
- * Convenience wrapper for getdelim().
- *
- * @param file
- * The file to read.
- * @param delim
- * The delimiter character to split on.
- * @return
- * The read chunk (without the delimiter), allocated with malloc().
- * NULL is returned on error (errno != 0) or end of file (errno == 0).
- */
-char *xgetdelim(FILE *file, char delim);
-
-#endif // BFS_UTIL_H