summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/codeql.yml9
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/ci.yml173
-rw-r--r--.github/workflows/codecov.yml19
-rw-r--r--.github/workflows/codeql.yml60
-rw-r--r--.gitignore2
-rw-r--r--LICENSE19
-rw-r--r--Makefile535
-rw-r--r--README.md198
-rw-r--r--bench/.gitignore3
-rw-r--r--bench/README.md51
-rw-r--r--bench/bench.sh715
-rwxr-xr-xbench/clone-tree.sh143
-rw-r--r--completions/bfs.bash26
-rw-r--r--completions/bfs.fish3
-rw-r--r--completions/bfs.zsh11
-rwxr-xr-xconfig/cc.sh16
-rw-r--r--config/config.mk63
-rwxr-xr-xconfig/define-if.sh20
-rw-r--r--config/deps.mk18
-rw-r--r--config/empty.c6
-rw-r--r--config/exports.mk20
-rw-r--r--config/flags.mk136
-rw-r--r--config/has/acl-get-entry.c8
-rw-r--r--config/has/acl-get-file.c8
-rw-r--r--config/has/acl-get-tag-type.c10
-rw-r--r--config/has/acl-is-trivial-np.c12
-rw-r--r--config/has/acl-trivial.c8
-rw-r--r--config/has/aligned-alloc.c8
-rw-r--r--config/has/confstr.c9
-rw-r--r--config/has/extattr-get-file.c10
-rw-r--r--config/has/extattr-get-link.c10
-rw-r--r--config/has/extattr-list-file.c10
-rw-r--r--config/has/extattr-list-link.c10
-rw-r--r--config/has/fdclosedir.c8
-rw-r--r--config/has/getdents.c10
-rw-r--r--config/has/getdents64-syscall.c12
-rw-r--r--config/has/getdents64.c10
-rw-r--r--config/has/getprogname-gnu.c9
-rw-r--r--config/has/getprogname.c9
-rw-r--r--config/has/max-align-t.c8
-rw-r--r--config/has/pipe2.c10
-rw-r--r--config/has/posix-spawn-addfchdir-np.c11
-rw-r--r--config/has/posix-spawn-addfchdir.c11
-rw-r--r--config/has/st-acmtim.c12
-rw-r--r--config/has/st-acmtimespec.c12
-rw-r--r--config/has/st-birthtim.c9
-rw-r--r--config/has/st-birthtimespec.c9
-rw-r--r--config/has/st-flags.c9
-rw-r--r--config/has/statx-syscall.c13
-rw-r--r--config/has/statx.c11
-rw-r--r--config/has/strerror-l.c11
-rw-r--r--config/has/strerror-r-gnu.c11
-rw-r--r--config/has/strerror-r-posix.c11
-rw-r--r--config/has/tm-gmtoff.c9
-rw-r--r--config/has/uselocale.c9
-rw-r--r--config/header.mk69
-rwxr-xr-xconfig/pkgconf.sh100
-rw-r--r--config/pkgs.mk36
-rw-r--r--config/prelude.mk162
-rw-r--r--config/use/libacl.c9
-rw-r--r--config/use/libcap.c9
-rw-r--r--config/use/libselinux.c9
-rw-r--r--config/use/liburing.c9
-rw-r--r--config/use/oniguruma.c9
-rw-r--r--docs/BUILDING.md98
-rw-r--r--docs/CHANGELOG.md252
-rw-r--r--docs/CONTRIBUTING.md (renamed from docs/HACKING.md)32
-rw-r--r--docs/RELATED.md42
-rw-r--r--docs/USAGE.md56
-rw-r--r--docs/bfs.1215
-rw-r--r--src/alloc.c384
-rw-r--r--src/alloc.h383
-rw-r--r--src/atomic.h85
-rw-r--r--src/bar.c49
-rw-r--r--src/bar.h17
-rw-r--r--src/bfs.h32
-rw-r--r--src/bfstd.c989
-rw-r--r--src/bfstd.h518
-rw-r--r--src/bftw.c2456
-rw-r--r--src/bftw.h49
-rw-r--r--src/bit.h401
-rw-r--r--src/color.c923
-rw-r--r--src/color.h43
-rw-r--r--src/ctx.c193
-rw-r--r--src/ctx.h95
-rw-r--r--src/darray.c103
-rw-r--r--src/darray.h110
-rw-r--r--src/diag.c145
-rw-r--r--src/diag.h161
-rw-r--r--src/dir.c380
-rw-r--r--src/dir.h89
-rw-r--r--src/dstring.c231
-rw-r--r--src/dstring.h212
-rw-r--r--src/eval.c548
-rw-r--r--src/eval.h23
-rw-r--r--src/exec.c127
-rw-r--r--src/exec.h17
-rw-r--r--src/expr.c85
-rw-r--r--src/expr.h110
-rw-r--r--src/fsade.c264
-rw-r--r--src/fsade.h48
-rw-r--r--src/ioq.c1100
-rw-r--r--src/ioq.h198
-rw-r--r--src/list.h581
-rw-r--r--src/main.c51
-rw-r--r--src/mtab.c208
-rw-r--r--src/mtab.h25
-rw-r--r--src/opt.c2613
-rw-r--r--src/opt.h18
-rw-r--r--src/parse.c2188
-rw-r--r--src/parse.h17
-rw-r--r--src/prelude.h385
-rw-r--r--src/printf.c567
-rw-r--r--src/printf.h17
-rw-r--r--src/pwcache.c353
-rw-r--r--src/pwcache.h85
-rw-r--r--src/sanity.h94
-rw-r--r--src/stat.c358
-rw-r--r--src/stat.h103
-rw-r--r--src/thread.c81
-rw-r--r--src/thread.h99
-rw-r--r--src/trie.c430
-rw-r--r--src/trie.h75
-rw-r--r--src/typo.c22
-rw-r--r--src/typo.h17
-rw-r--r--src/util.c510
-rw-r--r--src/util.h323
-rw-r--r--src/xregex.c114
-rw-r--r--src/xregex.h18
-rw-r--r--src/xspawn.c649
-rw-r--r--src/xspawn.h69
-rw-r--r--src/xtime.c193
-rw-r--r--src/xtime.h53
-rw-r--r--tests/alloc.c52
-rw-r--r--tests/bfs/D_all.out (renamed from tests/test_D_all.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_D_multi.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_O0.out)0
-rw-r--r--tests/bfs/D_unknown.sh4
-rw-r--r--tests/bfs/L_capable.out2
-rw-r--r--tests/bfs/L_capable.sh10
-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_O1.out)0
-rw-r--r--tests/bfs/O0.sh1
-rw-r--r--tests/bfs/O1.out (renamed from tests/test_O2.out)0
-rw-r--r--tests/bfs/O1.sh1
-rw-r--r--tests/bfs/O2.out (renamed from tests/test_O3.out)0
-rw-r--r--tests/bfs/O2.sh1
-rw-r--r--tests/bfs/O3.out (renamed from tests/test_Ofast.out)0
-rw-r--r--tests/bfs/O3.sh1
-rw-r--r--tests/bfs/O9.out (renamed from tests/test_S_dfs.out)0
-rw-r--r--tests/bfs/O9.sh4
-rw-r--r--tests/bfs/Ofast.out (renamed from tests/test_basic.out)0
-rw-r--r--tests/bfs/Ofast.sh1
-rw-r--r--tests/bfs/S_bfs.out (renamed from tests/test_S_bfs.out)0
-rw-r--r--tests/bfs/S_bfs.sh2
-rw-r--r--tests/bfs/S_dfs.out (renamed from tests/test_closed_stdin.out)0
-rw-r--r--tests/bfs/S_dfs.sh2
-rw-r--r--tests/bfs/S_ids.out (renamed from tests/test_S_ids.out)0
-rw-r--r--tests/bfs/S_ids.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.out (renamed from tests/test_d_path.out)0
-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_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_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_ext_underride.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.out (renamed from tests/test_color_mh0.out)13
-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_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)13
-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.out (renamed from tests/test_data_flow_group.out)0
-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_exclude_depth.out)0
-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_ilname.out)0
-rw-r--r--tests/bfs/exclude_mindepth.sh1
-rw-r--r--tests/bfs/exclude_name.out (renamed from tests/test_exclude_name.out)0
-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_exec_flush_fprint.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_plus.out (renamed from tests/test_execdir_plus.out)0
-rw-r--r--tests/bfs/execdir_plus.sh4
-rw-r--r--tests/bfs/execdir_plus_nonexistent.out (renamed from tests/test_data_flow_hidden.out)0
-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/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_append.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)0
-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.out (renamed from tests/test_data_flow_sparse.out)0
-rw-r--r--tests/bfs/j1.sh1
-rw-r--r--tests/bfs/j64.out (renamed from tests/test_data_flow_user.out)0
-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/nohidden.out (renamed from tests/test_nohidden.out)0
-rw-r--r--tests/bfs/nohidden.sh1
-rw-r--r--tests/bfs/nohidden_depth.out (renamed from tests/test_nohidden_depth.out)0
-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)0
-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.out (renamed from tests/test_L_lname.out)0
-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.out (renamed from tests/test_printf_color.out)16
-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_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.out (renamed from tests/test_daystart.out)0
-rw-r--r--tests/bfs/stderr_fails_silently.sh2
-rw-r--r--tests/bfs/type_multi.out (renamed from tests/test_type_multi.out)0
-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.out (renamed from tests/test_daystart_twice.out)0
-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_xdev_mount.out (renamed from tests/test_depth.out)0
-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)0
-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.sh4
-rw-r--r--tests/bfstd.c93
-rw-r--r--tests/bit.c141
-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/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)0
-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.out (renamed from tests/test_depth_overflow.out)0
-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)0
-rw-r--r--tests/bsd/data_flow_depth.sh1
-rw-r--r--tests/bsd/data_flow_sparse.out (renamed from tests/test_exec.out)0
-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.out (renamed from tests/test_exec_nopath.out)0
-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)0
-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.out (renamed from tests/test_exec_plus_status.out)0
-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.out (renamed from tests/test_perm_000_minus.out)0
-rw-r--r--tests/bsd/perm_000_plus.sh1
-rw-r--r--tests/bsd/perm_222_plus.out (renamed from tests/test_perm_222_plus.out)0
-rw-r--r--tests/bsd/perm_222_plus.sh1
-rw-r--r--tests/bsd/perm_644_plus.out (renamed from tests/test_perm_644_plus.out)0
-rw-r--r--tests/bsd/perm_644_plus.sh1
-rw-r--r--tests/bsd/printx.out (renamed from tests/test_printx.out)0
-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)0
-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.out (renamed from tests/test_fprint.out)0
-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.sh37
-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_exit_no_implicit_print.out)0
-rw-r--r--tests/common/L_ilname.sh2
-rw-r--r--tests/common/L_lname.out (renamed from tests/test_false.out)0
-rw-r--r--tests/common/L_lname.sh1
-rw-r--r--tests/common/L_ls.sh1
-rw-r--r--tests/common/L_mount.out5
-rw-r--r--tests/common/L_mount.sh13
-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.out (renamed from tests/test_H.out)0
-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/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)0
-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)0
-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)0
-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)0
-rw-r--r--tests/common/empty.sh1
-rw-r--r--tests/common/empty_error.out3
-rw-r--r--tests/common/empty_error.sh7
-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)0
-rw-r--r--tests/common/exec_substring.sh1
-rw-r--r--tests/common/execdir_nonexistent.out (renamed from tests/test_fstype.out)0
-rw-r--r--tests/common/execdir_nonexistent.sh2
-rw-r--r--tests/common/execdir_pwd.out (renamed from tests/test_execdir_pwd.out)0
-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_L.out)0
-rw-r--r--tests/common/follow.sh1
-rw-r--r--tests/common/ilname.out (renamed from tests/test_ilname.out)0
-rw-r--r--tests/common/ilname.sh2
-rw-r--r--tests/common/iname.out (renamed from tests/test_iname.out)0
-rw-r--r--tests/common/iname.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_ipath.out)0
-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_lname.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)0
-rw-r--r--tests/common/mindepth.sh1
-rw-r--r--tests/common/mindepth_incomplete.sh1
-rw-r--r--tests/common/mount.out4
-rw-r--r--tests/common/mount.sh11
-rw-r--r--tests/common/name_slash.out (renamed from tests/test_name_slash.out)0
-rw-r--r--tests/common/name_slash.sh1
-rw-r--r--tests/common/name_slashes.out (renamed from tests/test_name_slashes.out)0
-rw-r--r--tests/common/name_slashes.sh1
-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_name_backslash.out)0
-rw-r--r--tests/common/ok_closed_stdin.sh1
-rw-r--r--tests/common/okdir_closed_stdin.out (renamed from tests/test_nogroup.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_nogroup_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_nouser.out)0
-rw-r--r--tests/common/size_big.sh1
-rwxr-xr-xtests/find-color.sh17
-rw-r--r--tests/getopts.sh179
-rw-r--r--tests/gnu/L_delete.out2
-rw-r--r--tests/gnu/L_delete.sh8
-rw-r--r--tests/gnu/L_loops_continue.out (renamed from tests/test_L_loops_continue.out)0
-rw-r--r--tests/gnu/L_loops_continue.sh1
-rw-r--r--tests/gnu/L_xtype_f.out (renamed from tests/test_L_xtype_f.out)0
-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)0
-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_nouser_ulimit.out)0
-rw-r--r--tests/gnu/and_purity.sh2
-rw-r--r--tests/gnu/comma.out (renamed from tests/test_comma.out)0
-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_fprint_truncate.out)0
-rw-r--r--tests/gnu/comma_redundant_false.sh2
-rw-r--r--tests/gnu/comma_redundant_true.out (renamed from tests/test_not_reachability.out)0
-rw-r--r--tests/gnu/comma_redundant_true.sh2
-rw-r--r--tests/gnu/daystart.out (renamed from tests/test_gid.out)0
-rw-r--r--tests/gnu/daystart.sh1
-rw-r--r--tests/gnu/daystart_twice.out (renamed from tests/test_gid_minus.out)0
-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.out (renamed from tests/test_exec_plus_flush.out)bin22 -> 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.out (renamed from tests/test_execdir.out)0
-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_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.out (renamed from tests/test_executable.out)0
-rw-r--r--tests/gnu/executable.sh1
-rw-r--r--tests/gnu/false.out (renamed from tests/test_ok_closed_stdin.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_file.out)0
-rw-r--r--tests/gnu/files0_from_file.sh4
-rw-r--r--tests/gnu/files0_from_none.out (renamed from tests/test_okdir_closed_stdin.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_ok.sh1
-rw-r--r--tests/gnu/files0_from_stdin.out (renamed from tests/test_files0_from_stdin.out)0
-rw-r--r--tests/gnu/files0_from_stdin.sh2
-rw-r--r--tests/gnu/fls.sh1
-rw-r--r--tests/gnu/fls_nonexistent.sh1
-rw-r--r--tests/gnu/follow_comma.out (renamed from tests/test_follow_comma.out)0
-rw-r--r--tests/gnu/follow_comma.sh3
-rw-r--r--tests/gnu/fprint.out (renamed from tests/test_gid_minus_plus.out)0
-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_noerror.sh3
-rw-r--r--tests/gnu/fprint_nonexistent.sh1
-rw-r--r--tests/gnu/fprint_truncate.out (renamed from tests/test_printf_leak.out)0
-rw-r--r--tests/gnu/fprint_truncate.sh5
-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.out (renamed from tests/test_gid_name.out)0
-rw-r--r--tests/gnu/fstype.sh2
-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_or_purity.out)0
-rw-r--r--tests/gnu/fstype_umount.sh12
-rw-r--r--tests/gnu/gid.out (renamed from tests/test_gid_plus.out)0
-rw-r--r--tests/gnu/gid.sh1
-rw-r--r--tests/gnu/gid_minus.out (renamed from tests/test_gid_plus_plus.out)0
-rw-r--r--tests/gnu/gid_minus.sh1
-rw-r--r--tests/gnu/gid_minus_plus.out (renamed from tests/test_group_id.out)0
-rw-r--r--tests/gnu/gid_minus_plus.sh1
-rw-r--r--tests/gnu/gid_plus.out (renamed from tests/test_group_name.out)0
-rw-r--r--tests/gnu/gid_plus.sh2
-rw-r--r--tests/gnu/gid_plus_plus.out (renamed from tests/test_group_nogroup.out)0
-rw-r--r--tests/gnu/gid_plus_plus.sh2
-rw-r--r--tests/gnu/ignore_readdir_race.sh5
-rw-r--r--tests/gnu/ignore_readdir_race_notdir.sh7
-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_iwholename.out)0
-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.out (renamed from tests/test_path_d.out)0
-rw-r--r--tests/gnu/noleaf.sh1
-rw-r--r--tests/gnu/not.out (renamed from tests/test_bang.out)0
-rw-r--r--tests/gnu/not.sh1
-rw-r--r--tests/gnu/not_reachability.out (renamed from tests/test_quit_after_print.out)0
-rw-r--r--tests/gnu/not_reachability.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)0
-rw-r--r--tests/gnu/or.sh1
-rw-r--r--tests/gnu/path_d.out (renamed from tests/test_stderr_fails_silently.out)0
-rw-r--r--tests/gnu/path_d.sh1
-rw-r--r--tests/gnu/perm_000_slash.out (renamed from tests/test_perm_000_plus.out)0
-rw-r--r--tests/gnu/perm_000_slash.sh1
-rw-r--r--tests/gnu/perm_222_slash.out (renamed from tests/test_perm_222_slash.out)0
-rw-r--r--tests/gnu/perm_222_slash.sh1
-rw-r--r--tests/gnu/perm_644_slash.out (renamed from tests/test_perm_644_slash.out)0
-rw-r--r--tests/gnu/perm_644_slash.sh1
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.out (renamed from tests/test_perm_leading_plus_symbolic_slash.out)0
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.sh1
-rw-r--r--tests/gnu/perm_symbolic_slash.out (renamed from tests/test_perm_symbolic_slash.out)0
-rw-r--r--tests/gnu/perm_symbolic_slash.sh1
-rw-r--r--tests/gnu/precedence.out (renamed from tests/test_precedence.out)0
-rw-r--r--tests/gnu/precedence.sh1
-rw-r--r--tests/gnu/print0.outbin0 -> 16 bytes
-rw-r--r--tests/gnu/print0.sh2
-rw-r--r--tests/gnu/print_error.sh2
-rw-r--r--tests/gnu/printf.out (renamed from tests/test_printf.out)0
-rw-r--r--tests/gnu/printf.sh1
-rw-r--r--tests/gnu/printf_H.out (renamed from tests/test_printf_H.out)0
-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_leading_plus_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)0
-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)0
-rw-r--r--tests/gnu/printf_l_nonlink.sh1
-rw-r--r--tests/gnu/printf_leak.out (renamed from tests/test_quit_implicit_print.out)0
-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)0
-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)0
-rw-r--r--tests/gnu/printf_trailing_slashes.sh1
-rw-r--r--tests/gnu/printf_types.out (renamed from tests/test_printf_types.out)0
-rw-r--r--tests/gnu/printf_types.sh1
-rw-r--r--tests/gnu/printf_u_g_ulimit.sh2
-rw-r--r--tests/gnu/readable.out (renamed from tests/test_readable.out)0
-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_ed.out (renamed from tests/test_regextype_ed.out)0
-rw-r--r--tests/gnu/regextype_ed.sh2
-rw-r--r--tests/gnu/regextype_emacs.out (renamed from tests/test_regextype_emacs.out)0
-rw-r--r--tests/gnu/regextype_emacs.sh3
-rw-r--r--tests/gnu/regextype_grep.out (renamed from tests/test_name.out)0
-rw-r--r--tests/gnu/regextype_grep.sh3
-rw-r--r--tests/gnu/regextype_posix_basic.out (renamed from tests/test_regextype_posix_basic.out)0
-rw-r--r--tests/gnu/regextype_posix_basic.sh2
-rw-r--r--tests/gnu/regextype_posix_extended.out (renamed from tests/test_regextype_posix_extended.out)0
-rw-r--r--tests/gnu/regextype_posix_extended.sh2
-rw-r--r--tests/gnu/regextype_sed.out (renamed from tests/test_regextype_sed.out)0
-rw-r--r--tests/gnu/regextype_sed.sh2
-rw-r--r--tests/gnu/true.out (renamed from tests/test_true.out)0
-rw-r--r--tests/gnu/true.sh1
-rw-r--r--tests/gnu/uid.out (renamed from tests/test_uid.out)0
-rw-r--r--tests/gnu/uid.sh1
-rw-r--r--tests/gnu/uid_minus.out (renamed from tests/test_uid_minus.out)0
-rw-r--r--tests/gnu/uid_minus.sh1
-rw-r--r--tests/gnu/uid_minus_plus.out (renamed from tests/test_uid_minus_plus.out)0
-rw-r--r--tests/gnu/uid_minus_plus.sh1
-rw-r--r--tests/gnu/uid_plus.out (renamed from tests/test_uid_name.out)0
-rw-r--r--tests/gnu/uid_plus.sh2
-rw-r--r--tests/gnu/uid_plus_plus.out (renamed from tests/test_uid_plus.out)0
-rw-r--r--tests/gnu/uid_plus_plus.sh2
-rw-r--r--tests/gnu/used.out4
-rw-r--r--tests/gnu/used.sh40
-rw-r--r--tests/gnu/wholename.out (renamed from tests/test_path.out)0
-rw-r--r--tests/gnu/wholename.sh1
-rw-r--r--tests/gnu/writable.out (renamed from tests/test_writable.out)0
-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)0
-rw-r--r--tests/gnu/xtype_f.sh1
-rw-r--r--tests/gnu/xtype_l.out (renamed from tests/test_xtype_l.out)0
-rw-r--r--tests/gnu/xtype_l.sh1
-rw-r--r--tests/ioq.c77
-rwxr-xr-xtests/ls-color.sh54
-rw-r--r--tests/main.c125
-rw-r--r--tests/mksock.c45
-rw-r--r--tests/posix/H.out (renamed from tests/test_P.out)0
-rw-r--r--tests/posix/H.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.out (renamed from tests/test_L_depth.out)0
-rw-r--r--tests/posix/L.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.out (renamed from tests/test_follow.out)0
-rw-r--r--tests/posix/L_depth.sh1
-rw-r--r--tests/posix/L_loops.sh4
-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/bang.out (renamed from tests/test_not.out)0
-rw-r--r--tests/posix/bang.sh1
-rw-r--r--tests/posix/basic.out (renamed from tests/test_uid_plus_plus.out)0
-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)0
-rw-r--r--tests/posix/data_flow_and_swap.sh1
-rw-r--r--tests/posix/data_flow_group.out (renamed from tests/test_unique_depth.out)0
-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)0
-rw-r--r--tests/posix/data_flow_or_swap.sh1
-rw-r--r--tests/posix/data_flow_type.out (renamed from tests/test_perm_leading_plus_symbolic_minus.out)0
-rw-r--r--tests/posix/data_flow_type.sh1
-rw-r--r--tests/posix/data_flow_user.out (renamed from tests/test_user_id.out)0
-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)0
-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)0
-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.out (renamed from tests/test_user_name.out)0
-rw-r--r--tests/posix/depth.sh1
-rw-r--r--tests/posix/depth_error.out2
-rw-r--r--tests/posix/depth_error.sh7
-rw-r--r--tests/posix/depth_slash.out (renamed from tests/test_depth_slash.out)0
-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.out (renamed from tests/test_user_nouser.out)0
-rw-r--r--tests/posix/exec.sh1
-rw-r--r--tests/posix/exec_nonexistent.out (renamed from tests/test_fprint_duplicate_stdout.out)19
-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)0
-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_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)0
-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_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/implicit_and.out (renamed from tests/test_implicit_and.out)0
-rw-r--r--tests/posix/implicit_and.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/name.out (renamed from tests/test_name_star_star.out)0
-rw-r--r--tests/posix/name.sh1
-rw-r--r--tests/posix/name_backslash.out (renamed from tests/test_perm_symbolic.out)0
-rw-r--r--tests/posix/name_backslash.sh2
-rw-r--r--tests/posix/name_bracket.out (renamed from tests/test_name_bracket.out)0
-rw-r--r--tests/posix/name_bracket.sh9
-rw-r--r--tests/posix/name_character_class.out (renamed from tests/test_name_character_class.out)0
-rw-r--r--tests/posix/name_character_class.sh1
-rw-r--r--tests/posix/name_double_backslash.out (renamed from tests/test_name_double_backslash.out)0
-rw-r--r--tests/posix/name_double_backslash.sh2
-rw-r--r--tests/posix/name_root.out (renamed from tests/test_name_root.out)0
-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_star_star.out (renamed from tests/test_parens.out)0
-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_printf_empty.out)0
-rw-r--r--tests/posix/nogroup.sh1
-rw-r--r--tests/posix/nogroup_ulimit.out (renamed from tests/test_printf_w.out)0
-rw-r--r--tests/posix/nogroup_ulimit.sh2
-rw-r--r--tests/posix/not_prune.out (renamed from tests/test_not_prune.out)0
-rw-r--r--tests/posix/not_prune.sh1
-rw-r--r--tests/posix/nouser.out (renamed from tests/test_quit_before_print.out)0
-rw-r--r--tests/posix/nouser.sh1
-rw-r--r--tests/posix/nouser_ulimit.out (renamed from tests/test_size_big.out)0
-rw-r--r--tests/posix/nouser_ulimit.sh2
-rw-r--r--tests/posix/o.out (renamed from tests/test_or.out)0
-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)0
-rw-r--r--tests/posix/ok_stdin.sh3
-rw-r--r--tests/posix/or_purity.out (renamed from tests/test_xtype_reorder.out)0
-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.out (renamed from tests/test_regextype_grep.out)0
-rw-r--r--tests/posix/parens.sh1
-rw-r--r--tests/posix/path.out (renamed from tests/test_wholename.out)0
-rw-r--r--tests/posix/path.sh1
-rw-r--r--tests/posix/perm_000.out (renamed from tests/test_perm_000.out)0
-rw-r--r--tests/posix/perm_000.sh1
-rw-r--r--tests/posix/perm_000_minus.out (renamed from tests/test_perm_000_slash.out)0
-rw-r--r--tests/posix/perm_000_minus.sh1
-rw-r--r--tests/posix/perm_222.out (renamed from tests/test_perm_222.out)0
-rw-r--r--tests/posix/perm_222.sh1
-rw-r--r--tests/posix/perm_222_minus.out (renamed from tests/test_perm_222_minus.out)0
-rw-r--r--tests/posix/perm_222_minus.sh1
-rw-r--r--tests/posix/perm_644.out (renamed from tests/test_perm_644.out)0
-rw-r--r--tests/posix/perm_644.sh1
-rw-r--r--tests/posix/perm_644_minus.out (renamed from tests/test_perm_644_minus.out)0
-rw-r--r--tests/posix/perm_644_minus.sh1
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.out0
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.sh1
-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.out (renamed from tests/test_perm_symbolic_minus.out)0
-rw-r--r--tests/posix/perm_symbolic_minus.sh1
-rw-r--r--tests/posix/permcopy.out (renamed from tests/test_permcopy.out)0
-rw-r--r--tests/posix/permcopy.sh1
-rw-r--r--tests/posix/prune.out (renamed from tests/test_prune.out)0
-rw-r--r--tests/posix/prune.sh1
-rw-r--r--tests/posix/prune_file.out (renamed from tests/test_prune_file.out)0
-rw-r--r--tests/posix/prune_file.sh1
-rw-r--r--tests/posix/prune_or_print.out (renamed from tests/test_prune_or_print.out)0
-rw-r--r--tests/posix/prune_or_print.sh1
-rw-r--r--tests/posix/readdir_error.sh37
-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)0
-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_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/weird_names.out (renamed from tests/test_weird_names.out)0
-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/run.sh464
-rw-r--r--tests/stddirs.sh152
-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_acl.out1
-rw-r--r--tests/test_capable.out1
-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_delete.out1
-rw-r--r--tests/test_delete_many.out1
-rw-r--r--tests/test_depth_error.out2
-rw-r--r--tests/test_empty_special.out14
-rw-r--r--tests/test_exec_flush.outbin310 -> 0 bytes
-rw-r--r--tests/test_flags.out1
-rw-r--r--tests/test_inum_automount.out1
-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_print0.outbin17 -> 0 bytes
-rw-r--r--tests/test_printf_Y_error.out3
-rw-r--r--tests/test_printf_nul.outbin8 -> 0 bytes
-rw-r--r--tests/test_regex_invalid_utf8.out1
-rw-r--r--tests/test_rm.out1
-rw-r--r--tests/test_type_bind_mount.out1
-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.h73
-rw-r--r--tests/tests.mk13
-rwxr-xr-xtests/tests.sh3468
-rw-r--r--tests/trie.c96
-rw-r--r--tests/util.sh185
-rw-r--r--tests/xspawn.c206
-rw-r--r--tests/xspawnee.c17
-rw-r--r--tests/xtime.c191
-rw-r--r--tests/xtimegm.c107
-rw-r--r--tests/xtouch.c282
1057 files changed, 21769 insertions, 11406 deletions
diff --git a/.github/codeql.yml b/.github/codeql.yml
new file mode 100644
index 0000000..6ff8337
--- /dev/null
+++ b/.github/codeql.yml
@@ -0,0 +1,9 @@
+query-filters:
+ - exclude:
+ id: cpp/commented-out-code
+ - exclude:
+ id: cpp/long-switch
+ - exclude:
+ id: cpp/loop-variable-changed
+ - exclude:
+ id: cpp/poorly-documented-function
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9db363d..3890bb0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,34 +6,36 @@ jobs:
linux:
name: Linux
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo dpkg --add-architecture i386
sudo apt-get update -y
sudo apt-get install -y \
+ expect \
gcc-multilib \
+ libgcc-s1:i386 \
acl \
libacl1-dev \
libacl1:i386 \
attr \
- libattr1-dev \
- libattr1:i386 \
libcap2-bin \
libcap-dev \
libcap2:i386 \
libonig-dev \
- libonig5:i386
+ libonig5:i386 \
+ liburing-dev
# Ubuntu doesn't let you install the -dev packages for both amd64 and
- # i386 at once, so we make our own symlinks to fix -m32 -lacl -lattr -lcap
+ # i386 at once, so we make our own symlinks to fix -m32 -lacl -l...
sudo ln -s libacl.so.1 /lib/i386-linux-gnu/libacl.so
- sudo ln -s libattr.so.1 /lib/i386-linux-gnu/libattr.so
sudo ln -s libcap.so.2 /lib/i386-linux-gnu/libcap.so
sudo ln -s libonig.so.5 /lib/i386-linux-gnu/libonig.so
+ # Work around https://github.com/actions/runner-images/issues/9491
+ sudo sysctl vm.mmap_rnd_bits=28
- name: Run tests
run: |
@@ -42,46 +44,159 @@ jobs:
macos:
name: macOS
- runs-on: macos-latest
+ runs-on: macos-14
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install dependencies
run: |
- brew install coreutils
+ brew install \
+ bash \
+ expect
- name: Run tests
run: |
- make -j$(sysctl -n hw.ncpu) distcheck
+ jobs=$(sysctl -n hw.ncpu)
+ make -j$jobs distcheck
freebsd:
name: FreeBSD
- if: ${{ github.repository_owner == 'tavianator' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
+ runs-on: ubuntu-22.04
- runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: cross-platform-actions/action@v0.24.0
+ with:
+ operating_system: freebsd
+ version: "14.0"
+ sync_files: runner-to-vm
+
+ run: |
+ sudo pkg install -y \
+ bash \
+ expect \
+ oniguruma \
+ pkgconf \
+ tcl-wrapper
+ sudo mount -t fdescfs none /dev/fd
+ make -j$(nproc) distcheck
+
+ openbsd:
+ name: OpenBSD
- concurrency: spurion
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: tailscale/github-action@main
+ - name: Run tests
+ uses: cross-platform-actions/action@v0.24.0
with:
- authkey: ${{ secrets.TAILSCALE_KEY }}
+ operating_system: openbsd
+ version: "7.5"
+ sync_files: runner-to-vm
- - name: Configure SSH
- env:
- SSH_KEY: ${{ secrets.SSH_KEY }}
- run: |
- mkdir ~/.ssh
- printf '%s' "$SSH_KEY" >~/.ssh/github-actions
- chmod 0600 ~/.ssh/github-actions
- printf 'Host %s\n\tStrictHostKeyChecking=accept-new\n\tUser github\n\tIdentityFile ~/.ssh/github-actions\n' "$(tailscale ip -6 spurion)" >~/.ssh/config
+ run: |
+ sudo pkg_add \
+ bash \
+ expect \
+ gmake \
+ oniguruma
+ jobs=$(sysctl -n hw.ncpu)
+ gmake -j$jobs config
+ gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ netbsd:
+ name: NetBSD
+
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v4
- name: Run tests
- run: |
- spurion=$(tailscale ip -6 spurion)
- rsync -rl --delete . "[$spurion]:bfs"
- ssh "$spurion" 'gmake -C bfs -j$(sysctl -n hw.ncpu) distcheck'
+ uses: cross-platform-actions/action@v0.24.0
+ with:
+ operating_system: netbsd
+ version: "10.0"
+ sync_files: runner-to-vm
+
+ run: |
+ PATH="/sbin:/usr/sbin:$PATH"
+ sudo pkgin -y install \
+ bash \
+ oniguruma \
+ pkgconf \
+ tcl-expect
+ jobs=$(sysctl -n hw.ncpu)
+ make -j$jobs config
+ make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ dragonflybsd:
+ name: DragonFly BSD
+
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: vmactions/dragonflybsd-vm@v1
+ with:
+ release: "6.4.0"
+ usesh: true
+ copyback: false
+
+ prepare: |
+ pkg install -y \
+ bash \
+ expect \
+ oniguruma \
+ pkgconf \
+ sudo \
+ tcl-wrapper
+ pw useradd -n action -m -G wheel -s /usr/local/bin/bash
+ echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers
+
+ run: |
+ chown -R action:action .
+ jobs=$(sysctl -n hw.ncpu)
+ sudo -u action make config
+ sudo -u action make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
+
+ omnios:
+ name: OmniOS
+
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: vmactions/omnios-vm@v1
+ with:
+ release: "r151048"
+ usesh: true
+ copyback: false
+
+ prepare: |
+ pkg install \
+ bash \
+ build-essential \
+ expect \
+ gnu-make \
+ onig \
+ sudo
+ useradd -m -g staff action
+ echo "%staff ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
+
+ run: |
+ PATH="/usr/xpg4/bin:$PATH"
+ chown -R action:staff .
+ jobs=$(getconf NPROCESSORS_ONLN)
+ sudo -u action gmake config
+ sudo -u action gmake -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index c8808d3..2245f40 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -1,32 +1,35 @@
name: codecov.io
-on: [push, pull_request]
+on: [push]
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
+ expect \
gcc \
acl \
libacl1-dev \
attr \
- libattr1-dev \
libcap2-bin \
libcap-dev \
- libonig-dev
+ libonig-dev \
+ liburing-dev
- name: Generate coverage
run: |
- make -j$(nproc) gcov check TEST_FLAGS="--sudo"
- gcov -abcfu obj/*/*.o
+ make config GCOV=y
+ make -j$(nproc) check TEST_FLAGS="--sudo"
+ gcov -abcfpu obj/*/*.o
- - uses: codecov/codecov-action@v3.1.0
+ - uses: codecov/codecov-action@v4
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..c21fda5
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,60 @@
+name: CodeQL
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ schedule:
+ - cron: "10 14 * * 2"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-22.04
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update -y
+ sudo apt-get install -y \
+ gcc \
+ acl \
+ libacl1-dev \
+ attr \
+ libcap2-bin \
+ libcap-dev \
+ libonig-dev \
+ liburing-dev
+
+ - name: Configure
+ run: |
+ make -j$(nproc) config
+
+ - 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/.gitignore b/.gitignore
index 4ded7c4..84e47fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/bin/
+/gen/
/obj/
+/distcheck-*/
diff --git a/LICENSE b/LICENSE
index 069b145..240d2ac 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,12 +1,11 @@
-Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com>
+Copyright © 2015-2023 Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted.
+Permission to use, copy, modify, and/or distribute this software for any purpose with or
+without fee is hereby granted.
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT
+SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
+USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
index d6a61b0..981c322 100644
--- a/Makefile
+++ b/Makefile
@@ -1,313 +1,236 @@
-############################################################################
-# bfs #
-# Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
-
-ifneq ($(wildcard .git),)
-VERSION := $(shell git describe --always 2>/dev/null)
-endif
-
-ifndef VERSION
-VERSION := 2.6.1
-endif
-
-ifndef OS
-OS := $(shell uname)
-endif
-
-ifndef ARCH
-ARCH := $(shell uname -m)
-endif
-
-CC ?= gcc
-INSTALL ?= install
-MKDIR ?= mkdir -p
-RM ?= rm -f
-
-export BUILDDIR ?= .
-DESTDIR ?=
-PREFIX ?= /usr
-MANDIR ?= $(PREFIX)/share/man
-
-BIN := $(BUILDDIR)/bin
-OBJ := $(BUILDDIR)/obj
-
-DEFAULT_CFLAGS := \
- -g \
- -Wall \
- -Wmissing-declarations \
- -Wshadow \
- -Wsign-compare \
- -Wstrict-prototypes \
- -Wimplicit-fallthrough
-
-CFLAGS ?= $(DEFAULT_CFLAGS)
-LDFLAGS ?=
-DEPFLAGS ?= -MD -MP -MF $(@:.o=.d)
-
-LOCAL_CPPFLAGS := \
- -D__EXTENSIONS__ \
- -D_ATFILE_SOURCE \
- -D_BSD_SOURCE \
- -D_DARWIN_C_SOURCE \
- -D_DEFAULT_SOURCE \
- -D_FILE_OFFSET_BITS=64 \
- -D_TIME_BITS=64 \
- -D_GNU_SOURCE \
- -DBFS_VERSION=\"$(VERSION)\"
-
-LOCAL_CFLAGS := -std=c11
-LOCAL_LDFLAGS :=
-LOCAL_LDLIBS :=
-
-ASAN := $(filter asan,$(MAKECMDGOALS))
-LSAN := $(filter lsan,$(MAKECMDGOALS))
-MSAN := $(filter msan,$(MAKECMDGOALS))
-TSAN := $(filter tsan,$(MAKECMDGOALS))
-UBSAN := $(filter ubsan,$(MAKECMDGOALS))
-
-ifndef MSAN
-WITH_ONIGURUMA := y
-endif
-
-ifdef WITH_ONIGURUMA
-LOCAL_CPPFLAGS += -DBFS_WITH_ONIGURUMA=1
-
-ONIG_CONFIG := $(shell command -v onig-config 2>/dev/null)
-ifdef ONIG_CONFIG
-ONIG_CFLAGS := $(shell $(ONIG_CONFIG) --cflags)
-ONIG_LDLIBS := $(shell $(ONIG_CONFIG) --libs)
-else
-ONIG_LDLIBS := -lonig
-endif
-
-LOCAL_CFLAGS += $(ONIG_CFLAGS)
-LOCAL_LDLIBS += $(ONIG_LDLIBS)
-endif
-
-ifeq ($(OS),Linux)
-ifndef MSAN # These libraries are not built with msan
-WITH_ACL := y
-WITH_ATTR := y
-WITH_LIBCAP := y
-endif
-
-ifdef WITH_ACL
-LOCAL_LDLIBS += -lacl
-else
-LOCAL_CPPFLAGS += -DBFS_HAS_SYS_ACL=0
-endif
-
-ifdef WITH_ATTR
-LOCAL_LDLIBS += -lattr
-else
-LOCAL_CPPFLAGS += -DBFS_HAS_SYS_XATTR=0
-endif
-
-ifdef WITH_LIBCAP
-LOCAL_LDLIBS += -lcap
-else
-LOCAL_CPPFLAGS += -DBFS_HAS_SYS_CAPABILITY=0
-endif
-
-LOCAL_LDFLAGS += -Wl,--as-needed
-LOCAL_LDLIBS += -lrt
-endif
-
-ifeq ($(OS),NetBSD)
-LOCAL_LDLIBS += -lutil
-endif
-
-ifdef ASAN
-LOCAL_CFLAGS += -fsanitize=address
-SANITIZE := y
-endif
-
-ifdef LSAN
-LOCAL_CFLAGS += -fsanitize=leak
-SANITIZE := y
-endif
-
-ifdef MSAN
-LOCAL_CFLAGS += -fsanitize=memory -fsanitize-memory-track-origins
-SANITIZE := y
-endif
-
-ifdef TSAN
-LOCAL_CFLAGS += -fsanitize=thread
-SANITIZE := y
-endif
-
-ifdef UBSAN
-LOCAL_CFLAGS += -fsanitize=undefined
-SANITIZE := y
-endif
-
-ifdef SANITIZE
-LOCAL_CFLAGS += -fno-sanitize-recover=all
-endif
-
-ifneq ($(filter gcov,$(MAKECMDGOALS)),)
-LOCAL_CFLAGS += --coverage
-# gcov only intercepts fork()/exec() with -std=gnu*
-LOCAL_CFLAGS := $(patsubst -std=c%,-std=gnu%,$(LOCAL_CFLAGS))
-endif
-
-ifneq ($(filter release,$(MAKECMDGOALS)),)
-CFLAGS := $(DEFAULT_CFLAGS) -O3 -flto -DNDEBUG
-endif
-
-ALL_CPPFLAGS = $(LOCAL_CPPFLAGS) $(CPPFLAGS) $(EXTRA_CPPFLAGS)
-ALL_CFLAGS = $(ALL_CPPFLAGS) $(LOCAL_CFLAGS) $(CFLAGS) $(EXTRA_CFLAGS) $(DEPFLAGS)
-ALL_LDFLAGS = $(ALL_CFLAGS) $(LOCAL_LDFLAGS) $(LDFLAGS) $(EXTRA_LDFLAGS)
-ALL_LDLIBS = $(LOCAL_LDLIBS) $(LDLIBS) $(EXTRA_LDLIBS)
-
-# Goals that are treated like flags by this Makefile
-FLAG_GOALS := asan lsan msan tsan ubsan gcov release
-
-# These are the remaining non-flag goals
-GOALS := $(filter-out $(FLAG_GOALS),$(MAKECMDGOALS))
-
-# Build the default goal if only flag goals are specified
-FLAG_PREREQS :=
-ifndef GOALS
-FLAG_PREREQS += bfs
-endif
-
-# The different search strategies that we test
-STRATEGIES := bfs dfs ids eds
-STRATEGY_CHECKS := $(STRATEGIES:%=check-%)
-
-# All the different checks we run
-CHECKS := $(STRATEGY_CHECKS) check-trie check-xtimegm
-
-# Custom test flags for distcheck
-DISTCHECK_FLAGS := -s TEST_FLAGS="--sudo --verbose=skipped"
-
-bfs: $(BIN)/bfs
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# This Makefile implements the configuration and build steps for bfs. It is
+# portable to both GNU make and most BSD make implementations. To build bfs,
+# run
+#
+# $ make config
+# $ make
+
+# Utilities and GNU/BSD portability
+include config/prelude.mk
+
+# The default build target
+default: bfs
+.PHONY: default
+
+# Include the generated build config, if it exists
+-include ${CONFIG}
+
+## Configuration phase (`make config`)
+
+# The configuration goal itself
+config::
+ @+${MAKE} -sf config/config.mk
+
+# 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. ' "${MAKE}" $@ >&2
+ @printf 'Use `%s config %s=y` instead.\n' "${MAKE}" $$(echo $@ | tr 'a-z' 'A-Z') >&2
+ @false
+
+# Print an error if `make` is run before `make config`
+${CONFIG}::
+ @if ! [ -e $@ ]; then \
+ printf 'error: You must run `%s config` before `%s`.\n' "${MAKE}" "${MAKE}" >&2; \
+ false; \
+ fi
+
+## Build phase (`make`)
+
+# The main binary
+bfs: ${BIN}/bfs
.PHONY: bfs
-all: $(BIN)/bfs $(BIN)/tests/mksock $(BIN)/tests/trie $(BIN)/tests/xtimegm
-.PHONY: all
+# All binaries
+BINS := \
+ ${BIN}/bfs \
+ ${BIN}/tests/mksock \
+ ${BIN}/tests/units \
+ ${BIN}/tests/xspawnee \
+ ${BIN}/tests/xtouch
-$(BIN)/bfs: \
- $(OBJ)/src/bar.o \
- $(OBJ)/src/bftw.o \
- $(OBJ)/src/color.o \
- $(OBJ)/src/ctx.o \
- $(OBJ)/src/darray.o \
- $(OBJ)/src/diag.o \
- $(OBJ)/src/dir.o \
- $(OBJ)/src/dstring.o \
- $(OBJ)/src/eval.o \
- $(OBJ)/src/exec.o \
- $(OBJ)/src/fsade.o \
- $(OBJ)/src/main.o \
- $(OBJ)/src/mtab.o \
- $(OBJ)/src/opt.o \
- $(OBJ)/src/parse.o \
- $(OBJ)/src/printf.o \
- $(OBJ)/src/pwcache.o \
- $(OBJ)/src/stat.o \
- $(OBJ)/src/trie.o \
- $(OBJ)/src/typo.o \
- $(OBJ)/src/util.o \
- $(OBJ)/src/xregex.o \
- $(OBJ)/src/xspawn.o \
- $(OBJ)/src/xtime.o
-
-$(BIN)/tests/mksock: $(OBJ)/tests/mksock.o
-$(BIN)/tests/trie: $(OBJ)/src/trie.o $(OBJ)/tests/trie.o
-$(BIN)/tests/xtimegm: $(OBJ)/src/xtime.o $(OBJ)/tests/xtimegm.o
-
-$(BIN)/%:
- @$(MKDIR) $(@D)
- +$(CC) $(ALL_LDFLAGS) $^ $(ALL_LDLIBS) -o $@
-
-$(OBJ)/%.o: %.c $(OBJ)/FLAGS
- @$(MKDIR) $(@D)
- $(CC) $(ALL_CFLAGS) -c $< -o $@
-
-# Save the full set of flags to rebuild everything when they change
-$(OBJ)/FLAGS.new:
- @$(MKDIR) $(@D)
- @echo $(CC) : $(ALL_CFLAGS) : $(ALL_LDFLAGS) : $(ALL_LDLIBS) >$@
-.PHONY: $(OBJ)/FLAGS.new
-
-# Only update obj/FLAGS if obj/FLAGS.new is different
-$(OBJ)/FLAGS: $(OBJ)/FLAGS.new
- @test -e $@ && cmp -s $@ $< && rm $< || mv $< $@
-
-# Make sure that "make release" builds everything, but "make release obj/src/main.o" doesn't
-$(FLAG_GOALS): $(FLAG_PREREQS)
- @:
-.PHONY: $(FLAG_GOALS)
-
-check: $(CHECKS)
-.PHONY: check $(CHECKS)
-
-$(STRATEGY_CHECKS): check-%: $(BIN)/bfs $(BIN)/tests/mksock
- ./tests/tests.sh --bfs="$(BIN)/bfs -S $*" $(TEST_FLAGS)
-
-check-trie check-xtimegm: check-%: $(BIN)/tests/%
- $<
+all: ${BINS}
+.PHONY: all
+# Group relevant flags together
+ALL_CFLAGS = ${CPPFLAGS} ${CFLAGS} ${DEPFLAGS}
+ALL_LDFLAGS = ${CFLAGS} ${LDFLAGS}
+
+# The main binary
+${BIN}/bfs: ${LIBBFS} ${OBJ}/src/main.o
+
+${BINS}:
+ @${MKDIR} ${@D}
+ +${MSG} "[ LD ] ${TGT}" ${CC} ${ALL_LDFLAGS} ${.ALLSRC} ${LDLIBS} -o $@
+ ${POSTLINK}
+
+# Get the .c file for a .o file
+_CSRC = ${@:${OBJ}/%.o=%.c}
+CSRC = ${_CSRC:gen/%=${GEN}/%}
+
+# Depend on ${CONFIG} to make sure `make config` runs first, and to rebuild when
+# the configuration changes
+${OBJS}: ${CONFIG}
+ @${MKDIR} ${@D}
+ ${MSG} "[ CC ] ${_CSRC}" ${CC} ${ALL_CFLAGS} -c ${CSRC} -o $@
+
+# Save the version number to this file, but only update version.c if it changes
+${GEN}/version.c.new::
+ @${MKDIR} ${@D}
+ @printf 'const char bfs_version[] = "' >$@
+ @if [ "$$VERSION" ]; then \
+ printf '%s' "$$VERSION"; \
+ elif test -d .git && command -v git >/dev/null 2>&1; then \
+ git describe --always --dirty; \
+ else \
+ echo "3.1.3"; \
+ fi | tr -d '\n' >>$@
+ @printf '";\n' >>$@
+
+${GEN}/version.c: ${GEN}/version.c.new
+ @test -e $@ && cmp -s $@ ${.ALLSRC} && rm ${.ALLSRC} || mv ${.ALLSRC} $@
+
+${OBJ}/gen/version.o: ${GEN}/version.c
+
+## Test phase (`make check`)
+
+# Unit test binaries
+UTEST_BINS := \
+ ${BIN}/tests/units \
+ ${BIN}/tests/xspawnee
+
+# Integration test binaries
+ITEST_BINS := \
+ ${BIN}/tests/mksock \
+ ${BIN}/tests/xtouch
+
+# Build (but don't run) test binaries
+tests: ${UTEST_BINS} ${ITEST_BINS}
+.PHONY: tests
+
+# Run all the tests
+check: unit-tests integration-tests
+.PHONY: check
+
+# Run the unit tests
+unit-tests: ${UTEST_BINS}
+ ${MSG} "[TEST] tests/units" ${BIN}/tests/units
+.PHONY: unit-tests
+
+${BIN}/tests/units: \
+ ${UNIT_OBJS} \
+ ${LIBBFS}
+
+${BIN}/tests/xspawnee: \
+ ${OBJ}/tests/xspawnee.o
+
+# The different flag combinations we check
+INTEGRATIONS := default dfs ids eds j1 j2 j3 s
+INTEGRATION_TESTS := ${INTEGRATIONS:%=check-%}
+
+# Check just `bfs`
+check-default: ${BIN}/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}
+
+${BIN}/tests/xtouch: \
+ ${OBJ}/tests/xtouch.o \
+ ${LIBBFS}
+
+# `make distcheck` configurations
+DISTCHECKS := \
+ distcheck-asan \
+ distcheck-msan \
+ distcheck-tsan \
+ distcheck-m32 \
+ distcheck-release
+
+# Test multiple configurations
distcheck:
- +$(MAKE) -B asan ubsan check $(DISTCHECK_FLAGS)
-ifneq ($(OS),Darwin)
- +$(MAKE) -B msan check CC=clang $(DISTCHECK_FLAGS)
-endif
-ifeq ($(OS) $(ARCH),Linux x86_64)
- +$(MAKE) -B check EXTRA_CFLAGS="-m32" ONIG_CONFIG= $(DISTCHECK_FLAGS)
-endif
- +$(MAKE) -B release check $(DISTCHECK_FLAGS)
- +$(MAKE) -B check $(DISTCHECK_FLAGS)
- +$(MAKE) check-install $(DISTCHECK_FLAGS)
+ @+${MAKE} -s distcheck-asan
+ @+test "$$(uname)" = Darwin || ${MAKE} -s distcheck-msan
+ @+${MAKE} -s distcheck-tsan
+ @+test "$$(uname)-$$(uname -m)" != Linux-x86_64 || ${MAKE} -s distcheck-m32
+ @+${MAKE} -s distcheck-release
.PHONY: distcheck
-clean:
- $(RM) -r $(BIN) $(OBJ)
-.PHONY: clean
-
-install:
- $(MKDIR) $(DESTDIR)$(PREFIX)/bin
- $(INSTALL) -m755 $(BIN)/bfs $(DESTDIR)$(PREFIX)/bin/bfs
- $(MKDIR) $(DESTDIR)$(MANDIR)/man1
- $(INSTALL) -m644 docs/bfs.1 $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/bash-completion/completions
- $(INSTALL) -m644 completions/bfs.bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/zsh/site-functions
- $(INSTALL) -m644 completions/bfs.zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_bfs
- $(MKDIR) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d
- $(INSTALL) -m644 completions/bfs.fish $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/bfs.fish
-.PHONY: install
-
-uninstall:
- $(RM) $(DESTDIR)$(PREFIX)/share/bash-completion/completions/bfs
- $(RM) $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_bfs
- $(RM) $(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/bfs.fish
- $(RM) $(DESTDIR)$(MANDIR)/man1/bfs.1
- $(RM) $(DESTDIR)$(PREFIX)/bin/bfs
-.PHONY: uninstall
-
-check-install:
- +$(MAKE) install DESTDIR=$(BUILDDIR)/pkg
- +$(MAKE) uninstall DESTDIR=$(BUILDDIR)/pkg
- $(BIN)/bfs $(BUILDDIR)/pkg -not -type d -print -exit 1
- $(RM) -r $(BUILDDIR)/pkg
-.PHONY: check-install
-
-.SUFFIXES:
-
--include $(wildcard $(OBJ)/*/*.d)
+# Per-distcheck configuration
+DISTCHECK_CONFIG_asan := ASAN=y UBSAN=y
+DISTCHECK_CONFIG_msan := MSAN=y UBSAN=y CC=clang
+DISTCHECK_CONFIG_tsan := TSAN=y UBSAN=y CC=clang
+DISTCHECK_CONFIG_m32 := EXTRA_CFLAGS="-m32" PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig
+DISTCHECK_CONFIG_release := RELEASE=y
+
+${DISTCHECKS}::
+ +${MAKE} -rs BUILDDIR=${BUILDDIR}/$@ config ${DISTCHECK_CONFIG_${@:distcheck-%=%}}
+ +${MAKE} -s BUILDDIR=${BUILDDIR}/$@ check TEST_FLAGS="--sudo --verbose=skipped"
+
+## Packaging (`make install`)
+
+DEST_PREFIX := ${DESTDIR}${PREFIX}
+DEST_MANDIR := ${DESTDIR}${MANDIR}
+
+install::
+ ${Q}${MKDIR} ${DEST_PREFIX}/bin
+ ${MSG} "[INSTALL] bin/bfs" \
+ ${INSTALL} -m755 ${BIN}/bfs ${DEST_PREFIX}/bin/bfs
+ ${Q}${MKDIR} ${DEST_MANDIR}/man1
+ ${MSG} "[INSTALL] man/man1/bfs.1" \
+ ${INSTALL} -m644 docs/bfs.1 ${DEST_MANDIR}/man1/bfs.1
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/bash-completion/completions
+ ${MSG} "[INSTALL] completions/bfs.bash" \
+ ${INSTALL} -m644 completions/bfs.bash ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${Q}${MKDIR} ${DEST_PREFIX}/share/zsh/site-functions
+ ${MSG} "[INSTALL] 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} "[INSTALL] completions/bfs.fish" \
+ ${INSTALL} -m644 completions/bfs.fish ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+
+uninstall::
+ ${RM} ${DEST_PREFIX}/share/bash-completion/completions/bfs
+ ${RM} ${DEST_PREFIX}/share/zsh/site-functions/_bfs
+ ${RM} ${DEST_PREFIX}/share/fish/vendor_completions.d/bfs.fish
+ ${RM} ${DEST_MANDIR}/man1/bfs.1
+ ${RM} ${DEST_PREFIX}/bin/bfs
+
+# Check that `make install` works and `make uninstall` removes everything
+check-install::
+ +${MAKE} install DESTDIR=${BUILDDIR}/pkg
+ +${MAKE} uninstall DESTDIR=${BUILDDIR}/pkg
+ ${BIN}/bfs ${BUILDDIR}/pkg -not -type d -print -exit 1
+ ${RM} -r ${BUILDDIR}/pkg
+
+## Cleanup (`make clean`)
+
+# Clean all build products
+clean::
+ ${MSG} "[ RM ] bin obj" \
+ ${RM} -r ${BIN} ${OBJ}
+
+# Clean everything, including generated files
+distclean: clean
+ ${MSG} "[ RM ] gen" \
+ ${RM} -r ${GEN} ${DISTCHECKS}
+.PHONY: distclean
diff --git a/README.md b/README.md
index 7631f12..b95c16b 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,30 @@
<div align="center">
-`bfs`
-=====
-
+<h1>
+<code>bfs</code>
+<br clear="all">
<a href="https://github.com/tavianator/bfs/releases"><img src="https://img.shields.io/github/v/tag/tavianator/bfs?label=version" alt="Version" align="left"></a>
<a href="/LICENSE"><img src="https://img.shields.io/badge/license-0BSD-blue.svg" alt="License" align="left"></a>
-<a href="https://github.com/tavianator/bfs/actions/workflows/ci.yml"><img src="https://img.shields.io/github/workflow/status/tavianator/bfs/CI?label=CI" alt="CI Status" align="right"></a>
+<a href="https://github.com/tavianator/bfs/actions/workflows/ci.yml"><img src="https://github.com/tavianator/bfs/actions/workflows/ci.yml/badge.svg" alt="CI Status" align="right"></a>
<a href="https://codecov.io/gh/tavianator/bfs"><img src="https://img.shields.io/codecov/c/github/tavianator/bfs?token=PpBVuozOVC" alt="Code coverage" align="right"/></a>
-
-***Breadth-first search for your files.***
-
-[ **[Features](#features)** ]&emsp;
-[ **[Installation](#installation)** ]&emsp;
-[ **[Usage](/docs/USAGE.md)** ]&emsp;
-[ **[Building](/docs/BUILDING.md)** ]&emsp;
-[ **[Hacking](/docs/HACKING.md)** ]&emsp;
-[ **[Changelog](/docs/CHANGELOG.md)** ]
-
-<img src="https://tavianator.github.io/bfs/animation.svg" alt="Screenshot">
+</h1>
+
+**[Features]   •   [Installation]   •   [Usage]   •   [Building]   •   [Contributing]   •   [Changelog]**
+
+[Features]: #features
+[Installation]: #installation
+[Usage]: /docs/USAGE.md
+[Building]: /docs/BUILDING.md
+[Contributing]: /docs/CONTRIBUTING.md
+[Changelog]: /docs/CHANGELOG.md
+
+<picture>
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-dark.svg">
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg">
+ <img alt="Screencast" src="https://raw.githubusercontent.com/tavianator/bfs/gh-pages/animation-light.svg">
+</picture>
<p></p>
+
</div>
`bfs` is a variant of the UNIX `find` command that operates [**breadth-first**](https://en.wikipedia.org/wiki/Breadth-first_search) rather than [**depth-first**](https://en.wikipedia.org/wiki/Depth-first_search).
@@ -26,12 +32,14 @@ It is otherwise compatible with many versions of `find`, including
<div align="center">
-[ **[POSIX](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html)** ]&emsp;
-[ **[GNU](https://www.gnu.org/software/findutils/)** ]&emsp;
-[ **[FreeBSD](https://www.freebsd.org/cgi/man.cgi?find(1))** ]&emsp;
-[ **[OpenBSD](https://man.openbsd.org/find.1)** ]&emsp;
-[ **[NetBSD](https://man.netbsd.org/find.1)** ]&emsp;
-[ **[macOS](https://ss64.com/osx/find.html)** ]
+**[POSIX]   •   [GNU]   •   [FreeBSD]   •   [OpenBSD]   •   [NetBSD]   •   [macOS]**
+
+[POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/find.html
+[GNU]: https://www.gnu.org/software/findutils/
+[FreeBSD]: https://www.freebsd.org/cgi/man.cgi?find(1)
+[OpenBSD]: https://man.openbsd.org/find.1
+[NetBSD]: https://man.netbsd.org/find.1
+[macOS]: https://ss64.com/osx/find.html
</div>
@@ -44,8 +52,8 @@ Features
<details>
<summary>
<code>bfs</code> operates breadth-first, which typically finds the file(s) you're looking for faster.
-<p></p>
</summary>
+<p></p>
Imagine the following directory tree:
@@ -62,47 +70,57 @@ haystack
</pre>
`find` will explore the entire `deep` directory tree before it ever gets to the `shallow` one that contains what you're looking for.
+On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree.
-<pre>
-$ <strong>find</strong> haystack
+<table>
+<tbody>
+<tr><th><code>bfs</code></th><th><code>find</code></th></tr>
+<tr>
+<td width="506" valign="top">
+
+```console
+$ bfs haystack
haystack
haystack/deep
+haystack/shallow
haystack/deep/1
-haystack/deep/1/2
-haystack/deep/1/2/3
-haystack/deep/1/2/3/4
+haystack/shallow/needle
...
-haystack/shallow
-<strong>haystack/shallow/needle</strong>
-</pre>
+```
-On the other hand, `bfs` lists files from shallowest to deepest, so you never have to wait for it to explore an entire unrelated subtree.
+</td>
+<td width="506" valign="top">
-<pre>
-$ <strong>bfs</strong> haystack
+```console
+$ find haystack
haystack
haystack/deep
-haystack/shallow
haystack/deep/1
-<strong>haystack/shallow/needle</strong>
haystack/deep/1/2
haystack/deep/1/2/3
haystack/deep/1/2/3/4
...
-</pre>
+haystack/shallow
+haystack/shallow/needle
+```
+
+</td>
+</tr>
+</tbody>
+</table>
</details>
<details>
<summary>
<code>bfs</code> tries to be easier to use than <code>find</code>, while remaining compatible.
-<p></p>
</summary>
+<p></p>
For example, `bfs` is less picky about where you put its arguments:
<table>
<tbody>
-<tr></tr>
+<tr><th><code>bfs</code></th><th><code>find</code></th></tr>
<tr>
<td width="506">
@@ -140,33 +158,34 @@ haystack/needle
<details>
<summary>
<code>bfs</code> gives helpful errors and warnings.
-<p></p>
</summary>
+<p></p>
For example, `bfs` will detect and suggest corrections for typos:
-<pre>
+```console
$ bfs -nam needle
-<strong>bfs: error:</strong> bfs <strong>-nam</strong> needle
-<strong>bfs: error:</strong> <strong>~~~~</strong>
-<strong>bfs: error:</strong> Unknown argument; did you mean <strong>-name</strong>?
-</pre>
+bfs: error: bfs -nam needle
+bfs: error: ~~~~
+bfs: error: Unknown argument; did you mean -name?
+```
`bfs` also includes a powerful static analysis to help catch mistakes:
-<pre>
+```console
$ bfs -print -name 'needle'
-<strong>bfs: warning:</strong> bfs -print <strong>-name needle</strong>
-<strong>bfs: warning:</strong> <strong>~~~~~~~~~~~~</strong>
-<strong>bfs: warning:</strong> The result of this expression is ignored.
-</pre>
+bfs: warning: bfs -print -name needle
+bfs: warning: ~~~~~~~~~~~~
+bfs: warning: The result of this expression is ignored.
+```
+
</details>
<details>
<summary>
<code>bfs</code> adds some options that make common tasks easier.
-<p></p>
</summary>
+<p></p>
For example, the `-exclude` operator skips over entire subtrees whenever an expression matches.
`-exclude` is both more powerful and easier to use than the standard `-prune` action; compare
@@ -192,45 +211,78 @@ Installation
<details open>
<summary>
<code>bfs</code> may already be packaged for your operating system.
-<p></p>
</summary>
+<p></p>
+
+<table>
+<tbody>
+<tr><th>Linux</th><th>macOS</th></tr>
+
+<tr>
+<td width="506" valign="top" rowspan="3">
<pre>
<strong><a href="https://pkgs.alpinelinux.org/packages?name=bfs">Alpine Linux</a></strong>
# apk add bfs
-<strong><a href="https://aur.archlinux.org/packages/bfs">Arch Linux</a></strong>
-(Available in the AUR)
+<strong><a href="https://archlinux.org/packages/extra/x86_64/bfs/">Arch Linux</a></strong>
+# pacman -S bfs
<strong><a href="https://packages.debian.org/sid/bfs">Debian</a>/<a href="https://packages.ubuntu.com/kinetic/bfs">Ubuntu</a></strong>
# apt install bfs
-<strong><a href="https://copr.fedorainfracloud.org/coprs/xfgusta/bfs/">Fedora</a></strong>
-# dnf copr enable xfgusta/bfs
+<strong><a href="https://src.fedoraproject.org/rpms/bfs">Fedora Linux</a></strong>
# dnf install bfs
+<strong><a href="https://packages.gentoo.org/packages/sys-apps/bfs">Gentoo</a></strong>
+# emerge sys-apps/bfs
+
+<strong><a href="https://packages.guix.gnu.org/packages/bfs/">GNU Guix</a></strong>
+# guix install bfs
+
<strong><a href="https://search.nixos.org/packages?channel=unstable&show=bfs&from=0&size=1&sort=relevance&type=packages&query=bfs">NixOS</a></strong>
# nix-env -i bfs
<strong><a href="https://voidlinux.org/packages/?arch=x86_64&q=bfs">Void Linux</a></strong>
# xbps-install -S bfs
+</pre>
-<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong>
-# pkg install bfs
+</td>
+<td width="506" valign="top">
+
+<pre>
+<strong><a href="https://formulae.brew.sh/formula/bfs">Homebrew</a></strong>
+$ brew install bfs
<strong><a href="https://ports.macports.org/port/bfs/">MacPorts</a></strong>
# port install bfs
+</pre>
+
+</td>
+</tr>
+<tr><th height="1">BSD</th></tr>
+<tr>
+<td width="506" valign="top">
+
+<pre>
+<strong><a href="https://www.freshports.org/sysutils/bfs">FreeBSD</a></strong>
+# pkg install bfs
-<strong><a href="https://github.com/tavianator/homebrew-tap/blob/master/Formula/bfs.rb">Homebrew</a></strong>
-$ brew install tavianator/tap/bfs
+<strong><a href="https://openports.pl/path/sysutils/bfs">OpenBSD</a></strong>
+# pkg_add bfs
</pre>
+
+</td>
+</tr>
+</tbody>
+</table>
</details>
<details>
<summary>
To build <code>bfs</code> from source, you may need to install some dependencies.
-<p></p>
</summary>
+<p></p>
The only absolute requirements for building `bfs` are a C compiler, [GNU make](https://www.gnu.org/software/make/), and [Bash](https://www.gnu.org/software/bash/).
These are installed by default on many systems, and easy to install on most others.
@@ -241,31 +293,31 @@ Here's how to install them on some common platforms:
<pre>
<strong>Alpine Linux</strong>
-# apk add acl{,-dev} attr{,-dev} libcap{,-dev} oniguruma-dev
+# apk add acl{,-dev} attr libcap{,-dev} liburing-dev oniguruma-dev
<strong>Arch Linux</strong>
-# pacman -S acl attr libcap oniguruma
+# pacman -S acl attr libcap liburing oniguruma
<strong>Debian/Ubuntu</strong>
-# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev libonig-dev
+# apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev liburing-dev libonig-dev
<strong>Fedora</strong>
-# dnf install libacl-devel libattr-devel libcap-devel oniguruma-devel
+# dnf install acl libacl-devel attr libcap-devel liburing-devel oniguruma-devel
<strong>NixOS</strong>
-# nix-env -i acl attr libcap oniguruma
+# nix-env -i acl attr libcap liburing oniguruma
<strong>Void Linux</strong>
-# xbps-install -S acl-{devel,progs} attr-{devel,progs} libcap-{devel,progs} oniguruma-devel
+# xbps-install -S acl-{devel,progs} attr-progs libcap-{devel,progs} liburing-devel oniguruma-devel
-<strong>FreeBSD</strong>
-# pkg install oniguruma
+<strong>Homebrew</strong>
+$ brew install oniguruma
<strong>MacPorts</strong>
# port install oniguruma6
-<strong>Homebrew</strong>
-$ brew install oniguruma
+<strong>FreeBSD</strong>
+# pkg install oniguruma
</pre>
These dependencies are technically optional, though strongly recommended.
@@ -275,12 +327,13 @@ See the [build documentation](/docs/BUILDING.md#dependencies) for how to disable
<details>
<summary>
Once you have the dependencies, you can build <code>bfs</code>.
-<p></p>
</summary>
+<p></p>
Download one of the [releases](https://github.com/tavianator/bfs/releases) or clone the [git repo](https://github.com/tavianator/bfs).
Then run
+ $ make config
$ make
This will build the `./bin/bfs` binary.
@@ -290,7 +343,8 @@ Run the test suite to make sure it works correctly:
If you're interested in speed, you may want to build the release version instead:
- $ make release
+ $ make config RELEASE=y
+ $ make
Finally, if you want to install it globally, run
diff --git a/bench/.gitignore b/bench/.gitignore
new file mode 100644
index 0000000..170d850
--- /dev/null
+++ b/bench/.gitignore
@@ -0,0 +1,3 @@
+/corpus/
+/results/
+/worktree/
diff --git a/bench/README.md b/bench/README.md
new file mode 100644
index 0000000..56157a0
--- /dev/null
+++ b/bench/README.md
@@ -0,0 +1,51 @@
+This directory contains a suite of benchmarks used to evaluate `bfs` and detect performance regressions.
+To run them, you'll need the [tailfin] benchmark harness.
+You can read the full usage information with
+
+[tailfin]: https://github.com/tavianator/tailfin
+
+```console
+$ tailfin -n run bench/bench.sh --help
+Usage: tailfin run bench/bench.sh [--default]
+ [--complete] [--early-quit] [--print] [--strategies]
+ [--build=...] [--bfs] [--find] [--fd]
+ [--no-clean] [--help]
+...
+```
+
+The benchmarks use various git repositories to have a realistic and reproducible directory structure as a corpus.
+Currently, those are the [Linux], [Rust], and [Chromium] repos.
+The scripts will automatically clone those repos using [partial clone] filters to avoid downloading the actual file contents, saving bandwidth and space.
+
+[Linux]: https://github.com/torvalds/linux.git
+[Rust]: https://github.com/rust-lang/rust.git
+[Chromium]: https://chromium.googlesource.com/chromium/src.git
+[partial clone]: https://git-scm.com/docs/partial-clone
+
+You can try out a quick benchmark by running
+
+```console
+$ tailfin run bench/bench.sh --build=main --complete=linux
+```
+
+This will build the `main` branch, and measure the complete traversal of the Linux repo.
+Results will be both printed to the console and saved in a Markdown file, which you can find by running
+
+```console
+$ tailfin latest
+results/2023/09/29/15:32:49
+$ cat results/2023/09/29/15:32:49/runs/1/bench.md
+## Complete traversal
+...
+```
+
+To measure performance improvements/regressions of a change, compare the `main` branch to the topic branch on the full benchmark suite:
+
+```console
+$ tailfin run bench/bench.sh --build=main --build=branch --default
+```
+
+This will take a few minutes.
+Results from the full benchmark suite can be seen in performance-related pull requests, for example [#126].
+
+[#126]: https://github.com/tavianator/bfs/pull/126
diff --git a/bench/bench.sh b/bench/bench.sh
new file mode 100644
index 0000000..1526fe5
--- /dev/null
+++ b/bench/bench.sh
@@ -0,0 +1,715 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+declare -gA URLS=(
+ [chromium]="https://chromium.googlesource.com/chromium/src.git"
+ [linux]="https://github.com/torvalds/linux.git"
+ [rust]="https://github.com/rust-lang/rust.git"
+)
+
+declare -gA TAGS=(
+ [chromium]=119.0.6036.2
+ [linux]=v6.5
+ [rust]=1.72.1
+)
+
+COMPLETE_DEFAULT=(linux rust chromium)
+EARLY_QUIT_DEFAULT=(chromium)
+STAT_DEFAULT=(rust)
+PRINT_DEFAULT=(linux)
+STRATEGIES_DEFAULT=(rust)
+JOBS_DEFAULT=(rust)
+EXEC_DEFAULT=(linux)
+
+usage() {
+ printf 'Usage: tailfin run %s\n' "${BASH_SOURCE[0]}"
+ printf ' [--default] [--<BENCHMARK> [--<BENCHMARK>...]]\n'
+ printf ' [--build=...] [--bfs] [--find] [--fd]\n'
+ printf ' [--no-clean] [--help]\n\n'
+
+ printf ' --default\n'
+ printf ' Run the default set of benchmarks\n\n'
+
+ printf ' --complete[=CORPUS]\n'
+ printf ' Complete traversal benchmark.\n'
+ printf ' Default corpus is --complete="%s"\n\n' "${COMPLETE_DEFAULT[*]}"
+
+ printf ' --early-quit[=CORPUS]\n'
+ printf ' Early quitting benchmark.\n'
+ printf ' Default corpus is --early-quit=%s\n\n' "${EARLY_QUIT_DEFAULT[*]}"
+
+ printf ' --stat[=CORPUS]\n'
+ printf ' Traversal with stat().\n'
+ printf ' Default corpus is --stat=%s\n\n' "${STAT_DEFAULT[*]}"
+
+ printf ' --print[=CORPUS]\n'
+ printf ' Path printing benchmark.\n'
+ printf ' Default corpus is --print=%s\n\n' "${PRINT_DEFAULT[*]}"
+
+ printf ' --strategies[=CORPUS]\n'
+ printf ' Search strategy benchmark.\n'
+ printf ' Default corpus is --strategies=%s\n\n' "${STRATEGIES_DEFAULT[*]}"
+
+ printf ' --jobs[=CORPUS]\n'
+ printf ' Parallelism benchmark.\n'
+ printf ' Default corpus is --jobs=%s\n\n' "${JOBS_DEFAULT[*]}"
+
+ printf ' --exec[=CORPUS]\n'
+ printf ' Process spawning benchmark.\n'
+ printf ' Default corpus is --exec=%s\n\n' "${EXEC_DEFAULT[*]}"
+
+ printf ' --build=COMMIT\n'
+ printf ' Build this bfs commit and benchmark it. Specify multiple times to\n'
+ printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n'
+
+ printf ' --bfs[=COMMAND]\n'
+ printf ' Benchmark an existing build of bfs\n\n'
+
+ printf ' --find[=COMMAND]\n'
+ printf ' Compare against find\n\n'
+
+ printf ' --fd[=COMMAND]\n'
+ printf ' Compare against fd\n\n'
+
+ printf ' --no-clean\n'
+ printf ' Use any existing corpora as-is\n\n'
+
+ printf ' --help\n'
+ printf ' This message\n\n'
+}
+
+# Hack to export an array
+export_array() {
+ local str=$(declare -p "$1" | sed 's/ -a / -ga /')
+ unset "$1"
+ export "$1=$str"
+}
+
+# Hack to import an array
+import_array() {
+ local cmd="${!1}"
+ unset "$1"
+ eval "$cmd"
+}
+
+# Set up the benchmarks
+setup() {
+ ROOT=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")/..")
+ if ! [ "$PWD" -ef "$ROOT" ]; then
+ printf 'error: Please run this script from %s\n\n' "$ROOT" >&2
+ usage >&2
+ exit $EX_USAGE
+ fi
+
+ nproc=$(nproc)
+
+ # Options
+
+ CLEAN=1
+
+ BUILD=()
+ BFS=()
+ FIND=()
+ FD=()
+
+ COMPLETE=()
+ EARLY_QUIT=()
+ STAT=()
+ PRINT=()
+ STRATEGIES=()
+ JOBS=()
+ EXEC=()
+
+ for arg; do
+ case "$arg" in
+ # Flags
+ --no-clean)
+ CLEAN=0
+ ;;
+ # bfs commits/tags to benchmark
+ --build=*)
+ BUILD+=("${arg#*=}")
+ BFS+=("bfs-${arg#*=}")
+ ;;
+ # Utilities to benchmark against
+ --bfs)
+ BFS+=(bfs)
+ ;;
+ --bfs=*)
+ BFS+=("${arg#*=}")
+ ;;
+ --find)
+ FIND+=(find)
+ ;;
+ --find=*)
+ FIND+=("${arg#*=}")
+ ;;
+ --fd)
+ FD+=(fd)
+ ;;
+ --fd=*)
+ FD+=("${arg#*=}")
+ ;;
+ # Benchmark groups
+ --complete)
+ COMPLETE=("${COMPLETE_DEFAULT[@]}")
+ ;;
+ --complete=*)
+ read -ra COMPLETE <<<"${arg#*=}"
+ ;;
+ --early-quit)
+ EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
+ ;;
+ --early-quit=*)
+ read -ra EARLY_QUIT <<<"${arg#*=}"
+ ;;
+ --stat)
+ STAT=("${STAT_DEFAULT[@]}")
+ ;;
+ --stat=*)
+ read -ra STAT <<<"${arg#*=}"
+ ;;
+ --print)
+ PRINT=("${PRINT_DEFAULT[@]}")
+ ;;
+ --print=*)
+ read -ra PRINT <<<"${arg#*=}"
+ ;;
+ --strategies)
+ STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
+ ;;
+ --strategies=*)
+ read -ra STRATEGIES <<<"${arg#*=}"
+ ;;
+ --jobs)
+ JOBS=("${JOBS_DEFAULT[@]}")
+ ;;
+ --jobs=*)
+ read -ra JOBS <<<"${arg#*=}"
+ ;;
+ --exec)
+ EXEC=("${EXEC_DEFAULT[@]}")
+ ;;
+ --exec=*)
+ read -ra EXEC <<<"${arg#*=}"
+ ;;
+ --default)
+ COMPLETE=("${COMPLETE_DEFAULT[@]}")
+ EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
+ STAT=("${STAT_DEFAULT[@]}")
+ PRINT=("${PRINT_DEFAULT[@]}")
+ STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
+ JOBS=("${JOBS_DEFAULT[@]}")
+ EXEC=("${EXEC_DEFAULT[@]}")
+ ;;
+ --help)
+ usage
+ exit
+ ;;
+ *)
+ printf 'error: Unknown option %q\n\n' "$arg" >&2
+ usage >&2
+ exit $EX_USAGE
+ ;;
+ esac
+ done
+
+ if ((UID == 0)); then
+ max-freq
+ fi
+
+ echo "Building bfs ..."
+ as-user make -s -j"$nproc" RELEASE=y
+ as-user make -s -j"$nproc" all
+
+ as-user mkdir -p bench/corpus
+
+ declare -A cloned=()
+ for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}"; do
+ if ((cloned["$corpus"])); then
+ continue
+ fi
+ cloned["$corpus"]=1
+
+ dir="bench/corpus/$corpus"
+ if ((CLEAN)) || ! [ -e "$dir" ]; then
+ as-user ./bench/clone-tree.sh "${URLS[$corpus]}" "${TAGS[$corpus]}" "$dir"{,.git}
+ fi
+ done
+
+ if ((${#BUILD[@]} > 0)); then
+ echo "Creating bfs worktree ..."
+
+ worktree="bench/worktree"
+ as-user git worktree add -qd "$worktree"
+ defer as-user git worktree remove "$worktree"
+
+ bin="$(realpath -- "$SETUP_DIR")/bin"
+ as-user mkdir "$bin"
+
+ for commit in "${BUILD[@]}"; do
+ (
+ echo "Building bfs $commit ..."
+ cd "$worktree"
+ as-user git checkout -qd "$commit" --
+ if [ -e config ]; then
+ as-user make -s -j"$nproc" config RELEASE=1
+ as-user make -s -j"$nproc"
+ else
+ as-user make -s -j"$nproc" release
+ fi
+ if [ -e ./bin/bfs ]; then
+ as-user cp ./bin/bfs "$bin/bfs-$commit"
+ else
+ as-user cp ./bfs "$bin/bfs-$commit"
+ fi
+ as-user make -s clean
+ )
+ done
+
+ # $SETUP_DIR contains `:` so it won't work in $PATH
+ # Work around this with a symlink
+ tmp=$(as-user mktemp)
+ as-user ln -sf "$bin" "$tmp"
+ defer rm "$tmp"
+ export PATH="$tmp:$PATH"
+ fi
+
+ export_array BFS
+ export_array FIND
+ export_array FD
+
+ export_array COMPLETE
+ export_array EARLY_QUIT
+ export_array STAT
+ export_array PRINT
+ export_array STRATEGIES
+ export_array JOBS
+ export_array EXEC
+
+ if ((UID == 0)); then
+ turbo-off
+ fi
+
+ sync
+}
+
+# Runs hyperfine and saves the output
+do-hyperfine() {
+ local tmp_md="$BENCH_DIR/.bench.md"
+ local md="$BENCH_DIR/bench.md"
+ local tmp_json="$BENCH_DIR/.bench.json"
+ local json="$BENCH_DIR/bench.json"
+
+ if (($# == 0)); then
+ printf 'Nothing to do\n\n' | tee -a "$md"
+ return 1
+ fi
+
+ hyperfine -w2 -M20 --export-markdown="$tmp_md" --export-json="$tmp_json" "$@" &>/dev/tty
+ cat "$tmp_md" >>"$md"
+ cat "$tmp_json" >>"$json"
+ rm "$tmp_md" "$tmp_json"
+
+ printf '\n' | tee -a "$md"
+}
+
+# Print the header for a benchmark group
+group() {
+ printf "## $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Print the header for a benchmark subgroup
+subgroup() {
+ printf "### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Print the header for a benchmark sub-subgroup
+subsubgroup() {
+ printf "#### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
+}
+
+# Benchmark the complete traversal of a directory tree
+# (without printing anything)
+bench-complete-corpus() {
+ total=$(./bin/bfs "$2" -printf '.' | wc -c)
+
+ subgroup "%s (%'d files)" "$1" "$total"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -false")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2 -false")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u '^$' $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All complete traversal benchmarks
+bench-complete() {
+ if (($#)); then
+ group "Complete traversal"
+
+ for corpus; do
+ bench-complete-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark quiting as soon as a file is seen
+bench-early-quit-corpus() {
+ dir="$2"
+ max_depth=$(./bin/bfs "$dir" -printf '%d\n' | sort -rn | head -n1)
+
+ subgroup '%s (depth %d)' "$1" "$max_depth"
+
+ # Save the list of unique filenames, along with their depth
+ UNIQ="$BENCH_DIR/uniq"
+ ./bin/bfs "$dir" -printf '%d %f\n' | sort -k2 | uniq -uf1 >"$UNIQ"
+
+ for ((i = 2; i <= max_depth; i *= 2)); do
+ subsubgroup 'Depth %d' "$i"
+
+ # Sample random uniquely-named files at depth $i
+ export FILES="$BENCH_DIR/uniq-$i"
+ sed -n "s/^$i //p" "$UNIQ" | shuf -n20 >"$FILES"
+ if ! [ -s "$FILES" ]; then
+ continue
+ fi
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $dir -name \$(shuf -n1 \$FILES) -print -quit")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $dir -name \$(shuf -n1 \$FILES) -print -quit")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -usg1 \$(shuf -n1 \$FILES) $dir")
+ done
+
+ do-hyperfine "${cmds[@]}"
+ done
+}
+
+# All early-quitting benchmarks
+bench-early-quit() {
+ if (($#)); then
+ group "Early termination"
+
+ for corpus; do
+ bench-early-quit-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark traversal with stat()
+bench-stat-corpus() {
+ total=$(./bin/bfs "$2" -printf '.' | wc -c)
+
+ subgroup "%s (%'d files)" "$1" "$total"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -size 1024G")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2 -size 1024G")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2 --size 1024Gi")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# stat() benchmarks
+bench-stat() {
+ if (($#)); then
+ group "Traversal with stat()"
+
+ for corpus; do
+ bench-stat-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark printing paths without colors
+bench-print-nocolor() {
+ subsubgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2")
+ done
+
+ for find in "${FIND[@]}"; do
+ cmds+=("$find $2")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Benchmark printing paths with colors
+bench-print-color() {
+ subsubgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs $2 -color")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $2 --color=always")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All printing benchmarks
+bench-print() {
+ if (($#)); then
+ group "Printing paths"
+
+ subgroup "Without colors"
+ for corpus; do
+ bench-print-nocolor "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+
+ subgroup "With colors"
+ for corpus; do
+ bench-print-color "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark search strategies
+bench-strategies-corpus() {
+ subgroup '%s' "$1"
+
+ if ((${#BFS[@]} == 1)); then
+ cmds=("$BFS -S "{bfs,dfs,ids,eds}" $2 -false")
+ do-hyperfine "${cmds[@]}"
+ else
+ for S in bfs dfs ids eds; do
+ subsubgroup '`-S %s`' "$S"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs -S $S $2 -false")
+ done
+ do-hyperfine "${cmds[@]}"
+ done
+ fi
+}
+
+# All search strategy benchmarks
+bench-strategies() {
+ if (($#)); then
+ group "Search strategies"
+
+ for corpus; do
+ bench-strategies-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Benchmark parallelism
+bench-jobs-corpus() {
+ subgroup '%s' "$1"
+
+ if ((${#BFS[@]} + ${#FD[@]} == 1)); then
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ if "$bfs" -j1 -quit &>/dev/null; then
+ cmds+=("$bfs -j"{1,2,3,4,6,8,12,16}" $2 -false")
+ else
+ cmds+=("$bfs $2 -false")
+ fi
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -j"{1,2,3,4,6,8,12,16}" -u '^$' $2")
+ done
+
+ do-hyperfine "${cmds[@]}"
+ else
+ for j in 1 2 3 4 6 8 12 16; do
+ subsubgroup '`-j%d`' $j
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ if "$bfs" -j1 -quit &>/dev/null; then
+ cmds+=("$bfs -j$j $2 -false")
+ elif ((j == 1)); then
+ cmds+=("$bfs $2 -false")
+ fi
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -j$j -u '^$' $2")
+ done
+
+ if ((${#cmds[@]})); then
+ do-hyperfine "${cmds[@]}"
+ fi
+ done
+ fi
+}
+
+# All parallelism benchmarks
+bench-jobs() {
+ if (($#)); then
+ group "Parallelism"
+
+ for corpus; do
+ bench-jobs-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# One file/process
+bench-exec-single() {
+ subsubgroup "One file per process"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -maxdepth 2 -exec true -- {} \;")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $1 --max-depth=2 -x true --")
+ # Without -j1, fd runs multiple processes in parallel, which is unfair
+ cmds+=("$fd -j1 -u --search-path $1 --max-depth=2 -x true --")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Many files/process
+bench-exec-multi() {
+ subsubgroup "Many files per process"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -exec true -- {} +")
+ done
+
+ for fd in "${FD[@]}"; do
+ cmds+=("$fd -u --search-path $1 -X true --")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Many files, same dir
+bench-exec-chdir() {
+ if ((${#BFS[@]} + ${#FIND[@]} == 0)); then
+ return
+ fi
+
+ subsubgroup "Spawn in parent directory"
+
+ cmds=()
+ for cmd in "${BFS[@]}" "${FIND[@]}"; do
+ cmds+=("$cmd $1 -maxdepth 3 -execdir true -- {} +")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# Benchmark process spawning
+bench-exec-corpus() {
+ subgroup '%s' "$1"
+
+ bench-exec-single "$2"
+ bench-exec-multi "$2"
+ bench-exec-chdir "$2"
+}
+
+# All process spawning benchmarks
+bench-exec() {
+ if (($#)); then
+ group "Process spawning"
+
+ for corpus; do
+ bench-exec-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
+# Print benchmarked versions
+bench-versions() {
+ subgroup "Versions"
+
+ local md="$BENCH_DIR/bench.md"
+
+ printf '```console\n' >>"$md"
+
+ {
+ for bfs in "${BFS[@]}"; do
+ printf '$ %s --version | head -n1\n' "$bfs"
+ "$bfs" --version | head -n1
+ done
+
+ for find in "${FIND[@]}"; do
+ printf '$ %s --version | head -n1\n' "$find"
+ "$find" --version | head -n1
+ done
+
+ for fd in "${FD[@]}"; do
+ printf '$ %s --version\n' "$fd"
+ "$fd" --version
+ done
+ } | tee -a "$md"
+
+ printf '```' >>"$md"
+}
+
+# Print benchmark details
+bench-details() {
+ group "Details"
+
+ bench-versions
+}
+
+# Run all the benchmarks
+bench() {
+ import_array BFS
+ import_array FIND
+ import_array FD
+
+ import_array COMPLETE
+ import_array EARLY_QUIT
+ import_array STAT
+ import_array PRINT
+ import_array STRATEGIES
+ import_array JOBS
+ import_array EXEC
+
+ bench-complete "${COMPLETE[@]}"
+ bench-early-quit "${EARLY_QUIT[@]}"
+ bench-stat "${STAT[@]}"
+ bench-print "${PRINT[@]}"
+ bench-strategies "${STRATEGIES[@]}"
+ bench-jobs "${JOBS[@]}"
+ bench-exec "${EXEC[@]}"
+ bench-details
+}
diff --git a/bench/clone-tree.sh b/bench/clone-tree.sh
new file mode 100755
index 0000000..744b5f4
--- /dev/null
+++ b/bench/clone-tree.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Creates a directory tree that matches a git repo, but with empty files. E.g.
+#
+# $ ./bench/clone-tree.sh "https://.../linux.git" v6.5 ./linux ./linux.git
+#
+# will create or update a shallow clone at ./linux.git, then create a directory
+# tree at ./linux with the same directory tree as the tag v6.5, except all files
+# will be empty.
+
+set -eu
+
+if (($# != 4)); then
+ printf 'Usage: %s https://url/of/repo.git <TAG> path/to/checkout path/to/repo.git\n' "$0" >&2
+ exit 1
+fi
+
+URL="$1"
+TAG="$2"
+DIR="$3"
+REPO="$4"
+
+BENCH=$(dirname -- "${BASH_SOURCE[0]}")
+BIN=$(realpath -- "$BENCH/../bin")
+BFS="$BIN/bfs"
+XTOUCH="$BIN/tests/xtouch"
+
+if [ "${NPROC-}" ]; then
+ # Use fewer cores in recursive calls
+ export NPROC=$(((NPROC + 1) / 2))
+else
+ export NPROC=$(nproc)
+fi
+
+JOBS=$((NPROC < 8 ? NPROC : 8))
+
+do-git() {
+ git -C "$REPO" "$@"
+}
+
+if ! [ -e "$REPO" ]; then
+ mkdir -p -- "$REPO"
+ do-git init -q --bare
+fi
+
+has-ref() {
+ do-git rev-list --quiet -1 --missing=allow-promisor "$1" &>/dev/null
+}
+
+sparse-fetch() {
+ do-git -c fetch.negotiationAlgorithm=noop fetch -q --filter=blob:none --depth=1 --no-tags --no-write-fetch-head --no-auto-gc "$@"
+}
+
+if ! has-ref "$TAG"; then
+ printf 'Fetching %s ...\n' "$TAG" >&2
+ do-git config remote.origin.url "$URL"
+ if ((${#TAG} >= 40)); then
+ sparse-fetch origin "$TAG"
+ else
+ sparse-fetch origin tag "$TAG"
+ fi
+fi
+
+# Delete a tree in parallel
+clean() {
+ local d=5
+ "$BFS" -f "$1" -mindepth $d -maxdepth $d -type d -print0 \
+ | xargs -0r -n1 -P$JOBS -- "$BFS" -j1 -mindepth 1 -delete -f
+ "$BFS" -f "$1" -delete
+}
+
+if [ -e "$DIR" ]; then
+ printf 'Cleaning old directory tree %s ...\n' "$DIR" >&2
+ TMP=$(mktemp -dp "$(dirname -- "$DIR")")
+ mv -- "$DIR" "$TMP"
+ clean "$TMP" &
+fi
+
+# List gitlinks (submodule references) in the tree
+ls-gitlinks() {
+ do-git ls-tree -zr "$TAG" \
+ | sed -zn 's/.* commit //p'
+}
+
+# Get the submodule ID for a path
+submodule-for-path() {
+ do-git config --blob "$TAG:.gitmodules" \
+ --name-only \
+ --fixed-value \
+ --get-regexp 'submodule\..**\.path' "$1" \
+ | sed -En 's/submodule\.(.*)\.path/\1/p'
+}
+
+# Get the URL for a submodule
+submodule-url() {
+ # - https://chrome-internal.googlesource.com/
+ # - not publicly accessible
+ # - https://chromium.googlesource.com/external/github.com/WebKit/webkit.git
+ # - is accessible, but the commit (59e9de61b7b3) isn't
+ # - https://android.googlesource.com/
+ # - is accessible, but you need an account
+
+ do-git config --blob "$TAG:.gitmodules" \
+ --get "submodule.$1.url" \
+ | sed -E \
+ -e '\|^https://chrome-internal.googlesource.com/|Q1' \
+ -e '\|^https://chromium.googlesource.com/external/github.com/WebKit/webkit.git|Q1' \
+ -e '\|^https://android.googlesource.com/|Q1'
+}
+
+# Recursively checkout submodules
+while read -rd '' SUBREF SUBDIR; do
+ SUBNAME=$(submodule-for-path "$SUBDIR")
+ SUBURL=$(submodule-url "$SUBNAME") || continue
+
+ if (($(jobs -pr | wc -w) >= JOBS)); then
+ wait -n
+ fi
+ "$0" "$SUBURL" "$SUBREF" "$DIR/$SUBDIR" "$REPO/modules/$SUBNAME" &
+done < <(ls-gitlinks)
+
+# Touch files in parallel
+xtouch() (
+ cd "$DIR"
+ if ((JOBS > 1)); then
+ xargs -0r -n4096 -P$JOBS -- "$XTOUCH" -p --
+ else
+ xargs -0r -- "$XTOUCH" -p --
+ fi
+)
+
+# Check out files
+printf 'Checking out %s ...\n' "$DIR" >&2
+mkdir -p -- "$DIR"
+do-git ls-tree -zr "$TAG"\
+ | sed -zn 's/.* blob .*\t//p' \
+ | xtouch
+
+# Wait for cleaning/submodules
+wait
diff --git a/completions/bfs.bash b/completions/bfs.bash
index f734ab1..6fd82c8 100644
--- a/completions/bfs.bash
+++ b/completions/bfs.bash
@@ -1,21 +1,8 @@
-# bash completion script for bfs
+# Copyright © Benjamin Mundt <benMundt@ibm.com>
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
-############################################################################
-# bfs #
-# Copyright (C) 2020 Benjamin Mundt <benMundt@ibm.com> #
-# Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# bash completion script for bfs
_bfs() {
local cur prev words cword
@@ -31,6 +18,7 @@ _bfs() {
-fstype
-gid
-group
+ -j
-ok
-okdir
-regextype
@@ -44,12 +32,14 @@ _bfs() {
# (e.g. because they are numeric, glob, regexp, time, etc.)
local nocomp=(
-{a,B,c,m}{min,since,time}
+ -context
-ilname
-iname
-inum
-ipath
-iregex
-iwholename
+ -limit
-links
-lname
-maxdepth
@@ -107,8 +97,6 @@ _bfs() {
-depth
-follow
-ignore_readdir_race
- -maxdepth
- -mindepth
-mount
-nocolor
-noignore_readdir_race
diff --git a/completions/bfs.fish b/completions/bfs.fish
index 3f399e7..24b0ad9 100644
--- a/completions/bfs.fish
+++ b/completions/bfs.fish
@@ -20,6 +20,7 @@ complete -c bfs -o f -d "Treat specified path as a path to search" -a "(__fish_c
complete -c bfs -o D -d "Turn on a debugging flag" -a $debug_flag_comp -x
complete -c bfs -s O -d "Enable specified optimization level" -a $optimization_comp -x
complete -c bfs -o S -d "Choose the search strategy" -a $strategy_comp -x
+complete -c bfs -s j -d "Use this many threads" -x
# Operators
@@ -70,6 +71,7 @@ complete -c bfs -o Btime -d "Find files birthed specified number of days ago" -x
complete -c bfs -o ctime -d "Find files changed specified number of days ago" -x
complete -c bfs -o mtime -d "Find files modified specified number of days ago" -x
complete -c bfs -o capable -d "Find files with capabilities set"
+complete -c bfs -o context -d "Find files by SELinux context" -x
complete -c bfs -o depth -d "Find files with specified number of depth" -x
complete -c bfs -o empty -d "Find empty files/directories"
complete -c bfs -o executable -d "Find files the current user can execute"
@@ -133,6 +135,7 @@ complete -c bfs -o fls -d "Like -ls, but write to specified file" -F
complete -c bfs -o fprint -d "Like -print, but write to specified file" -F
complete -c bfs -o fprint0 -d "Like -print0, but write to specified file" -F
complete -c bfs -o fprintf -d "Like -printf, but write to specified file" -F
+complete -c bfs -o limit -d "Limit the number of results" -x
complete -c bfs -o ls -d "List files like ls -dils"
complete -c bfs -o print -d "Print the path to the found file"
complete -c bfs -o print0 -d "Like -print, but use the null character as a separator rather than newlines"
diff --git a/completions/bfs.zsh b/completions/bfs.zsh
index 3d7dc3a..432ab8c 100644
--- a/completions/bfs.zsh
+++ b/completions/bfs.zsh
@@ -10,7 +10,7 @@ args=(
'-D[print diagnostics]:debug option:(cost exec opt rates search stat time tree all help)'
'-E[use extended regular expressions with -regex/-iregex]'
'-f[specify file hierarchy to traverse]:path:_directories'
- '-O+[enable query optimisation]:level:(1 2 3)'
+ '-O+[enable query optimisation]:level:(0 1 2 3 4 fast)'
'-s[traverse directories in sorted order]'
'-X[warn if filename contains characters special to xargs]'
"-x[don't span filesystems]"
@@ -19,6 +19,7 @@ args=(
'(-L -P)-H[only follow symlinks when resolving command-line arguments]'
"-S[select search method]:value:(bfs dfs ids eds)"
'-f[treat path as path to search]:path:_files -/'
+ '-j+[use this many threads]:threads:'
# Operators
'*-and'
@@ -73,6 +74,7 @@ args=(
'*-mtime[find files modified N days ago]:modification time (days):->times'
'*-capable[find files with POSIX.1e capabilities set]'
+ '*-context[find files by SELinux context]:pattern'
# -depth without parameters exist above. I don't know how to handle this gracefully
'*-empty[find empty files/directories]'
'*-executable[find files the current user can execute]'
@@ -81,7 +83,7 @@ args=(
'*-false[always false]'
'*-true[always true]'
'*-fstype[find files on file systems with the given type]:file system type:_file_systems'
-
+
'*-gid[find files owned by group ID N]:numeric group ID:'
'*-group[find files owned by group NAME]:group:_groups'
'*-uid[find files owned by user ID N]:numeric user ID'
@@ -117,7 +119,7 @@ args=(
'*-xattr[find files with extended attributes]'
'*-xattrname[find files with extended attribute NAME]:name:'
'*-xtype[find files of the given type following links when -type would not, and vice versa]:file type:((b\:block\ device c\:character\ device d\:directory p\:named\ pipe f\:normal\ file l\:symbolic\ link s\:socket w\:whiteout D\:Door))'
-
+
# Actions
'*-delete[delete any found files (-implies -depth)]'
'*-rm[delete any found files (-implies -depth)]'
@@ -126,13 +128,14 @@ args=(
'*-execdir[execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal'
'*-ok[prompt the user whether to execute a command]:program: _command_names -e:*(\;|+)::program arguments: _normal'
'*-okdir[prompt the user whether to execute a command in the same directory as the found files]:program: _command_names -e:*(\;|+)::program arguments: _normal'
-
+
'-exit[exit with status if found, default 0]'
'*-fls[list files like ls -dils, but write to FILE instead of standard output]:output file:_files'
'*-fprint[print the path to the found file, but write to FILE instead of standard output]:output file:_files'
'*-fprint0[print the path to the found file using null character as separator, but write to FILE instead of standard output]:output file:_files'
'*-fprintf[print according to format string, but write to FILE instead of standard output]:output file:_files:output format'
+ '*-limit[quit after N results]:maximum result count'
'*-ls[list files like ls -dils]'
'*-print[print the path to the found file]'
'*-print0[print the path to the found file using null character as separator]'
diff --git a/config/cc.sh b/config/cc.sh
new file mode 100755
index 0000000..45d51ca
--- /dev/null
+++ b/config/cc.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Run the compiler and check if it succeeded
+
+set -eu
+
+TMP=$(mktemp)
+trap 'rm -f "$TMP"' EXIT
+
+(
+ set -x
+ $XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS -o "$TMP"
+)
diff --git a/config/config.mk b/config/config.mk
new file mode 100644
index 0000000..280c6ac
--- /dev/null
+++ b/config/config.mk
@@ -0,0 +1,63 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile fragment that implements `make config`
+
+include config/prelude.mk
+include config/exports.mk
+
+# All configuration steps
+config: ${CONFIG} ${GEN}/config.h
+.PHONY: config
+
+# Makefile fragments generated by `make config`
+MKS := \
+ ${GEN}/vars.mk \
+ ${GEN}/flags.mk \
+ ${GEN}/deps.mk \
+ ${GEN}/pkgs.mk
+
+# The main configuration file, which includes the others
+${CONFIG}: ${MKS}
+ ${MSG} "[ GEN] ${TGT}"
+ @printf '# %s\n' "${TGT}" >$@
+ @printf 'include $${GEN}/%s\n' ${MKS:${GEN}/%=%} >>$@
+ ${VCAT} ${CONFIG}
+.PHONY: ${CONFIG}
+
+# Saves the configurable variables
+${GEN}/vars.mk::
+ @${MKDIR} ${@D}
+ ${MSG} "[ GEN] ${TGT}"
+ @printf '# %s\n' "${TGT}" >$@
+ @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" >>$@
+ @printf 'PKGS :=\n' >>$@
+ ${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
+ @+${MAKE} -sf config/flags.mk $@
+.PHONY: ${GEN}/flags.mk
+
+# Check for dependency generation support
+${GEN}/deps.mk: ${GEN}/flags.mk
+ @+${MAKE} -sf config/deps.mk $@
+.PHONY: ${GEN}/deps.mk
+
+# Auto-detect dependencies and their build flags
+${GEN}/pkgs.mk: ${GEN}/flags.mk
+ @+${MAKE} -sf config/pkgs.mk $@
+.PHONY: ${GEN}/pkgs.mk
+
+# Compile-time feature detection
+${GEN}/config.h: ${CONFIG}
+ @+${MAKE} -sf config/header.mk $@
+.PHONY: ${GEN}/config.h
diff --git a/config/define-if.sh b/config/define-if.sh
new file mode 100755
index 0000000..059c1ac
--- /dev/null
+++ b/config/define-if.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Output a C preprocessor definition based on whether a command succeeds
+
+set -eu
+
+SLUG="${1#config/}"
+SLUG="${SLUG%.c}"
+MACRO="BFS_$(printf '%s' "$SLUG" | tr '/a-z-' '_A-Z_')"
+shift
+
+if "$@"; then
+ printf '#define %s true\n' "$MACRO"
+else
+ printf '#define %s false\n' "$MACRO"
+ exit 1
+fi
diff --git a/config/deps.mk b/config/deps.mk
new file mode 100644
index 0000000..ac394a5
--- /dev/null
+++ b/config/deps.mk
@@ -0,0 +1,18 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/deps.mk
+
+include config/prelude.mk
+include ${GEN}/vars.mk
+include ${GEN}/flags.mk
+include config/exports.mk
+
+${GEN}/deps.mk::
+ ${MSG} "[ GEN] ${TGT}"
+ printf '# %s\n' "${TGT}" >$@
+ if config/cc.sh -MD -MP -MF /dev/null config/empty.c; then \
+ printf 'DEPFLAGS = -MD -MP\n'; \
+ fi >>$@ 2>$@.log
+ ${VCAT} $@
+ printf -- '-include %s\n' ${OBJS:.o=.d} >>$@
diff --git a/config/empty.c b/config/empty.c
new file mode 100644
index 0000000..4fa9a5b
--- /dev/null
+++ b/config/empty.c
@@ -0,0 +1,6 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+int main(void) {
+ return 0;
+}
diff --git a/config/exports.mk b/config/exports.mk
new file mode 100644
index 0000000..ed19134
--- /dev/null
+++ b/config/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/config/flags.mk b/config/flags.mk
new file mode 100644
index 0000000..837c30f
--- /dev/null
+++ b/config/flags.mk
@@ -0,0 +1,136 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/flags.mk
+
+include config/prelude.mk
+include ${GEN}/vars.mk
+
+# Configurable flags
+CPPFLAGS ?=
+CFLAGS ?= \
+ -g \
+ -Wall \
+ -Wformat=2 \
+ -Werror=implicit \
+ -Wimplicit-fallthrough \
+ -Wmissing-declarations \
+ -Wshadow \
+ -Wsign-compare \
+ -Wstrict-prototypes
+LDFLAGS ?=
+LDLIBS ?=
+
+export XCPPFLAGS=${CPPFLAGS}
+export XCFLAGS=${CFLAGS}
+export XLDFLAGS=${LDFLAGS}
+export XLDLIBS=${LDLIBS}
+
+# Immutable flags
+export BFS_CPPFLAGS= \
+ -Isrc \
+ -I${GEN} \
+ -D__EXTENSIONS__ \
+ -D_ATFILE_SOURCE \
+ -D_BSD_SOURCE \
+ -D_DARWIN_C_SOURCE \
+ -D_DEFAULT_SOURCE \
+ -D_GNU_SOURCE \
+ -D_POSIX_PTHREAD_SEMANTICS \
+ -D_FILE_OFFSET_BITS=64 \
+ -D_TIME_BITS=64
+export BFS_CFLAGS= -std=c17 -pthread
+
+# Platform-specific system libraries
+LDLIBS,DragonFly := -lposix1e
+LDLIBS,Linux := -lrt
+LDLIBS,NetBSD := -lutil
+LDLIBS,SunOS := -lsec -lsocket -lnsl
+export BFS_LDLIBS=${LDLIBS,${OS}}
+
+# Build profiles
+_ASAN := ${TRUTHY,${ASAN}}
+_LSAN := ${TRUTHY,${LSAN}}
+_MSAN := ${TRUTHY,${MSAN}}
+_TSAN := ${TRUTHY,${TSAN}}
+_UBSAN := ${TRUTHY,${UBSAN}}
+_GCOV := ${TRUTHY,${GCOV}}
+_LINT := ${TRUTHY,${LINT}}
+_RELEASE := ${TRUTHY,${RELEASE}}
+
+# https://github.com/google/sanitizers/issues/342
+TSAN_CPPFLAGS,y := -DBFS_USE_TARGET_CLONES=0
+export TSAN_CPPFLAGS=${TSAN_CPPFLAGS,${_TSAN}}
+
+ASAN_CFLAGS,y := -fsanitize=address
+LSAN_CFLAGS,y := -fsanitize=leak
+MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
+TSAN_CFLAGS,y := -fsanitize=thread
+UBSAN_CFLAGS.y := -fsanitize=undefined
+
+export ASAN_CFLAGS=${ASAN_CFLAGS,${_ASAN}}
+export LSAN_CFLAGS=${LSAN_CFLAGS,${_LSAN}}
+export MSAN_CFLAGS=${MSAN_CFLAGS,${_MSAN}}
+export TSAN_CFLAGS=${TSAN_CFLAGS,${_TSAN}}
+export UBSAN_CFLAGS=${UBSAN_CFLAGS,${_UBSAN}}
+
+SAN_CFLAGS,y := -fno-sanitize-recover=all
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+SAN := ${NOT,${INSANE}}
+export SAN_CFLAGS=${SAN_CFLAGS,${SAN}}
+
+# MSAN and TSAN both need all code to be instrumented
+YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+NOLIBS ?= ${NOT,${YESLIBS}}
+export XNOLIBS=${NOLIBS}
+
+# gcov only intercepts fork()/exec() with -std=gnu*
+GCOV_CFLAGS,y := -std=gnu17 --coverage
+export GCOV_CFLAGS=${GCOV_CFLAGS,${_GCOV}}
+
+LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
+LINT_CFLAGS,y := -Werror -O2
+
+export LINT_CPPFLAGS=${LINT_CPPFLAGS,${_LINT}}
+export LINT_CFLAGS=${LINT_CFLAGS,${_LINT}}
+
+RELEASE_CPPFLAGS,y := -DNDEBUG
+RELEASE_CFLAGS,y := -O3 -flto=auto
+
+export RELEASE_CPPFLAGS=${RELEASE_CPPFLAGS,${_RELEASE}}
+export RELEASE_CFLAGS=${RELEASE_CFLAGS,${_RELEASE}}
+
+# Set a variable
+SETVAR = printf '%s := %s\n' >>$@
+
+# Append to a variable, if non-empty
+APPEND = append() { test -z "$$2" || printf '%s += %s\n' "$$1" "$$2" >>$@; }; append
+
+${GEN}/flags.mk::
+ ${MSG} "[ GEN] ${TGT}"
+ printf '# %s\n' "${TGT}" >$@
+ ${SETVAR} CPPFLAGS "$$BFS_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$TSAN_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$LINT_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$RELEASE_CPPFLAGS"
+ ${APPEND} CPPFLAGS "$$XCPPFLAGS"
+ ${APPEND} CPPFLAGS "$$EXTRA_CPPFLAGS"
+ ${SETVAR} CFLAGS "$$BFS_CFLAGS"
+ ${APPEND} CFLAGS "$$ASAN_CFLAGS"
+ ${APPEND} CFLAGS "$$LSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$MSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$TSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$UBSAN_CFLAGS"
+ ${APPEND} CFLAGS "$$SAN_CFLAGS"
+ ${APPEND} CFLAGS "$$GCOV_CFLAGS"
+ ${APPEND} CFLAGS "$$LINT_CFLAGS"
+ ${APPEND} CFLAGS "$$RELEASE_CFLAGS"
+ ${APPEND} CFLAGS "$$XCFLAGS"
+ ${APPEND} CFLAGS "$$EXTRA_CFLAGS"
+ ${SETVAR} LDFLAGS "$$XLDFLAGS"
+ ${SETVAR} LDLIBS "$$XLDLIBS"
+ ${APPEND} LDLIBS "$$EXTRA_LDLIBS"
+ ${APPEND} LDLIBS "$$BFS_LDLIBS"
+ ${SETVAR} NOLIBS "$$XNOLIBS"
+ test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
+ ${VCAT} $@
diff --git a/config/has/acl-get-entry.c b/config/has/acl-get-entry.c
new file mode 100644
index 0000000..3cce771
--- /dev/null
+++ b/config/has/acl-get-entry.c
@@ -0,0 +1,8 @@
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ acl_entry_t entry;
+ return acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+}
diff --git a/config/has/acl-get-file.c b/config/has/acl-get-file.c
new file mode 100644
index 0000000..89fbf23
--- /dev/null
+++ b/config/has/acl-get-file.c
@@ -0,0 +1,8 @@
+#include <stddef.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_t acl = acl_get_file(".", ACL_TYPE_DEFAULT);
+ return acl == (acl_t)NULL;
+}
diff --git a/config/has/acl-get-tag-type.c b/config/has/acl-get-tag-type.c
new file mode 100644
index 0000000..2901956
--- /dev/null
+++ b/config/has/acl-get-tag-type.c
@@ -0,0 +1,10 @@
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+
+int main(void) {
+ acl_entry_t entry;
+ memset(&entry, 0, sizeof(entry));
+ acl_tag_t tag;
+ return acl_get_tag_type(entry, &tag);
+}
diff --git a/config/has/acl-is-trivial-np.c b/config/has/acl-is-trivial-np.c
new file mode 100644
index 0000000..9ca9fc7
--- /dev/null
+++ b/config/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/config/has/acl-trivial.c b/config/has/acl-trivial.c
new file mode 100644
index 0000000..7efc838
--- /dev/null
+++ b/config/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/config/has/aligned-alloc.c b/config/has/aligned-alloc.c
new file mode 100644
index 0000000..4460038
--- /dev/null
+++ b/config/has/aligned-alloc.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+
+int main(void) {
+ return !aligned_alloc(_Alignof(void *), sizeof(void *));
+}
diff --git a/config/has/confstr.c b/config/has/confstr.c
new file mode 100644
index 0000000..58280b4
--- /dev/null
+++ b/config/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/config/has/extattr-get-file.c b/config/has/extattr-get-file.c
new file mode 100644
index 0000000..ac9cf96
--- /dev/null
+++ b/config/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/config/has/extattr-get-link.c b/config/has/extattr-get-link.c
new file mode 100644
index 0000000..c35be5b
--- /dev/null
+++ b/config/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/config/has/extattr-list-file.c b/config/has/extattr-list-file.c
new file mode 100644
index 0000000..e68a8bb
--- /dev/null
+++ b/config/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/config/has/extattr-list-link.c b/config/has/extattr-list-link.c
new file mode 100644
index 0000000..49f0ec2
--- /dev/null
+++ b/config/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/config/has/fdclosedir.c b/config/has/fdclosedir.c
new file mode 100644
index 0000000..f4ad1f5
--- /dev/null
+++ b/config/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/config/has/getdents.c b/config/has/getdents.c
new file mode 100644
index 0000000..d0d4228
--- /dev/null
+++ b/config/has/getdents.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ struct dirent de;
+ getdents(3, &de, 1024);
+ return 0;
+}
diff --git a/config/has/getdents64-syscall.c b/config/has/getdents64-syscall.c
new file mode 100644
index 0000000..4838c14
--- /dev/null
+++ b/config/has/getdents64-syscall.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+int main(void) {
+ struct dirent64 de;
+ syscall(SYS_getdents64, 3, &de, 1024);
+ return 0;
+}
diff --git a/config/has/getdents64.c b/config/has/getdents64.c
new file mode 100644
index 0000000..1abf36d
--- /dev/null
+++ b/config/has/getdents64.c
@@ -0,0 +1,10 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <dirent.h>
+
+int main(void) {
+ struct dirent64 de;
+ getdents64(3, &de, 1024);
+ return 0;
+}
diff --git a/config/has/getprogname-gnu.c b/config/has/getprogname-gnu.c
new file mode 100644
index 0000000..6b97c5e
--- /dev/null
+++ b/config/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/config/has/getprogname.c b/config/has/getprogname.c
new file mode 100644
index 0000000..83dc8e8
--- /dev/null
+++ b/config/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/config/has/max-align-t.c b/config/has/max-align-t.c
new file mode 100644
index 0000000..96165ce
--- /dev/null
+++ b/config/has/max-align-t.c
@@ -0,0 +1,8 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stddef.h>
+
+int main(void) {
+ return _Alignof(max_align_t);
+}
diff --git a/config/has/pipe2.c b/config/has/pipe2.c
new file mode 100644
index 0000000..4cb43b5
--- /dev/null
+++ b/config/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/config/has/posix-spawn-addfchdir-np.c b/config/has/posix-spawn-addfchdir-np.c
new file mode 100644
index 0000000..b870a53
--- /dev/null
+++ b/config/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/config/has/posix-spawn-addfchdir.c b/config/has/posix-spawn-addfchdir.c
new file mode 100644
index 0000000..c52ff81
--- /dev/null
+++ b/config/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/config/has/st-acmtim.c b/config/has/st-acmtim.c
new file mode 100644
index 0000000..d687ab0
--- /dev/null
+++ b/config/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/config/has/st-acmtimespec.c b/config/has/st-acmtimespec.c
new file mode 100644
index 0000000..f747bc0
--- /dev/null
+++ b/config/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/config/has/st-birthtim.c b/config/has/st-birthtim.c
new file mode 100644
index 0000000..4964571
--- /dev/null
+++ b/config/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/config/has/st-birthtimespec.c b/config/has/st-birthtimespec.c
new file mode 100644
index 0000000..91a613f
--- /dev/null
+++ b/config/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/config/has/st-flags.c b/config/has/st-flags.c
new file mode 100644
index 0000000..b1d0c32
--- /dev/null
+++ b/config/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/config/has/statx-syscall.c b/config/has/statx-syscall.c
new file mode 100644
index 0000000..87ec869
--- /dev/null
+++ b/config/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/config/has/statx.c b/config/has/statx.c
new file mode 100644
index 0000000..65f1674
--- /dev/null
+++ b/config/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/config/has/strerror-l.c b/config/has/strerror-l.c
new file mode 100644
index 0000000..3dcc4d7
--- /dev/null
+++ b/config/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/config/has/strerror-r-gnu.c b/config/has/strerror-r-gnu.c
new file mode 100644
index 0000000..26ca0ee
--- /dev/null
+++ b/config/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/config/has/strerror-r-posix.c b/config/has/strerror-r-posix.c
new file mode 100644
index 0000000..41b2d30
--- /dev/null
+++ b/config/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/config/has/tm-gmtoff.c b/config/has/tm-gmtoff.c
new file mode 100644
index 0000000..543df48
--- /dev/null
+++ b/config/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/config/has/uselocale.c b/config/has/uselocale.c
new file mode 100644
index 0000000..a712ff8
--- /dev/null
+++ b/config/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/config/header.mk b/config/header.mk
new file mode 100644
index 0000000..50ad5e8
--- /dev/null
+++ b/config/header.mk
@@ -0,0 +1,69 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/config.h
+
+include config/prelude.mk
+include ${GEN}/config.mk
+include config/exports.mk
+
+# All header fragments we generate
+HEADERS := \
+ ${GEN}/has/acl-get-entry.h \
+ ${GEN}/has/acl-get-file.h \
+ ${GEN}/has/acl-get-tag-type.h \
+ ${GEN}/has/acl-is-trivial-np.h \
+ ${GEN}/has/acl-trivial.h \
+ ${GEN}/has/aligned-alloc.h \
+ ${GEN}/has/confstr.h \
+ ${GEN}/has/extattr-get-file.h \
+ ${GEN}/has/extattr-get-link.h \
+ ${GEN}/has/extattr-list-file.h \
+ ${GEN}/has/extattr-list-link.h \
+ ${GEN}/has/fdclosedir.h \
+ ${GEN}/has/getdents.h \
+ ${GEN}/has/getdents64.h \
+ ${GEN}/has/getdents64-syscall.h \
+ ${GEN}/has/getprogname.h \
+ ${GEN}/has/getprogname-gnu.h \
+ ${GEN}/has/max-align-t.h \
+ ${GEN}/has/pipe2.h \
+ ${GEN}/has/posix-spawn-addfchdir.h \
+ ${GEN}/has/posix-spawn-addfchdir-np.h \
+ ${GEN}/has/st-acmtim.h \
+ ${GEN}/has/st-acmtimespec.h \
+ ${GEN}/has/st-birthtim.h \
+ ${GEN}/has/st-birthtimespec.h \
+ ${GEN}/has/st-flags.h \
+ ${GEN}/has/statx.h \
+ ${GEN}/has/statx-syscall.h \
+ ${GEN}/has/strerror-l.h \
+ ${GEN}/has/strerror-r-gnu.h \
+ ${GEN}/has/strerror-r-posix.h \
+ ${GEN}/has/tm-gmtoff.h \
+ ${GEN}/has/uselocale.h
+
+# Previously generated by pkgs.mk
+PKG_HEADERS := ${ALL_PKGS:%=${GEN}/use/%.h}
+
+${GEN}/config.h: ${PKG_HEADERS} ${HEADERS}
+ ${MSG} "[ GEN] ${TGT}"
+ printf '// %s\n' "${TGT}" >$@
+ printf '#ifndef BFS_CONFIG_H\n' >>$@
+ printf '#define BFS_CONFIG_H\n' >>$@
+ cat ${.ALLSRC} >>$@
+ printf '#endif // BFS_CONFIG_H\n' >>$@
+ cat ${.ALLSRC:%=%.log} >$@.log
+ ${VCAT} $@
+.PHONY: ${GEN}/config.h
+
+# The short name of the config test
+SLUG = ${@:${GEN}/%.h=%}
+
+${HEADERS}::
+ ${MKDIR} ${@D}
+ if config/define-if.sh ${SLUG} config/cc.sh config/${SLUG}.c >$@ 2>$@.log; then \
+ test "${IS_V}" || printf '[ CC ] %-${MSG_WIDTH}s ✔\n' ${SLUG}.c; \
+ else \
+ test "${IS_V}" || printf '[ CC ] %-${MSG_WIDTH}s ✘\n' ${SLUG}.c; \
+ fi
diff --git a/config/pkgconf.sh b/config/pkgconf.sh
new file mode 100755
index 0000000..365ed61
--- /dev/null
+++ b/config/pkgconf.sh
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# pkg-config wrapper with hardcoded fallbacks
+
+set -eu
+
+MODE=
+case "${1:-}" in
+ --*)
+ MODE="$1"
+ shift
+esac
+
+if [ $# -lt 1 ]; then
+ exit
+fi
+
+case "$XNOLIBS" in
+ y|1)
+ exit 1
+esac
+
+if [ -z "$MODE" ]; then
+ # Check whether the libraries exist at all
+ for LIB; do
+ # Check ${USE_$LIB}
+ USE_LIB="USE_$(printf '%s' "$LIB" | tr 'a-z-' 'A-Z_')"
+ eval "USE=\"\${$USE_LIB:-}\""
+ case "$USE" in
+ y|1)
+ continue
+ ;;
+ n|0)
+ exit 1
+ ;;
+ esac
+
+ CFLAGS=$("$0" --cflags "$LIB") || exit 1
+ LDFLAGS=$("$0" --ldflags "$LIB") || exit 1
+ LDLIBS=$("$0" --ldlibs "$LIB") || exit 1
+ config/cc.sh $CFLAGS $LDFLAGS config/use/$LIB.c $LDLIBS || exit 1
+ done
+fi
+
+# Defer to pkg-config if possible
+if command -v "${XPKG_CONFIG:-}" >/dev/null 2>&1; then
+ case "$MODE" in
+ --cflags)
+ "$XPKG_CONFIG" --cflags "$@"
+ ;;
+ --ldflags)
+ "$XPKG_CONFIG" --libs-only-L --libs-only-other "$@"
+ ;;
+ --ldlibs)
+ "$XPKG_CONFIG" --libs-only-l "$@"
+ ;;
+ esac
+
+ exit
+fi
+
+# pkg-config unavailable, emulate it ourselves
+CFLAGS=""
+LDFLAGS=""
+LDLIBS=""
+
+for LIB; do
+ case "$LIB" in
+ libacl)
+ LDLIB=-lacl
+ ;;
+ libcap)
+ LDLIB=-lcap
+ ;;
+ libselinux)
+ LDLIB=-lselinux
+ ;;
+ liburing)
+ LDLIB=-luring
+ ;;
+ oniguruma)
+ LDLIB=-lonig
+ ;;
+ *)
+ printf 'error: Unknown package %s\n' "$LIB" >&2
+ exit 1
+ ;;
+ esac
+
+ LDLIBS="$LDLIBS$LDLIB "
+done
+
+case "$MODE" in
+ --ldlibs)
+ printf '%s\n' "$LDLIBS"
+ ;;
+esac
diff --git a/config/pkgs.mk b/config/pkgs.mk
new file mode 100644
index 0000000..898f262
--- /dev/null
+++ b/config/pkgs.mk
@@ -0,0 +1,36 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/pkgs.mk
+
+include config/prelude.mk
+include ${GEN}/vars.mk
+include ${GEN}/flags.mk
+include config/exports.mk
+
+HEADERS := ${ALL_PKGS:%=${GEN}/use/%.h}
+
+${GEN}/pkgs.mk: ${HEADERS}
+ ${MSG} "[ GEN] ${TGT}"
+ printf '# %s\n' "${TGT}" >$@
+ gen() { \
+ printf 'PKGS := %s\n' "$$*"; \
+ printf 'CFLAGS += %s\n' "$$(config/pkgconf.sh --cflags "$$@")"; \
+ printf 'LDFLAGS += %s\n' "$$(config/pkgconf.sh --ldflags "$$@")"; \
+ printf 'LDLIBS := %s $${LDLIBS}\n' "$$(config/pkgconf.sh --ldlibs "$$@")"; \
+ }; \
+ gen $$(grep -l ' true$$' ${.ALLSRC} | sed 's|.*/\(.*\)\.h|\1|') >>$@
+ ${VCAT} $@
+
+.PHONY: ${GEN}/pkgs.mk
+
+# Convert ${GEN}/use/foo.h to foo
+PKG = ${@:${GEN}/use/%.h=%}
+
+${HEADERS}::
+ ${MKDIR} ${@D}
+ if config/define-if.sh use/${PKG} config/pkgconf.sh ${PKG} >$@ 2>$@.log; then \
+ test "${IS_V}" || printf '[ CC ] %-${MSG_WIDTH}s ✔\n' use/${PKG}.c; \
+ else \
+ test "${IS_V}" || printf '[ CC ] %-${MSG_WIDTH}s ✘\n' use/${PKG}.c; \
+ fi
diff --git a/config/prelude.mk b/config/prelude.mk
new file mode 100644
index 0000000..109e60c
--- /dev/null
+++ b/config/prelude.mk
@@ -0,0 +1,162 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Common makefile utilities. Compatible with both GNU make and most BSD makes.
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# We don't use any suffix rules
+.SUFFIXES:
+
+# GNU make has $^ for the full list of targets, while BSD make has $> and the
+# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would
+# break if one of them implemented support for the other. So instead, bring
+# BSD's ${.ALLSRC} to GNU.
+.ALLSRC ?= $^
+
+# For out-of-tree builds, e.g.
+#
+# $ make config BUILDDIR=/path/to/build/dir
+# $ make BUILDDIR=/path/to/build/dir
+BUILDDIR ?= .
+
+# Shorthand for build subdirectories
+BIN := ${BUILDDIR}/bin
+GEN := ${BUILDDIR}/gen
+OBJ := ${BUILDDIR}/obj
+
+# GNU make strips a leading ./ from target names, so do the same for BSD make
+BIN := ${BIN:./%=%}
+GEN := ${GEN:./%=%}
+OBJ := ${OBJ:./%=%}
+
+# The configuration file generated by `make config`
+CONFIG := ${GEN}/config.mk
+
+# Installation paths
+DESTDIR ?=
+PREFIX ?= /usr
+MANDIR ?= ${PREFIX}/share/man
+
+# GNU make supports `export VAR`, but BSD make requires `export VAR=value`.
+# Sadly, GNU make gives a recursion error on `export VAR=${VAR}`.
+_BUILDDIR := ${BUILDDIR}
+export BUILDDIR=${_BUILDDIR}
+
+# Configurable executables; can be overridden with
+#
+# $ make config CC=clang
+CC ?= cc
+INSTALL ?= install
+MKDIR ?= mkdir -p
+PKG_CONFIG ?= pkg-config
+RM ?= rm -f
+
+# GNU and BSD make have incompatible syntax for conditionals, but we can do a
+# lot with just nested variable expansion. We use "y" as the canonical
+# truthy value, and "" (the empty string) as the canonical falsey value.
+#
+# To normalize a boolean, use ${TRUTHY,${VAR}}, which expands like this:
+#
+# VAR=y ${TRUTHY,${VAR}} => ${TRUTHY,y} => y
+# VAR=1 ${TRUTHY,${VAR}} => ${TRUTHY,1} => y
+# VAR=n ${TRUTHY,${VAR}} => ${TRUTHY,n} => [empty]
+# VAR=other ${TRUTHY,${VAR}} => ${TRUTHY,other} => [empty]
+# VAR= ${TRUTHY,${VAR}} => ${TRUTHY,} => [emtpy]
+#
+# Inspired by https://github.com/wahern/autoguess
+TRUTHY,y := y
+TRUTHY,1 := y
+
+# Boolean operators are also implemented with nested expansion
+NOT, := y
+
+# Normalize ${V} to either "y" or ""
+IS_V := ${TRUTHY,${V}}
+
+# Suppress output unless V=1
+Q, := @
+Q := ${Q,${IS_V}}
+
+# The current target, with ${BUILDDIR} stripped for shorter messages
+TGT = ${@:${BUILDDIR}/%=%}
+
+# Show full commands with `make V=1`, otherwise short summaries
+MSG = @msg() { \
+ MSG="$$1"; \
+ shift; \
+ test "${IS_V}" || printf '%s\n' "$$MSG"; \
+ test "$${1:-}" || return 0; \
+ test "${IS_V}" && printf '%s\n' "$$*"; \
+ "$$@"; \
+ }; \
+ msg
+
+# Maximum width of a short message, to align the ✔/✘
+MSG_WIDTH := 30
+
+# cat a file if V=1
+VCAT,y := @cat
+VCAT, := @:
+VCAT := ${VCAT,${IS_V}}
+
+# All external dependencies
+ALL_PKGS := \
+ libacl \
+ libcap \
+ libselinux \
+ liburing \
+ oniguruma
+
+# List all object files here, as they're needed by both `make config` and `make`
+
+# All object files except the entry point
+LIBBFS := \
+ ${OBJ}/src/alloc.o \
+ ${OBJ}/src/bar.o \
+ ${OBJ}/src/bfstd.o \
+ ${OBJ}/src/bftw.o \
+ ${OBJ}/src/color.o \
+ ${OBJ}/src/ctx.o \
+ ${OBJ}/src/diag.o \
+ ${OBJ}/src/dir.o \
+ ${OBJ}/src/dstring.o \
+ ${OBJ}/src/eval.o \
+ ${OBJ}/src/exec.o \
+ ${OBJ}/src/expr.o \
+ ${OBJ}/src/fsade.o \
+ ${OBJ}/src/ioq.o \
+ ${OBJ}/src/mtab.o \
+ ${OBJ}/src/opt.o \
+ ${OBJ}/src/parse.o \
+ ${OBJ}/src/printf.o \
+ ${OBJ}/src/pwcache.o \
+ ${OBJ}/src/stat.o \
+ ${OBJ}/src/thread.o \
+ ${OBJ}/src/trie.o \
+ ${OBJ}/src/typo.o \
+ ${OBJ}/src/xregex.o \
+ ${OBJ}/src/xspawn.o \
+ ${OBJ}/src/xtime.o \
+ ${OBJ}/gen/version.o
+
+# Unit test objects
+UNIT_OBJS := \
+ ${OBJ}/tests/alloc.o \
+ ${OBJ}/tests/bfstd.o \
+ ${OBJ}/tests/bit.o \
+ ${OBJ}/tests/ioq.o \
+ ${OBJ}/tests/main.o \
+ ${OBJ}/tests/trie.o \
+ ${OBJ}/tests/xspawn.o \
+ ${OBJ}/tests/xtime.o
+
+# All object files
+OBJS := \
+ ${OBJ}/src/main.o \
+ ${OBJ}/tests/mksock.o \
+ ${OBJ}/tests/xspawnee.o \
+ ${OBJ}/tests/xtouch.o \
+ ${LIBBFS} \
+ ${UNIT_OBJS}
diff --git a/config/use/libacl.c b/config/use/libacl.c
new file mode 100644
index 0000000..de1fe50
--- /dev/null
+++ b/config/use/libacl.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/acl.h>
+
+int main(void) {
+ acl_free(0);
+ return 0;
+}
diff --git a/config/use/libcap.c b/config/use/libcap.c
new file mode 100644
index 0000000..58e832c
--- /dev/null
+++ b/config/use/libcap.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <sys/capability.h>
+
+int main(void) {
+ cap_free(0);
+ return 0;
+}
diff --git a/config/use/libselinux.c b/config/use/libselinux.c
new file mode 100644
index 0000000..bca409d
--- /dev/null
+++ b/config/use/libselinux.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <selinux/selinux.h>
+
+int main(void) {
+ freecon(0);
+ return 0;
+}
diff --git a/config/use/liburing.c b/config/use/liburing.c
new file mode 100644
index 0000000..bea499a
--- /dev/null
+++ b/config/use/liburing.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <liburing.h>
+
+int main(void) {
+ io_uring_free_probe(0);
+ return 0;
+}
diff --git a/config/use/oniguruma.c b/config/use/oniguruma.c
new file mode 100644
index 0000000..cb17596
--- /dev/null
+++ b/config/use/oniguruma.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <oniguruma.h>
+
+int main(void) {
+ onig_free(0);
+ return 0;
+}
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
index 932845b..4ed139c 100644
--- a/docs/BUILDING.md
+++ b/docs/BUILDING.md
@@ -7,9 +7,10 @@ Compiling
`bfs` uses [GNU Make](https://www.gnu.org/software/make/) as its build system.
A simple invocation of
+ $ make config
$ make
-should build `bfs` successfully, with no additional steps necessary.
+should build `bfs` successfully.
As usual with `make`, you can run a [parallel build](https://www.gnu.org/software/make/manual/html_node/Parallel.html) with `-j`.
For example, to use all your cores, run `make -j$(nproc)`.
@@ -17,25 +18,28 @@ For example, to use all your cores, run `make -j$(nproc)`.
| Command | Description |
|------------------|---------------------------------------------------------------|
+| `make config` | Configures the build system |
| `make` | Builds just the `bfs` binary |
| `make all` | Builds everything, including the tests (but doesn't run them) |
| `make check` | Builds everything, and runs the tests |
| `make install` | Installs `bfs` (with man page, shell completions, etc.) |
| `make uninstall` | Uninstalls `bfs` |
+| `make clean` | Delete the build products |
+| `make distclean` | Delete all generated files, including the build configuration |
-### Flag-like targets
+### Build profiles
-The build system provides a few shorthand targets for handy configurations:
+The configuration system provides a few shorthand flags for handy configurations:
-| Command | Description |
-|----------------|-------------------------------------------------------------|
-| `make release` | Build `bfs` with optimizations, LTO, and without assertions |
-| `make asan` | Enable [AddressSanitizer] |
-| `make lsan` | Enable [LeakSanitizer] |
-| `make msan` | Enable [MemorySanitizer] |
-| `make tsan` | Enable [ThreadSanitizer] |
-| `make ubsan` | Enable [UndefinedBehaviorSanitizer] |
-| `make gcov` | Enable [code coverage] |
+| Command | Description |
+|-------------------------|-------------------------------------------------------------|
+| `make config RELEASE=y` | Build `bfs` with optimizations, LTO, and without assertions |
+| `make config ASAN=y` | Enable [AddressSanitizer] |
+| `make config LSAN=y` | Enable [LeakSanitizer] |
+| `make config MSAN=y` | Enable [MemorySanitizer] |
+| `make config TSAN=y` | Enable [ThreadSanitizer] |
+| `make config UBSAN=y` | Enable [UndefinedBehaviorSanitizer] |
+| `make config GCOV=y` | Enable [code coverage] |
[AddressSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizer
[LeakSanitizer]: https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer#stand-alone-mode
@@ -44,42 +48,44 @@ The build system provides a few shorthand targets for handy configurations:
[UndefinedBehaviorSanitizer]: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
[code coverage]: https://gcc.gnu.org/onlinedocs/gcc/Gcov.html
-You can combine multiple flags and other targets (e.g. `make asan ubsan check`), but not all of them will work together.
+You can combine multiple profiles (e.g. `make config ASAN=y UBSAN=y`), but not all of them will work together.
### Flags
-Other flags are controlled with `make` variables and/or environment variables.
+Other flags can be specified on the `make config` command line or in the environment.
Here are some of the common ones; check the [`Makefile`](/Makefile) for more.
-| Flag | Description |
-|----------------------------------|---------------------------------------------|
-| `CC` | The C compiler to use, e.g. `make CC=clang` |
-| `CFLAGS`<br>`EXTRA_CFLAGS` | Override/add to the default compiler flags |
-| `LDFLAGS`<br>`EXTRA_LDFLAGS` | Override/add to the linker flags |
-| `WITH_ACL`<br>`WITH_ATTR`<br>... | Enable/disable [optional dependencies] |
-| `TEST_FLAGS` | `tests.sh` flags for `make check` |
-| `BUILDDIR` | The build output directory (default: `.`) |
-| `DESTDIR` | The root directory for `make install` |
-| `PREFIX` | The installation prefix (default: `/usr`) |
-| `MANDIR` | The man page installation directory |
+| Flag | Description |
+|-------------------------------------|----------------------------------------------------|
+| `CC` | The C compiler to use, e.g. `make config CC=clang` |
+| `CFLAGS`<br>`EXTRA_CFLAGS` | Override/add to the default compiler flags |
+| `LDFLAGS`<br>`EXTRA_LDFLAGS` | Override/add to the linker flags |
+| `USE_LIBACL`<br>`USE_LIBCAP`<br>... | Enable/disable [optional dependencies] |
+| `TEST_FLAGS` | `tests.sh` flags for `make check` |
+| `BUILDDIR` | The build output directory (default: `.`) |
+| `DESTDIR` | The root directory for `make install` |
+| `PREFIX` | The installation prefix (default: `/usr`) |
+| `MANDIR` | The man page installation directory |
[optional dependencies]: #dependencies
### Dependencies
`bfs` depends on some system libraries for some of its features.
-These dependencies are optional, and can be turned off at build time if necessary by setting the appropriate variable to the empty string (e.g. `make WITH_ONIGURUMA=`).
+These dependencies are optional, and can be turned off in `make config` if necessary by setting the appropriate variable to `n` (e.g. `make config USE_ONIGURUMA=n`).
-| Dependency | Platforms | `make` flag |
-|-------------|------------|------------------|
-| [acl] | Linux only | `WITH_ACL` |
-| [attr] | Linux only | `WITH_ATTR` |
-| [libcap] | Linux only | `WITH_LIBCAP` |
-| [Oniguruma] | All | `WITH_ONIGURUMA` |
+| Dependency | Platforms | `make config` flag |
+|--------------|------------|--------------------|
+| [libacl] | Linux only | `USE_LIBACL` |
+| [libcap] | Linux only | `USE_LIBCAP` |
+| [liburing] | Linux only | `USE_LIBURING` |
+| [libselinux] | Linux only | `USE_LIBSELINUX` |
+| [Oniguruma] | All | `USE_ONIGURUMA` |
-[acl]: https://savannah.nongnu.org/projects/acl
-[attr]: https://savannah.nongnu.org/projects/attr
+[libacl]: https://savannah.nongnu.org/projects/acl
[libcap]: https://sites.google.com/site/fullycapable/
+[libselinux]: https://github.com/SELinuxProject/selinux
+[liburing]: https://github.com/axboe/liburing
[Oniguruma]: https://github.com/kkos/oniguruma
### Dependency tracking
@@ -87,18 +93,19 @@ These dependencies are optional, and can be turned off at build time if necessar
The build system automatically tracks header dependencies with the `-M` family of compiler options (see `DEPFLAGS` in the [`Makefile`](/Makefile)).
So if you edit a header file, `make` will rebuild the necessary object files ensuring they don't go out of sync.
+We also add a dependency on the current configuration, so you can change configurations and rebuild without having to `make clean`.
+
We go one step further than most build systems by tracking the flags that were used for the previous compilation.
That means you can change configurations without having to `make clean`.
For example,
+ $ make config
+ $ make
+ $ make config RELEASE=y
$ make
- $ make release
will build the project in debug mode and then rebuild it in release mode.
-A side effect of this may be surprising: `make check` by itself will rebuild the project in the default configuration.
-To test a different configuration, you'll have to repeat it (e.g. `make release check`).
-
Testing
-------
@@ -107,27 +114,28 @@ Testing
$ make check
-Most of the testsuite is implemented in the file [`tests.sh`](/tests.sh).
-This script contains hundreds of separate test cases.
-Most of them are *snapshot tests* which compare `bfs`'s output to a known-good copy saved under [`tests`](/tests).
+The test harness is implemented in the file [`tests/tests.sh`](/tests/tests.sh).
+Individual test cases are found in `tests/*/*.sh`.
+Most of them are *snapshot tests* which compare `bfs`'s output to a known-good copy saved under the matching `tests/*/*.out`.
You can pass the name of a particular test case (or a few) to run just those tests.
For example:
- $ ./tests/tests.sh test_basic
+ $ ./tests/tests.sh posix/basic
If you need to update the reference snapshot, pass `--update`.
It can be handy to generate the snapshot with a different `find` implementation to ensure the output is correct, for example:
- $ ./tests/tests.sh test_basic --bfs=find --update
+ $ ./tests/tests.sh posix/basic --bfs=find --update
But keep in mind, other `find` implementations may not be correct.
To my knowledge, no other implementation passes even the POSIX-compatible subset of the tests:
$ ./tests/tests.sh --bfs=find --posix
...
- tests passed: 89
- tests failed: 5
+ tests passed: 90
+ tests skipped: 3
+ tests failed: 6
Run
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 9d02e8e..672c2b4 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,258 @@
+3.*
+===
+
+3.1.3
+-----
+
+**March 6, 2024**
+
+### Bug fixes
+
+- On Linux, the `io_uring` feature probing introduced in `bfs` 3.1.2 only applied to one thread, causing all other threads to avoid using io_uring entirely.
+ The probe results are now copied to all threads correctly.
+ ([`f64f76b`](https://github.com/tavianator/bfs/commit/f64f76b55400b71e8576ed7e4a377eb5ef9576aa))
+
+
+3.1.2
+-----
+
+**February 29, 2024**
+
+### Bug fixes
+
+- On Linux, we now check for supported `io_uring` operations before using them, which should fix `bfs` on 5.X series kernels that support `io_uring` but not all of `openat()`/`close()`/`statx()` ([`8bc72d6`](https://github.com/tavianator/bfs/commit/8bc72d6c20c5e38783c4956c4d9fde9b3ee9140c))
+
+- Fixed a test failure triggered by certain filesystem types for `/tmp` ([#131](https://github.com/tavianator/bfs/issues/131))
+
+- Fixed parsing and interpretation of timezone offsets for explicit reference times used in `-*since` and `-newerXt` ([`a9f3cde`](https://github.com/tavianator/bfs/commit/a9f3cde30426b546ba6e3172e1a7951213a72049))
+
+- Fixed the build on m68k ([`c749c11`](https://github.com/tavianator/bfs/commit/c749c11b04444ca40941dd2ddc5802faed148f6a))
+
+
+3.1.1
+-----
+
+**February 16, 2024**
+
+### Changes
+
+- Performance and scalability improvements
+
+- The file count in `bfs -status` now has a thousands separator
+
+
+3.1
+---
+
+**February 6, 2024**
+
+### New features
+
+- On Linux, `bfs` now uses [io_uring](https://en.wikipedia.org/wiki/Io_uring) for async I/O
+
+- On all platforms, `bfs` can now perform `stat()` calls in parallel, accelerating queries like `-links`, `-newer`, and `-size`, as well as colorized output
+
+- On FreeBSD, `-type w` now works to find whiteouts like the system `find`
+
+### Changes
+
+- Improved `bfs -j2` performance ([`b2ab7a1`](https://github.com/tavianator/bfs/commit/b2ab7a151fca517f4879e76e626ec85ad3de97c7))
+
+- Optimized `-exec` by using `posix_spawn()` when possible, which can avoid the overhead of `fork()` ([`95fbde1`](https://github.com/tavianator/bfs/commit/95fbde17a66377b6fbe7ff1f014301dbbf09270d))
+
+- `-execdir` and `-okdir` are now rejected if `$PATH` contains a relative path, matching the behaviour of GNU find ([`163baf1`](https://github.com/tavianator/bfs/commit/163baf1c9af13be0ce705b133e41e0c3d6427398))
+
+- Leading whitespace is no longer accepted in integer command line arguments like `-links ' 1'` ([`e0d7dc5`](https://github.com/tavianator/bfs/commit/e0d7dc5dfd7bdaa62b6bc18e9c1cce00bbe08577))
+
+### Bug fixes
+
+- `-quit` and `-exit` could be ignored in the iterative deepening modes (`-S {ids,eds}`).
+ This is now fixed ([`670ebd9`](https://github.com/tavianator/bfs/commit/670ebd97fb431e830b1500b2e7e8013b121fb2c5)).
+ The bug was introduced in version 3.0.3 (commit [`5f16169`]).
+
+- Fixed two possible errors in sort mode (`-s`):
+ - Too many open files ([`710c083`](https://github.com/tavianator/bfs/commit/710c083ff02eb1cc5b8daa6778784f3d1cd3c08d))
+ - Out of memory ([`76ffc8d`](https://github.com/tavianator/bfs/commit/76ffc8d30cb1160d55d855d8ac630a2b9075fbcf))
+
+- Fixed handling of FreeBSD union mounts ([`3ac3bee`](https://github.com/tavianator/bfs/commit/3ac3bee7b0d9c9be693415206efa664bf4a7d4a7))
+
+- Fixed `NO_COLOR` handling when it's set to the empty string ([`79aee58`](https://github.com/tavianator/bfs/commit/79aee58a4621d01c4b1e98c332775f3b87213ddb))
+
+- Fixed some portability issues:
+ - [OpenBSD](https://github.com/tavianator/bfs/compare/ee200c07643801c8b53e5b80df704ecbf77a884e...79f1521b0e628be72bed3a648f0ae90b62fc69b8)
+ - [NetBSD](https://github.com/tavianator/bfs/compare/683f2c41c72efcb82ce866e3dcc311ac9bd8b66d...6435684a7d515e18247ae1b3dd9ec8681fee22d0)
+ - [DragonFly BSD](https://github.com/tavianator/bfs/compare/08867473e75e8e20ca76c7fb181204839e28b271...45fb1d952c3b262278a3b22e9c7d60cca19a5407)
+ - [Illumos](https://github.com/tavianator/bfs/compare/4010140cb748cc4f7f57b0a3d514485796c665ce...ae94cdc00136685abe61d55e1e357caaa636d785)
+
+
+3.0.4
+-----
+
+**October 12, 2023**
+
+### Bug fixes
+
+- Fixed a segfault when reporting errors under musl ([`d40eb87`])
+
+[`d40eb87`]: https://github.com/tavianator/bfs/commit/d40eb87cc00f50a5debb8899eacb7fcf1065badf
+
+
+3.0.3
+-----
+
+**October 12, 2023**
+
+### Changes
+
+- Iterative deepening modes (`-S {ids,eds}`) were optimized by delaying teardown until the very end ([`5f16169`])
+
+- Parallel depth-first search (`-S dfs`) was optimized to avoid enqueueing every file separately ([`2572273`])
+
+### Bug fixes
+
+- Iterative deepening modes (`-S {ids,eds}`) were performing iterative *breadth*-first searches since `bfs` 3.0, negating any advantages they may have had over normal breadth-first search.
+ They now do iterative *depth*-first searches as expected.
+ ([`a029d95`])
+
+- Fixed a linked-list corruption that could lead to an infinite loop on macOS and other non-Linux, non-FreeBSD platforms ([`773f4a4`])
+
+[`5f16169`]: https://github.com/tavianator/bfs/commit/5f1616912ba3a7a23ce6bce02df3791b73da38ab
+[`2572273`]: https://github.com/tavianator/bfs/commit/257227326fe60fe70e80433fd34d1ebcb2f9f623
+[`a029d95`]: https://github.com/tavianator/bfs/commit/a029d95b5736a74879f32089514a5a6b63d6efbc
+[`773f4a4`]: https://github.com/tavianator/bfs/commit/773f4a446f03da62d88e6d17be49fdc0a3e38465
+
+
+3.0.2
+-----
+
+**September 6, 2023**
+
+### Changes
+
+- `-files0-from` now allows an empty set of paths to be given, matching GNU findutils 4.9.0
+
+- Reduced memory consumption in multi-threaded searches
+
+- Many man page updates
+
+### Bug fixes
+
+- Fixed an out-of-bounds memory read that could occur when escaping a string containing an incomplete multi-byte character
+
+
+3.0.1
+-----
+
+**July 18, 2023**
+
+### Bug fixes
+
+- Traversal fixes that mostly affect large directory trees ([#107])
+
+ - `bfs` could encounter `EMFILE`, close a file, and retry many times, particularly with `-j1`
+
+ - Breadth-first search could become highly unbalanced, negating many of the benefits of `bfs`
+
+ - On non-{Linux,FreeBSD} plaforms, directories could stay open longer than necessary, consuming extra memory
+
+[#107]: https://github.com/tavianator/bfs/pull/107
+
+
+3.0
+---
+
+**July 13, 2023**
+
+### New features
+
+- `bfs` now reads directories asynchronously and in parallel ([#101]).
+ Performance is significantly improved as a result.
+ Parallelism is controlled by the new `-j` flag, e.g. `-j1`, `-j2`, etc.
+
+[#101]: https://github.com/tavianator/bfs/issues/101
+
+### Changes
+
+- `bfs` now uses the [C17] standard version, up from C11
+
+- Due to [#101], `bfs` now requires some additional C and POSIX features:
+ - [Standard C atomics] (`<stdatomic.h>`)
+ - [POSIX threads] (`<pthread.h>`)
+
+- `$LS_COLORS` extensions written in different cases (e.g. `*.jpg=35:*.JPG=01;35`) are now matched case-sensitively, to match the new behaviour of GNU ls since coreutils version 9.2
+
+- Added a warning/error if `$LS_COLORS` can't be parsed, depending on whether `-color` is requested explicitly
+
+- Filenames with control characters are now escaped when printing with `-color`
+
+- Build flags like `WITH_ONIGURUMA` have been renamed to `USE_ONIGURUMA`
+
+[C17]: https://en.cppreference.com/w/c/17
+[Standard C atomics]: https://en.cppreference.com/w/c/atomic
+[POSIX threads]: https://pubs.opengroup.org/onlinepubs/9699919799/idx/threads.html
+
+### Bug fixes
+
+- Fixed handling of the "normal text" color (`no` in `$LS_COLORS`) to match GNU ls
+
+
2.*
===
+2.6.3
+-----
+
+**January 31, 2023**
+
+- Fixed running the tests as root on Linux [`8b24de3`]
+
+- Fixed some tests on Android [`2724dfb`] [`0a5a80c`]
+
+- Stopped relying on non-POSIX touch(1) features in the tests.
+ This should fix the tests on at least OpenBSD.
+ [`2d5edb3`]
+
+- User/group caches are now filled lazily instead of eagerly [`b41dca5`]
+
+- More caches and I/O streams are flushed before -exec/-ok [`f98a1c4`]
+
+- Fixed various memory safety issues found by fuzzing \
+ [`712b137`] [`5ce883d`] [`da02def`] [`c55e855`]
+
+- Fixed a test failure on certain macOS versions [`8b24de3`]
+
+- Mitigated a race condition when determining filesystem types ([#97])
+
+- Lots of refactoring and optimization
+
+[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559
+[`2724dfb`]: https://github.com/tavianator/bfs/commit/2724dfbd17552f892a0d8b39b96cbe9e49d66fdb
+[`0a5a80c`]: https://github.com/tavianator/bfs/commit/0a5a80c98cc7e5d8735b615fa197a6cff2bb08cc
+[`2d5edb3`]: https://github.com/tavianator/bfs/commit/2d5edb37b924715b4fbee4d917ac334c773fca61
+[`b41dca5`]: https://github.com/tavianator/bfs/commit/b41dca52762c5188638236ae81b9f4597bb29ac9
+[`f98a1c4`]: https://github.com/tavianator/bfs/commit/f98a1c4a1cf61ff7d6483388ca1fac365fb0b31b
+[`712b137`]: https://github.com/tavianator/bfs/commit/712b13756a09014ef730c8f9b96da4dc2f09b762
+[`5ce883d`]: https://github.com/tavianator/bfs/commit/5ce883daaafc69f83b01dac5db0647e9662a6e87
+[`da02def`]: https://github.com/tavianator/bfs/commit/da02defb91c3a1bda0ea7e653d81f997f1c8884a
+[`c55e855`]: https://github.com/tavianator/bfs/commit/c55e85580df10c5afdc6fc0710e756a456aa8e93
+[`8b24de3`]: https://github.com/tavianator/bfs/commit/8b24de3882ff5a3e33b82ab20bb4eadf134cf559
+[#97]: https://github.com/tavianator/bfs/issues/97
+
+
+2.6.2
+-----
+
+**October 21, 2022**
+
+- Fixed use of uninitialized memory on parsing errors involving `-fprintf`
+
+- Fixed Android build issues ([#96])
+
+- Refactored the test suite
+
+[#96]: https://github.com/tavianator/bfs/issues/96
+
+
2.6.1
-----
diff --git a/docs/HACKING.md b/docs/CONTRIBUTING.md
index 08ddac2..099157d 100644
--- a/docs/HACKING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,5 +1,5 @@
-Hacking on `bfs`
-================
+Contributing to `bfs`
+=====================
License
-------
@@ -7,11 +7,17 @@ License
`bfs` is licensed under the [Zero-Clause BSD License](https://opensource.org/licenses/0BSD), a maximally permissive license.
Contributions must use the same license.
+Individual files contain the following tag instead of the full license text:
+
+ SPDX-License-Identifier: 0BSD
+
+This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/
+
Implementation
--------------
-`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C11](https://en.wikipedia.org/wiki/C11_(C_standard_revision)).
+`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision)).
You can get a feel for the coding style by skimming the source code.
[`main.c`](/src/main.c) contains an overview of the rest of source files.
A quick summary:
@@ -30,18 +36,26 @@ Tests
`bfs` includes an extensive test suite.
See the [build documentation](BUILDING.md#testing) for details on running the tests.
+Test cases are grouped by the standard or `find` implementation that supports the tested feature(s):
+
+| Group | Description |
+|---------------------------------|---------------------------------------|
+| [`tests/posix`](/tests/posix) | POSIX compatibility tests |
+| [`tests/bsd`](/tests/bsd) | BSD `find` features |
+| [`tests/gnu`](/tests/gnu) | GNU `find` features |
+| [`tests/common`](/tests/common) | Features common to BSD and GNU `find` |
+| [`tests/bfs`](/tests/bfs) | `bfs`-specific tests |
+
Both new features and bug fixes should have associated tests.
-To add a test, create a new function in `tests.sh` called `test_<something>`.
+To add a test, create a new `*.sh` file in the appropriate group.
Snapshot tests use the `bfs_diff` function to automatically compare the generated and expected outputs.
For example,
```bash
-function test_something() {
- bfs_diff basic -name something
-}
+# posix/something.sh
+bfs_diff basic -name something
```
`basic` is one of the directory trees generated for test cases; others include `links`, `loops`, `deep`, and `rainbow`.
-Run `./tests/tests.sh test_something --update` to generate the reference snapshot (and don't forget to `git add` it).
-Finally, add the test case to one of the arrays `posix_tests`, `bsd_tests`, `gnu_tests`, or `bfs_tests`, depending on which `find` implementations it should be compatible with.
+Run `./tests/tests.sh posix/something --update` to generate the reference snapshot (and don't forget to `git add` it).
diff --git a/docs/RELATED.md b/docs/RELATED.md
new file mode 100644
index 0000000..cf52b70
--- /dev/null
+++ b/docs/RELATED.md
@@ -0,0 +1,42 @@
+# Related utilities
+
+There are many tools that can be used to find files.
+This is a catalogue of some of the most important/interesting ones.
+
+## `find`-compatible
+
+### System `find` implementations
+
+These `find` implementations are commonly installed as the system `find` utility in UNIX-like operating systems:
+
+- [GNU findutils](https://www.gnu.org/software/findutils/) ([manual](https://www.gnu.org/software/findutils/manual/html_node/find_html/index.html), [source](https://git.savannah.gnu.org/cgit/findutils.git))
+- BSD `find`
+ - FreeBSD `find` ([manual](https://www.freebsd.org/cgi/man.cgi?find(1)), [source](https://cgit.freebsd.org/src/tree/usr.bin/find))
+ - OpenBSD `find` ([manual](https://man.openbsd.org/find.1), [source](https://cvsweb.openbsd.org/src/usr.bin/find/))
+ - NetBSD `find` ([manual](https://man.netbsd.org/find.1), [source](http://cvsweb.netbsd.org/bsdweb.cgi/src/usr.bin/find/))
+- macOS `find` ([manual](https://ss64.com/osx/find.html), [source](https://github.com/apple-oss-distributions/shell_cmds/tree/main/find))
+- Solaris `find`
+ - [Illumos](https://illumos.org/) `find` ([manual](https://illumos.org/man/1/find), [source](https://github.com/illumos/illumos-gate/blob/master/usr/src/cmd/find/find.c))
+
+### Alternative `find` implementations
+
+These are not usually installed as the system `find`, but are designed to be `find`-compatible
+
+- [`bfs`](https://tavianator.com/projects/bfs.html) ([manual](https://man.archlinux.org/man/bfs.1), [source](https://github.com/tavianator/bfs))
+- [schilytools](https://codeberg.org/schilytools/schilytools) `sfind` ([source](https://codeberg.org/schilytools/schilytools/src/branch/master/sfind))
+- [BusyBox](https://busybox.net/) `find` ([manual](https://busybox.net/downloads/BusyBox.html#find), [source](https://git.busybox.net/busybox/tree/findutils/find.c))
+- [ToyBox](http://landley.net/toybox/) `find` ([manual](http://landley.net/toybox/help.html#find), [source](https://github.com/landley/toybox/blob/master/toys/posix/find.c))
+- uutils `find` ([source](https://github.com/uutils/findutils))
+
+## `find` alternatives
+
+These utilities are not `find`-compatible, but serve a similar purpose:
+
+- [`fd`](https://github.com/sharkdp/fd): A simple, fast and user-friendly alternative to 'find'
+- `locate`
+ - [GNU `locate`](https://www.gnu.org/software/findutils/locate)
+ - [`mlocate`](https://pagure.io/mlocate) ([manual](), [source](https://pagure.io/mlocate/tree/master))
+ - [`plocate`](https://plocate.sesse.net/) ([manual](https://plocate.sesse.net/plocate.1.html), [source](https://git.sesse.net/?p=plocate))
+- [`walk`](https://github.com/google/walk): Plan 9 style utilities to replace find(1)
+- [fselect](https://github.com/jhspetersson/fselect): Find files with SQL-like queries
+- [rawhide](https://github.com/raforg/rawhide): find files using pretty C expressions
diff --git a/docs/USAGE.md b/docs/USAGE.md
index e2cff44..70f8475 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -18,10 +18,9 @@ $ bfs
./completions/bfs.zsh
./docs/BUILDING.md
./docs/CHANGELOG.md
-./docs/HACKING.md
+./docs/CONTRIBUTING.md
./docs/USAGE.md
./docs/bfs.1
-./src/bfs.h
...
```
@@ -54,7 +53,7 @@ $ bfs -name '*.md'
./README.md
./docs/BUILDING.md
./docs/CHANGELOG.md
-./docs/HACKING.md
+./docs/CONTRIBUTING.md
./docs/USAGE.md
```
@@ -65,7 +64,7 @@ When you put multiple expressions next to each other, both of them must match:
```console
$ bfs -name '*.md' -name '*ING*'
./docs/BUILDING.md
-./docs/HACKING.md
+./docs/CONTRIBUTING.md
```
This works because the expressions are implicitly combined with *logical and*.
@@ -78,17 +77,16 @@ $ bfs -name '*.md' -and -name '*ING'`
There are other operators like `-or`:
```console
-$ bfs -name '*.md' -or -name '*.sh'
+$ bfs -name '*.md' -or -name 'bfs.*'
./README.md
-./tests/find-color.sh
-./tests/ls-color.sh
-./tests/remove-sibling.sh
-./tests/sort-args.sh
-./tests/tests.sh
-./docs/CHANGELOG.md
-./docs/HACKING.md
+./completions/bfs.bash
+./completions/bfs.fish
+./completions/bfs.zsh
./docs/BUILDING.md
+./docs/CHANGELOG.md
+./docs/CONTRIBUTING.md
./docs/USAGE.md
+./docs/bfs.1
```
and `-not`:
@@ -132,6 +130,40 @@ Unlike `-prune`, `-exclude` even works in combination with `-depth`/`-delete`.
---
+### `-limit`
+
+The `-limit N` action makes `bfs` quit once it gets evaluated `N` times.
+Placing it after an action like `-print` limits the number of results that get printed, for example:
+
+```console
+$ bfs -s -type f -name '*.txt'
+./1.txt
+./2.txt
+./3.txt
+./4.txt
+$ bfs -s -type f -name '*.txt' -print -limit 2
+./1.txt
+./2.txt
+```
+
+This is similar to
+
+```console
+$ bfs -s -type f -name '*.txt' | head -n2
+```
+
+but more powerful because you can apply separate limits to different expressions:
+
+```console
+$ bfs \( -name '*.txt' -print -limit 3 -o -name '*.log' -print -limit 4 \) -limit 5
+[At most 3 .txt files, at most 4 .log files, and at most 5 in total]
+```
+
+and more efficient because it will quit immediately.
+When piping to `head`, `bfs` will only quit *after* it tries to output too many results.
+
+---
+
### `-hidden`/`-nohidden`
`-hidden` matches "hidden" files (dotfiles).
diff --git a/docs/bfs.1 b/docs/bfs.1
index 53a9831..54166ab 100644
--- a/docs/bfs.1
+++ b/docs/bfs.1
@@ -90,7 +90,7 @@ Follow all symbolic links.
Never follow symbolic links (the default).
.TP
.B \-E
-Use extended regular expressions (same as \fB\-regextype posix-extended\fR).
+Use extended regular expressions (same as \fB\-regextype \fIposix-extended\fR).
.TP
.B \-X
Filter out files with
@@ -171,12 +171,18 @@ consumes too much memory.
.TP
.I eds
Exponential deepening search.
-A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g 0-1, 1-2, 2-4, 4-8, etc.).
+A compromise between breadth- and depth-first search, which searches exponentially increasing depth ranges (e.g. 0-1, 1-2, 2-4, 4-8, etc.).
Provides many of the benefits of breadth-first search with depth-first's reduced memory consumption.
Typically far faster than
.B \-S
.IR ids .
.RE
+.TP
+\fB\-j\fIN\fR
+Search with
+.I N
+threads in parallel (default: number of CPUs, up to
+.IR 8 ).
.SH OPERATORS
.TP
\fB( \fIexpression \fB)\fR
@@ -245,6 +251,20 @@ or
.B \-mindepth
for example.
Exclusions are always applied before other expressions, so it may be least confusing to put them first on the command line.
+.PP
+.B \-help
+.br
+.B \-\-help
+.RS
+Print usage information, and exit immediately (without parsing the rest of the command line or processing any files).
+.RE
+.PP
+.B \-version
+.br
+.B \-\-version
+.RS
+Print version information, and exit immediately.
+.RE
.SH OPTIONS
.PP
.B \-color
@@ -309,11 +329,40 @@ Ignored; for compatibility with GNU find.
\fB\-regextype \fITYPE\fR
Use
.IR TYPE -flavored
-regexes (default:
-.IR posix-basic ;
-see
-.B \-regextype
-.IR help ).
+regular expressions.
+The possible types are
+.RS
+.TP
+.I posix-basic
+POSIX basic regular expressions (the default).
+.TP
+.I posix-extended
+POSIX extended resular expressions.
+.TP
+.I ed
+Like
+.BR ed (1)
+(same as
+.IR posix-basic ).
+.TP
+.I emacs
+Like
+.BR emacs (1).
+.TP
+.I grep
+Like
+.BR grep (1).
+.TP
+.I sed
+Like
+.BR sed (1)
+(same as
+.IR posix-basic ).
+.PP
+See
+.BR regex (7)
+for a description of regular expression syntax.
+.RE
.TP
.B \-status
Display a status bar while searching.
@@ -403,6 +452,10 @@ Find files with POSIX.1e
.BR capabilities (7)
set.
.TP
+\fB\-context \fIGLOB\fR
+Find files whose SELinux context matches the
+.IR GLOB .
+.TP
\fB\-depth\fR [\fI\-+\fR]\fIN\fR
Find files with depth
.IR N .
@@ -426,9 +479,9 @@ Find files the current user can execute/read/write.
Always false/true.
.RE
.TP
-.B \-fstype TYPE
+\fB\-fstype \fITYPE\fR
Find files on file systems with the given
-.BR TYPE .
+.IR TYPE .
.PP
\fB\-gid\fR [\fI\-+\fR]\fIN\fR
.br
@@ -506,13 +559,12 @@ to parse
as an ISO 8601-style timestamp. For example:
.PP
.RS
-1991-12-14
-.br
-1991-12-14T03:00
-.br
-1991-12-14T03:00-07:00
-.br
-1991-12-14T10:00Z
+.nf
+\(bu \fI1991-12-14\fR
+\(bu \fI1991-12-14T03:00\fR
+\(bu \fI1991-12-14T03:00-07:00\fR
+\(bu '\fI1991-12-14 10:00Z\fR'
+.fi
.RE
.PP
.B \-nogroup
@@ -549,35 +601,67 @@ See
for examples of the timestamp format.
.TP
\fB\-size\fR [\fI\-+\fR]\fIN\fR[\fIcwbkMGTP\fR]
-Find files with the given size, in 1-byte
-.IR c haracters,
-2-byte
-.IR w ords,
-512-byte
-.IR b locks
-(default), or
-.IR k iB/ M iB/ G iB/ T iB/ P iB.
+Find files with the given size.
+The unit can be one of
+.PP
+.RS
+.nf
+\(bu \fIc\fRhars (1 byte)
+\(bu \fIw\fRords (2 bytes)
+\(bu \fIb\fRlocks (512 bytes, the default)
+\(bu \fIk\fRiB (1024 bytes)
+\(bu \fIM\fRiB (1024 kiB)
+\(bu \fIG\fRiB (1024 MiB)
+\(bu \fIT\fRiB (1024 GiB)
+\(bu \fIP\fRiB (1024 TiB)
+.fi
+.RE
.TP
.B \-sparse
Find files that occupy fewer disk blocks than expected.
.TP
\fB\-type\fR [\fIbcdlpfswD\fR]
Find files of the given type.
-Possible types are
+The possible types are
+.PP
+.RS
+\(bu
.IR b lock
-device,
+device
+.br
+\(bu
.IR c haracter
-device,
-.IR d irectory,
-symbolic
-.IR l ink,
-.IR p ipe,
-regular
-.IR f ile,
-.IR s ocket,
-.IR w hiteout,
-and
-.IR D oor.
+device
+.br
+\(bu
+.IR d irectory
+.br
+\(bu
+.IR l ink
+(symbolic)
+.br
+\(bu
+.IR p ipe
+.br
+\(bu
+.IR f ile
+(regular)
+.br
+\(bu
+.IR s ocket
+.br
+\(bu
+.IR w hiteout
+.br
+\(bu
+.IR D oor
+.PP
+Multiple types can be given at once, separated by commas.
+For example,
+.B \-type
+.I d,f
+matches both directories and regular files.
+.RE
.TP
\fB\-used\fR [\fI\-+\fR]\fIN\fR
Find files last accessed
@@ -626,7 +710,9 @@ but run the command in the same directory as the found file(s).
.RE
.TP
\fB\-exit\fR [\fISTATUS\fR]
-Exit immediately with the given status (0 if unspecified).
+Exit immediately with the given status
+.RI ( 0
+if unspecified).
.PP
\fB\-fls \fIFILE\fR
.br
@@ -643,6 +729,11 @@ but write to
instead of standard output.
.RE
.TP
+\fB\-limit \fIN\fR
+Quit once this action is evaluated
+.I N
+times.
+.TP
.B \-ls
List files like
.B ls
@@ -689,15 +780,16 @@ instead.
.TP
.B \-prune
Don't descend into this directory.
+This has no effect if
+.B \-depth
+is enabled (either explicitly, or implicitly by
+.BR \-delete ).
+Use
+.B \-exclude
+instead in that case.
.TP
.B \-quit
Quit immediately.
-.TP
-.B \-version
-Print version information.
-.TP
-.B \-help
-Print usage information.
.SH ENVIRONMENT
Certain environment variables affect the behavior of
.BR bfs .
@@ -748,17 +840,48 @@ Specifies the pager used for
.B \-help
output.
Defaults to
+.BR less (1),
+if found on the current
+.BR PATH ,
+otherwise
.BR more (1).
.TP
+.B PATH
+Used to resolve executables for
+.BR \-exec [ dir ]
+and
+.BR \-ok [ dir ].
+.TP
.B POSIXLY_CORRECT
Makes
.B bfs
conform more strictly to the POSIX.1-2017 specification for
.BR find (1).
-Currently this just disables warnings by default.
+Currently this has two effects:
+.RS
+.IP \(bu
+Disables warnings by default, because POSIX prohibits writing to standard error (except for the
+.B \-ok
+prompt), unless the command also fails with a non-zero exit status.
+.IP \(bu
+Makes
+.B \-ls
+and
+.B \-fls
+use 512-byte blocks instead of 1024-byte blocks.
+(POSIX does not specify these actions, but BSD
+.BR find (1)
+implementations use 512-byte blocks, while GNU
+.BR find (1)
+uses 1024-byte blocks by default.)
+.PP
It does not disable
.BR bfs 's
various extensions to the base POSIX functionality.
+.B POSIXLY_CORRECT
+has the same effects on GNU
+.BR find (1).
+.RE
.SH EXAMPLES
.TP
.B bfs
@@ -795,7 +918,7 @@ skipping every
.B .git
directory.
.TP
-.B bfs \-type f \-executable \-exec strip '{}' +
+.B bfs \-type f \-executable \-exec strip {} +
Runs
.BR strip (1)
on all executable files it finds, passing it multiple files at a time.
diff --git a/src/alloc.c b/src/alloc.c
new file mode 100644
index 0000000..ebaff38
--- /dev/null
+++ b/src/alloc.c
@@ -0,0 +1,384 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "alloc.h"
+#include "bit.h"
+#include "diag.h"
+#include "sanity.h"
+#include <errno.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+/** The largest possible allocation size. */
+#if PTRDIFF_MAX < SIZE_MAX / 2
+# define ALLOC_MAX ((size_t)PTRDIFF_MAX)
+#else
+# define ALLOC_MAX (SIZE_MAX / 2)
+#endif
+
+/** Portable aligned_alloc()/posix_memalign(). */
+static void *xmemalign(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(align >= sizeof(void *));
+ bfs_assert(is_aligned(align, size));
+
+#if BFS_HAS_ALIGNED_ALLOC
+ return aligned_alloc(align, size);
+#else
+ void *ptr = NULL;
+ errno = posix_memalign(&ptr, align, size);
+ return ptr;
+#endif
+}
+
+void *alloc(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(is_aligned(align, size));
+
+ if (size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return malloc(size);
+ } else {
+ return xmemalign(align, size);
+ }
+}
+
+void *zalloc(size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(is_aligned(align, size));
+
+ if (size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return calloc(1, size);
+ }
+
+ void *ret = xmemalign(align, size);
+ if (ret) {
+ memset(ret, 0, size);
+ }
+ return ret;
+}
+
+void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(is_aligned(align, old_size));
+ bfs_assert(is_aligned(align, new_size));
+
+ if (new_size == 0) {
+ free(ptr);
+ return NULL;
+ } else if (new_size > ALLOC_MAX) {
+ errno = EOVERFLOW;
+ return NULL;
+ }
+
+ if (align <= alignof(max_align_t)) {
+ return realloc(ptr, new_size);
+ }
+
+ // There is no aligned_realloc(), so reallocate and copy manually
+ void *ret = xmemalign(align, new_size);
+ if (!ret) {
+ return NULL;
+ }
+
+ size_t min_size = old_size < new_size ? old_size : new_size;
+ if (min_size) {
+ memcpy(ret, ptr, min_size);
+ }
+
+ free(ptr);
+ return ret;
+}
+
+void *reserve(void *ptr, size_t align, size_t size, size_t count) {
+ // No need to overflow-check the current size
+ size_t old_size = size * count;
+
+ // Capacity is doubled every power of two, from 0→1, 1→2, 2→4, etc.
+ // If we stayed within the same size class, re-use ptr.
+ if (count & (count - 1)) {
+ // Tell sanitizers about the new array element
+ sanitize_alloc((char *)ptr + old_size, size);
+ errno = 0;
+ return ptr;
+ }
+
+ // No need to overflow-check; xrealloc() will fail before we overflow
+ size_t new_size = count ? 2 * old_size : size;
+ void *ret = xrealloc(ptr, align, old_size, new_size);
+ if (!ret) {
+ // errno is used to communicate success/failure to the RESERVE() macro
+ bfs_assert(errno != 0);
+ return ptr;
+ }
+
+ // Pretend we only allocated one more element
+ sanitize_free((char *)ret + old_size + size, new_size - old_size - size);
+ errno = 0;
+ return ret;
+}
+
+/**
+ * An arena allocator chunk.
+ */
+union chunk {
+ /**
+ * Free chunks are stored in a singly linked list. The pointer to the
+ * next chunk is represented by an offset from the chunk immediately
+ * after this one in memory, so that zalloc() correctly initializes a
+ * linked list of chunks (except for the last one).
+ */
+ uintptr_t next;
+
+ // char object[];
+};
+
+/** Decode the next chunk. */
+static union chunk *chunk_next(const struct arena *arena, const union chunk *chunk) {
+ uintptr_t base = (uintptr_t)chunk + arena->size;
+ return (union chunk *)(base + chunk->next);
+}
+
+/** Encode the next chunk. */
+static void chunk_set_next(const struct arena *arena, union chunk *chunk, union chunk *next) {
+ uintptr_t base = (uintptr_t)chunk + arena->size;
+ chunk->next = (uintptr_t)next - base;
+}
+
+void arena_init(struct arena *arena, size_t align, size_t size) {
+ bfs_assert(has_single_bit(align));
+ bfs_assert(is_aligned(align, size));
+
+ if (align < alignof(union chunk)) {
+ align = alignof(union chunk);
+ }
+ if (size < sizeof(union chunk)) {
+ size = sizeof(union chunk);
+ }
+ bfs_assert(is_aligned(align, size));
+
+ arena->chunks = NULL;
+ arena->nslabs = 0;
+ arena->slabs = NULL;
+ arena->align = align;
+ arena->size = size;
+}
+
+/** Allocate a new slab. */
+attr(cold)
+static int slab_alloc(struct arena *arena) {
+ // Make the initial allocation size ~4K
+ size_t size = 4096;
+ if (size < arena->size) {
+ size = arena->size;
+ }
+ // Trim off the excess
+ size -= size % arena->size;
+ // Double the size for every slab
+ size <<= arena->nslabs;
+
+ // Allocate the slab
+ void *slab = zalloc(arena->align, size);
+ if (!slab) {
+ return -1;
+ }
+
+ // Grow the slab array
+ void **pslab = RESERVE(void *, &arena->slabs, &arena->nslabs);
+ if (!pslab) {
+ free(slab);
+ return -1;
+ }
+
+ // Fix the last chunk->next offset
+ void *last = (char *)slab + size - arena->size;
+ chunk_set_next(arena, last, arena->chunks);
+
+ // We can rely on zero-initialized slabs, but others shouldn't
+ sanitize_uninit(slab, size);
+
+ arena->chunks = *pslab = slab;
+ return 0;
+}
+
+void *arena_alloc(struct arena *arena) {
+ if (!arena->chunks && slab_alloc(arena) != 0) {
+ return NULL;
+ }
+
+ union chunk *chunk = arena->chunks;
+ sanitize_alloc(chunk, arena->size);
+
+ sanitize_init(chunk);
+ arena->chunks = chunk_next(arena, chunk);
+ sanitize_uninit(chunk, arena->size);
+
+ return chunk;
+}
+
+void arena_free(struct arena *arena, void *ptr) {
+ union chunk *chunk = ptr;
+ chunk_set_next(arena, chunk, arena->chunks);
+ arena->chunks = chunk;
+ sanitize_free(chunk, arena->size);
+}
+
+void arena_clear(struct arena *arena) {
+ for (size_t i = 0; i < arena->nslabs; ++i) {
+ free(arena->slabs[i]);
+ }
+ free(arena->slabs);
+
+ arena->chunks = NULL;
+ arena->nslabs = 0;
+ arena->slabs = NULL;
+}
+
+void arena_destroy(struct arena *arena) {
+ arena_clear(arena);
+ sanitize_uninit(arena);
+}
+
+void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size) {
+ varena->align = align;
+ varena->offset = offset;
+ varena->size = size;
+ varena->narenas = 0;
+ varena->arenas = NULL;
+
+ // The smallest size class is at least as many as fit in the smallest
+ // aligned allocation size
+ size_t min_count = (flex_size(align, min, offset, size, 1) - offset + size - 1) / size;
+ varena->shift = bit_width(min_count - 1);
+}
+
+/** Get the size class for the given array length. */
+static size_t varena_size_class(struct varena *varena, size_t count) {
+ // Since powers of two are common array lengths, make them the
+ // (inclusive) upper bound for each size class
+ return bit_width((count - !!count) >> varena->shift);
+}
+
+/** Get the exact size of a flexible struct. */
+static size_t varena_exact_size(const struct varena *varena, size_t count) {
+ return flex_size(varena->align, 0, varena->offset, varena->size, count);
+}
+
+/** Get the arena for the given array length. */
+static struct arena *varena_get(struct varena *varena, size_t count) {
+ size_t i = varena_size_class(varena, count);
+
+ while (i >= varena->narenas) {
+ size_t j = varena->narenas;
+ struct arena *arena = RESERVE(struct arena, &varena->arenas, &varena->narenas);
+ if (!arena) {
+ return NULL;
+ }
+
+ size_t shift = j + varena->shift;
+ size_t size = varena_exact_size(varena, (size_t)1 << shift);
+ arena_init(arena, varena->align, size);
+ }
+
+ return &varena->arenas[i];
+}
+
+void *varena_alloc(struct varena *varena, size_t count) {
+ struct arena *arena = varena_get(varena, count);
+ if (!arena) {
+ return NULL;
+ }
+
+ void *ret = arena_alloc(arena);
+ if (!ret) {
+ return NULL;
+ }
+
+ // Tell the sanitizers the exact size of the allocated struct
+ sanitize_free(ret, arena->size);
+ sanitize_alloc(ret, varena_exact_size(varena, count));
+
+ return ret;
+}
+
+void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count) {
+ struct arena *new_arena = varena_get(varena, new_count);
+ struct arena *old_arena = varena_get(varena, old_count);
+ if (!new_arena) {
+ return NULL;
+ }
+
+ size_t new_exact_size = varena_exact_size(varena, new_count);
+ size_t old_exact_size = varena_exact_size(varena, old_count);
+
+ if (new_arena == old_arena) {
+ if (new_count < old_count) {
+ sanitize_free((char *)ptr + new_exact_size, old_exact_size - new_exact_size);
+ } else if (new_count > old_count) {
+ sanitize_alloc((char *)ptr + old_exact_size, new_exact_size - old_exact_size);
+ }
+ return ptr;
+ }
+
+ void *ret = arena_alloc(new_arena);
+ if (!ret) {
+ return NULL;
+ }
+
+ size_t old_size = old_arena->size;
+ sanitize_alloc((char *)ptr + old_exact_size, old_size - old_exact_size);
+
+ size_t new_size = new_arena->size;
+ size_t min_size = new_size < old_size ? new_size : old_size;
+ memcpy(ret, ptr, min_size);
+
+ arena_free(old_arena, ptr);
+ sanitize_free((char *)ret + new_exact_size, new_size - new_exact_size);
+
+ return ret;
+}
+
+void *varena_grow(struct varena *varena, void *ptr, size_t *count) {
+ size_t old_count = *count;
+
+ // Round up to the limit of the current size class. If we're already at
+ // the limit, go to the next size class.
+ size_t new_shift = varena_size_class(varena, old_count + 1) + varena->shift;
+ size_t new_count = (size_t)1 << new_shift;
+
+ ptr = varena_realloc(varena, ptr, old_count, new_count);
+ if (ptr) {
+ *count = new_count;
+ }
+ return ptr;
+}
+
+void varena_free(struct varena *varena, void *ptr, size_t count) {
+ struct arena *arena = varena_get(varena, count);
+ arena_free(arena, ptr);
+}
+
+void varena_clear(struct varena *varena) {
+ for (size_t i = 0; i < varena->narenas; ++i) {
+ arena_clear(&varena->arenas[i]);
+ }
+}
+
+void varena_destroy(struct varena *varena) {
+ for (size_t i = 0; i < varena->narenas; ++i) {
+ arena_destroy(&varena->arenas[i]);
+ }
+ free(varena->arenas);
+ sanitize_uninit(varena);
+}
diff --git a/src/alloc.h b/src/alloc.h
new file mode 100644
index 0000000..095134a
--- /dev/null
+++ b/src/alloc.h
@@ -0,0 +1,383 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Memory allocation.
+ */
+
+#ifndef BFS_ALLOC_H
+#define BFS_ALLOC_H
+
+#include "prelude.h"
+#include <errno.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+/** Check if a size is properly aligned. */
+static inline bool is_aligned(size_t align, size_t size) {
+ return (size & (align - 1)) == 0;
+}
+
+/** Round down to a multiple of an alignment. */
+static inline size_t align_floor(size_t align, size_t size) {
+ return size & ~(align - 1);
+}
+
+/** Round up to a multiple of an alignment. */
+static inline size_t align_ceil(size_t align, size_t size) {
+ return align_floor(align, size + align - 1);
+}
+
+/**
+ * Saturating array size.
+ *
+ * @param align
+ * Array element alignment.
+ * @param size
+ * Array element size.
+ * @param count
+ * Array element count.
+ * @return
+ * size * count, saturating to the maximum aligned value on overflow.
+ */
+static inline size_t array_size(size_t align, size_t size, size_t count) {
+ size_t ret = size * count;
+ return ret / size == count ? ret : ~(align - 1);
+}
+
+/** Saturating array sizeof. */
+#define sizeof_array(type, count) \
+ array_size(alignof(type), sizeof(type), count)
+
+/** Size of a struct/union field. */
+#define sizeof_member(type, member) \
+ sizeof(((type *)NULL)->member)
+
+/**
+ * Saturating flexible struct size.
+ *
+ * @param align
+ * Struct alignment.
+ * @param min
+ * Minimum struct size.
+ * @param offset
+ * Flexible array member offset.
+ * @param size
+ * Flexible array element size.
+ * @param count
+ * Flexible array element count.
+ * @return
+ * The size of the struct with count flexible array elements. Saturates
+ * to the maximum aligned value on overflow.
+ */
+static inline size_t flex_size(size_t align, size_t min, size_t offset, size_t size, size_t count) {
+ size_t ret = size * count;
+ size_t overflow = ret / size != count;
+
+ size_t extra = offset + align - 1;
+ ret += extra;
+ overflow |= ret < extra;
+ ret |= -overflow;
+ ret = align_floor(align, ret);
+
+ // Make sure flex_sizeof(type, member, 0) >= sizeof(type), even if the
+ // type has more padding than necessary for alignment
+ if (min > align_ceil(align, offset)) {
+ ret = ret < min ? min : ret;
+ }
+
+ return ret;
+}
+
+/**
+ * Computes the size of a flexible struct.
+ *
+ * @param type
+ * The type of the struct containing the flexible array.
+ * @param member
+ * The name of the flexible array member.
+ * @param count
+ * The length of the flexible array.
+ * @return
+ * The size of the struct with count flexible array elements. Saturates
+ * to the maximum aligned value on overflow.
+ */
+#define sizeof_flex(type, member, count) \
+ flex_size(alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0]), count)
+
+/**
+ * General memory allocator.
+ *
+ * @param align
+ * The required alignment.
+ * @param size
+ * The size of the allocation.
+ * @return
+ * The allocated memory, or NULL on failure.
+ */
+attr(malloc(free, 1), aligned_alloc(1, 2))
+void *alloc(size_t align, size_t size);
+
+/**
+ * Zero-initialized memory allocator.
+ *
+ * @param align
+ * The required alignment.
+ * @param size
+ * The size of the allocation.
+ * @return
+ * The allocated memory, or NULL on failure.
+ */
+attr(malloc(free, 1), aligned_alloc(1, 2))
+void *zalloc(size_t align, size_t size);
+
+/** Allocate memory for the given type. */
+#define ALLOC(type) \
+ (type *)alloc(alignof(type), sizeof(type))
+
+/** Allocate zeroed memory for the given type. */
+#define ZALLOC(type) \
+ (type *)zalloc(alignof(type), sizeof(type))
+
+/** Allocate memory for an array. */
+#define ALLOC_ARRAY(type, count) \
+ (type *)alloc(alignof(type), sizeof_array(type, count))
+
+/** Allocate zeroed memory for an array. */
+#define ZALLOC_ARRAY(type, count) \
+ (type *)zalloc(alignof(type), sizeof_array(type, count))
+
+/** Allocate memory for a flexible struct. */
+#define ALLOC_FLEX(type, member, count) \
+ (type *)alloc(alignof(type), sizeof_flex(type, member, count))
+
+/** Allocate zeroed memory for a flexible struct. */
+#define ZALLOC_FLEX(type, member, count) \
+ (type *)zalloc(alignof(type), sizeof_flex(type, member, count))
+
+/**
+ * Alignment-aware realloc().
+ *
+ * @param ptr
+ * The pointer to reallocate.
+ * @param align
+ * The required alignment.
+ * @param old_size
+ * The previous allocation size.
+ * @param new_size
+ * The new allocation size.
+ * @return
+ * The reallocated memory, or NULL on failure.
+ */
+attr(nodiscard, aligned_alloc(2, 4))
+void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size);
+
+/** Reallocate memory for an array. */
+#define REALLOC_ARRAY(type, ptr, old_count, new_count) \
+ (type *)xrealloc((ptr), alignof(type), sizeof_array(type, old_count), sizeof_array(type, new_count))
+
+/** Reallocate memory for a flexible struct. */
+#define REALLOC_FLEX(type, member, ptr, old_count, new_count) \
+ (type *)xrealloc((ptr), alignof(type), sizeof_flex(type, member, old_count), sizeof_flex(type, member, new_count))
+
+/**
+ * Reserve space for one more element in a dynamic array.
+ *
+ * @param ptr
+ * The pointer to reallocate.
+ * @param align
+ * The required alignment.
+ * @param count
+ * The current size of the array.
+ * @return
+ * The reallocated memory, on both success *and* failure. On success,
+ * errno will be set to zero, and the returned pointer will have room
+ * for (count + 1) elements. On failure, errno will be non-zero, and
+ * ptr will returned unchanged.
+ */
+attr(nodiscard)
+void *reserve(void *ptr, size_t align, size_t size, size_t count);
+
+/**
+ * Convenience macro to grow a dynamic array.
+ *
+ * @param type
+ * The array element type.
+ * @param type **ptr
+ * A pointer to the array.
+ * @param size_t *count
+ * A pointer to the array's size.
+ * @return
+ * On success, a pointer to the newly reserved array element, i.e.
+ * `*ptr + *count++`. On failure, NULL is returned, and both *ptr and
+ * *count remain unchanged.
+ */
+#define RESERVE(type, ptr, count) \
+ ((*ptr) = reserve((*ptr), alignof(type), sizeof(type), (*count)), \
+ errno ? NULL : (*ptr) + (*count)++)
+
+/**
+ * An arena allocator for fixed-size types.
+ *
+ * Arena allocators are intentionally not thread safe.
+ */
+struct arena {
+ /** The list of free chunks. */
+ void *chunks;
+ /** The number of allocated slabs. */
+ size_t nslabs;
+ /** The array of slabs. */
+ void **slabs;
+ /** Chunk alignment. */
+ size_t align;
+ /** Chunk size. */
+ size_t size;
+};
+
+/**
+ * Initialize an arena for chunks of the given size and alignment.
+ */
+void arena_init(struct arena *arena, size_t align, size_t size);
+
+/**
+ * Initialize an arena for the given type.
+ */
+#define ARENA_INIT(arena, type) \
+ arena_init((arena), alignof(type), sizeof(type))
+
+/**
+ * Free an object from the arena.
+ */
+void arena_free(struct arena *arena, void *ptr);
+
+/**
+ * Allocate an object out of the arena.
+ */
+attr(malloc(arena_free, 2))
+void *arena_alloc(struct arena *arena);
+
+/**
+ * Free all allocations from an arena.
+ */
+void arena_clear(struct arena *arena);
+
+/**
+ * Destroy an arena, freeing all allocations.
+ */
+void arena_destroy(struct arena *arena);
+
+/**
+ * An arena allocator for flexibly-sized types.
+ */
+struct varena {
+ /** The alignment of the struct. */
+ size_t align;
+ /** The offset of the flexible array. */
+ size_t offset;
+ /** The size of the flexible array elements. */
+ size_t size;
+ /** Shift amount for the smallest size class. */
+ size_t shift;
+ /** The number of arenas of different sizes. */
+ size_t narenas;
+ /** The array of differently-sized arenas. */
+ struct arena *arenas;
+};
+
+/**
+ * Initialize a varena for a struct with the given layout.
+ *
+ * @param varena
+ * The varena to initialize.
+ * @param align
+ * alignof(type)
+ * @param min
+ * sizeof(type)
+ * @param offset
+ * offsetof(type, flexible_array)
+ * @param size
+ * sizeof(flexible_array[i])
+ */
+void varena_init(struct varena *varena, size_t align, size_t min, size_t offset, size_t size);
+
+/**
+ * Initialize a varena for the given type and flexible array.
+ *
+ * @param varena
+ * The varena to initialize.
+ * @param type
+ * A struct type containing a flexible array.
+ * @param member
+ * The name of the flexible array member.
+ */
+#define VARENA_INIT(varena, type, member) \
+ varena_init(varena, alignof(type), sizeof(type), offsetof(type, member), sizeof_member(type, member[0]))
+
+/**
+ * Free an arena-allocated flexible struct.
+ *
+ * @param varena
+ * The that allocated the object.
+ * @param ptr
+ * The object to free.
+ * @param count
+ * The length of the flexible array.
+ */
+void varena_free(struct varena *varena, void *ptr, size_t count);
+
+/**
+ * Arena-allocate a flexible struct.
+ *
+ * @param varena
+ * The varena to allocate from.
+ * @param count
+ * The length of the flexible array.
+ * @return
+ * The allocated struct, or NULL on failure.
+ */
+attr(malloc(varena_free, 2))
+void *varena_alloc(struct varena *varena, size_t count);
+
+/**
+ * Resize a flexible struct.
+ *
+ * @param varena
+ * The varena to allocate from.
+ * @param ptr
+ * The object to resize.
+ * @param old_count
+ * The old array lenth.
+ * @param new_count
+ * The new array length.
+ * @return
+ * The resized struct, or NULL on failure.
+ */
+attr(nodiscard)
+void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count);
+
+/**
+ * Grow a flexible struct by an arbitrary amount.
+ *
+ * @param varena
+ * The varena to allocate from.
+ * @param ptr
+ * The object to resize.
+ * @param count
+ * Pointer to the flexible array length.
+ * @return
+ * The resized struct, or NULL on failure.
+ */
+attr(nodiscard)
+void *varena_grow(struct varena *varena, void *ptr, size_t *count);
+
+/**
+ * Free all allocations from a varena.
+ */
+void varena_clear(struct varena *varena);
+
+/**
+ * Destroy a varena, freeing all allocations.
+ */
+void varena_destroy(struct varena *varena);
+
+#endif // BFS_ALLOC_H
diff --git a/src/atomic.h b/src/atomic.h
new file mode 100644
index 0000000..f1a6bea
--- /dev/null
+++ b/src/atomic.h
@@ -0,0 +1,85 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Shorthand for standard C atomic operations.
+ */
+
+#ifndef BFS_ATOMIC_H
+#define BFS_ATOMIC_H
+
+#include <stdatomic.h>
+
+/**
+ * Prettier spelling of _Atomic.
+ */
+#define atomic _Atomic
+
+/**
+ * Shorthand for atomic_load_explicit().
+ *
+ * @param obj
+ * A pointer to the atomic object.
+ * @param order
+ * The memory ordering to use, without the memory_order_ prefix.
+ * @return
+ * The loaded value.
+ */
+#define load(obj, order) \
+ atomic_load_explicit(obj, memory_order_##order)
+
+/**
+ * Shorthand for atomic_store_explicit().
+ */
+#define store(obj, value, order) \
+ atomic_store_explicit(obj, value, memory_order_##order)
+
+/**
+ * Shorthand for atomic_exchange_explicit().
+ */
+#define exchange(obj, value, order) \
+ atomic_exchange_explicit(obj, value, memory_order_##order)
+
+/**
+ * Shorthand for atomic_compare_exchange_weak_explicit().
+ */
+#define compare_exchange_weak(obj, expected, desired, succ, fail) \
+ atomic_compare_exchange_weak_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail)
+
+/**
+ * Shorthand for atomic_compare_exchange_strong_explicit().
+ */
+#define compare_exchange_strong(obj, expected, desired, succ, fail) \
+ atomic_compare_exchange_strong_explicit(obj, expected, desired, memory_order_##succ, memory_order_##fail)
+
+/**
+ * Shorthand for atomic_fetch_add_explicit().
+ */
+#define fetch_add(obj, arg, order) \
+ atomic_fetch_add_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_sub_explicit().
+ */
+#define fetch_sub(obj, arg, order) \
+ atomic_fetch_sub_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_or_explicit().
+ */
+#define fetch_or(obj, arg, order) \
+ atomic_fetch_or_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_xor_explicit().
+ */
+#define fetch_xor(obj, arg, order) \
+ atomic_fetch_xor_explicit(obj, arg, memory_order_##order)
+
+/**
+ * Shorthand for atomic_fetch_and_explicit().
+ */
+#define fetch_and(obj, arg, order) \
+ atomic_fetch_and_explicit(obj, arg, memory_order_##order)
+
+#endif // BFS_ATOMIC_H
diff --git a/src/bar.c b/src/bar.c
index b0e595e..184d9a0 100644
--- a/src/bar.c
+++ b/src/bar.c
@@ -1,36 +1,24 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "bar.h"
+#include "atomic.h"
+#include "bfstd.h"
+#include "bit.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;
+ atomic unsigned int width;
+ atomic unsigned int height;
};
/** The global status bar instance. */
@@ -46,8 +34,8 @@ static int bfs_bar_getsize(struct bfs_bar *bar) {
return -1;
}
- bar->width = ws.ws_col;
- bar->height = ws.ws_row;
+ store(&bar->width, ws.ws_col, relaxed);
+ store(&bar->height, ws.ws_row, relaxed);
return 0;
#else
errno = ENOTSUP;
@@ -62,7 +50,7 @@ static int ass_puts(int fd, const char *str) {
}
/** Number of decimal digits needed for terminal sizes. */
-#define ITOA_DIGITS ((sizeof(unsigned short) * CHAR_BIT + 2) / 3)
+#define ITOA_DIGITS ((USHRT_WIDTH + 2) / 3)
/** Async Signal Safe itoa(). */
static char *ass_itoa(char *str, unsigned int n) {
@@ -87,8 +75,9 @@ static int bfs_bar_resize(struct bfs_bar *bar) {
"\033[;"; // DECSTBM: Set scrollable region
// DECSTBM takes the height as the second argument
+ unsigned int height = load(&bar->height, relaxed);
char *ptr = esc_seq + strlen(esc_seq);
- ptr = ass_itoa(ptr, bar->height - 1);
+ ptr = ass_itoa(ptr, height - 1);
strcpy(ptr,
"r" // DECSTBM
@@ -138,11 +127,11 @@ static void reset_before_death_by(int sig) {
}
/** printf() to the status bar with a single write(). */
-BFS_FORMATTER(2, 3)
+attr(printf(2, 3))
static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) {
va_list args;
va_start(args, format);
- char *str = dstrvprintf(format, args);
+ dchar *str = dstrvprintf(format, args);
va_end(args);
if (!str) {
@@ -191,13 +180,14 @@ struct bfs_bar *bfs_bar_show(void) {
sigaction(SIGWINCH, &sa, NULL);
#endif
+ unsigned int height = load(&the_bar.height, relaxed);
bfs_bar_printf(&the_bar,
"\n" // Make space for the bar
"\0337" // DECSC: Save cursor
"\033[;%ur" // DECSTBM: Set scrollable region
"\0338" // DECRC: Restore cursor
"\033[1A", // CUU: Move cursor up 1 row
- (unsigned int)(the_bar.height - 1)
+ height - 1
);
return &the_bar;
@@ -210,10 +200,11 @@ fail:
}
unsigned int bfs_bar_width(const struct bfs_bar *bar) {
- return bar->width;
+ return load(&bar->width, relaxed);
}
int bfs_bar_update(struct bfs_bar *bar, const char *str) {
+ unsigned int height = load(&bar->height, relaxed);
return bfs_bar_printf(bar,
"\0337" // DECSC: Save cursor
"\033[%u;0f" // HVP: Move cursor to row, column
@@ -222,7 +213,7 @@ int bfs_bar_update(struct bfs_bar *bar, const char *str) {
"%s"
"\033[27m" // SGR reverse video off
"\0338", // DECRC: Restore cursor
- (unsigned int)bar->height,
+ height,
str
);
}
diff --git a/src/bar.h b/src/bar.h
index 3e509d6..20d92a9 100644
--- a/src/bar.h
+++ b/src/bar.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A terminal status bar.
diff --git a/src/bfs.h b/src/bfs.h
deleted file mode 100644
index cf81412..0000000
--- a/src/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.6.1"
-#endif
-
-#ifndef BFS_HOMEPAGE
-# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html"
-#endif
-
-#endif // BFS_H
diff --git a/src/bfstd.c b/src/bfstd.c
new file mode 100644
index 0000000..f8ce871
--- /dev/null
+++ b/src/bfstd.c
@@ -0,0 +1,989 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "bfstd.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 <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#if BFS_USE_SYS_SYSMACROS_H
+# include <sys/sysmacros.h>
+#elif BFS_USE_SYS_MKDEV_H
+# include <sys/mkdev.h>
+#endif
+
+#if BFS_USE_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;
+}
+
+/** 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;
+}
+
+/** 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) {
+#ifdef 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 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;
+}
+
+int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
+#if BSD && !__GNU__
+ 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;
+
+#if __NetBSD__
+ int ret = string_to_flags(&str_arg, &set_arg, &clear_arg);
+#else
+ int ret = strtofflags(&str_arg, &set_arg, &clear_arg);
+#endif
+
+ *str = str_arg;
+ *set = set_arg;
+ *clear = clear_arg;
+
+ if (ret != 0) {
+ errno = EINVAL;
+ }
+ return ret;
+#else // !BSD
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+size_t asciilen(const char *str) {
+ return asciinlen(str, strlen(str));
+}
+
+size_t asciinlen(const char *str, size_t n) {
+ size_t i = 0;
+
+#if SIZE_WIDTH % 8 == 0
+ // Word-at-a-time isascii()
+ for (size_t word; i + sizeof(word) <= n; i += sizeof(word)) {
+ memcpy(&word, str + i, sizeof(word));
+
+ const size_t mask = (SIZE_MAX / 0xFF) << 7; // 0x808080...
+ word &= mask;
+ if (!word) {
+ continue;
+ }
+
+#if ENDIAN_NATIVE == ENDIAN_BIG
+ word = bswap(word);
+#elif ENDIAN_NATIVE != ENDIAN_LITTLE
+ break;
+#endif
+
+ size_t first = trailing_zeros(word) / 8;
+ return i + first;
+ }
+#endif
+
+ for (; i < n; ++i) {
+ if (!xisascii(str[i])) {
+ break;
+ }
+ }
+
+ return i;
+}
+
+wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb) {
+ wchar_t wc;
+ size_t mblen = mbrtowc(&wc, str + *i, len - *i, mb);
+ switch (mblen) {
+ case -1: // Invalid byte sequence
+ case -2: // Incomplete byte sequence
+ *i += 1;
+ *mb = (mbstate_t){0};
+ return WEOF;
+ default:
+ *i += mblen;
+ return wc;
+ }
+}
+
+size_t xstrwidth(const char *str) {
+ size_t len = strlen(str);
+ size_t ret = 0;
+
+ 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/9699919799/utilities/V3_chap02.html#tag_18_02
+ size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}");
+ return ret < len ? ret : len;
+}
+
+/** How much of this string is safe to double-quote? */
+static size_t quotable_len(const char *str, size_t len) {
+ // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_03
+ size_t ret = strcspn(str, "`$\\\"!");
+ return ret < len ? ret : len;
+}
+
+/** "Quote" a string for the shell. */
+static char *double_quote(char *dest, char *end, const char *str, size_t len) {
+ dest = xstpecpy(dest, end, "\"");
+ dest = xstpencpy(dest, end, str, len);
+ return xstpecpy(dest, end, "\"");
+}
+
+/** '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..f91e380
--- /dev/null
+++ b/src/bfstd.h
@@ -0,0 +1,518 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Standard library wrappers and polyfills.
+ */
+
+#ifndef BFS_BFSTD_H
+#define BFS_BFSTD_H
+
+#include "prelude.h"
+#include "sanity.h"
+#include <stddef.h>
+
+#include <ctype.h>
+
+/**
+ * Work around https://github.com/llvm/llvm-project/issues/65532 by forcing a
+ * function, not a macro, to be called.
+ */
+#if __FreeBSD__ && SANITIZE_MEMORY
+# define BFS_INTERCEPT(fn) (fn)
+#else
+# define BFS_INTERCEPT(fn) fn
+#endif
+
+/**
+ * Wrap isalpha()/isdigit()/etc.
+ */
+#define BFS_ISCTYPE(fn, c) BFS_INTERCEPT(fn)((unsigned char)(c))
+
+#define xisalnum(c) BFS_ISCTYPE(isalnum, c)
+#define xisalpha(c) BFS_ISCTYPE(isalpha, c)
+#define xisascii(c) BFS_ISCTYPE(isascii, c)
+#define xiscntrl(c) BFS_ISCTYPE(iscntrl, c)
+#define xisdigit(c) BFS_ISCTYPE(isdigit, c)
+#define xislower(c) BFS_ISCTYPE(islower, c)
+#define xisgraph(c) BFS_ISCTYPE(isgraph, c)
+#define xisprint(c) BFS_ISCTYPE(isprint, c)
+#define xispunct(c) BFS_ISCTYPE(ispunct, c)
+#define xisspace(c) BFS_ISCTYPE(isspace, c)
+#define xisupper(c) BFS_ISCTYPE(isupper, c)
+#define xisxdigit(c) BFS_ISCTYPE(isxdigit, c)
+
+// #include <errno.h>
+
+/**
+ * Check if an error code is "like" another one. For example, ENOTDIR is
+ * like ENOENT because they can both be triggered by non-existent paths.
+ *
+ * @param error
+ * The error code to check.
+ * @param category
+ * The category to test for. Known categories include ENOENT and
+ * ENAMETOOLONG.
+ * @return
+ * Whether the error belongs to the given category.
+ */
+bool error_is_like(int error, int category);
+
+/**
+ * Equivalent to error_is_like(errno, category).
+ */
+bool errno_is_like(int category);
+
+/**
+ * Apply the "negative errno" convention.
+ *
+ * @param ret
+ * The return value of the attempted operation.
+ * @return
+ * ret, if non-negative, otherwise -errno.
+ */
+int try(int ret);
+
+#include <fcntl.h>
+
+#ifndef O_EXEC
+# ifdef O_PATH
+# define O_EXEC O_PATH
+# else
+# define O_EXEC O_RDONLY
+# endif
+#endif
+
+#ifndef O_SEARCH
+# ifdef O_PATH
+# define O_SEARCH O_PATH
+# else
+# define O_SEARCH O_RDONLY
+# endif
+#endif
+
+#ifndef O_DIRECTORY
+# define O_DIRECTORY 0
+#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.
+ *
+ * @param 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.
+ *
+ * @param 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.
+ *
+ * @param 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.
+ *
+ * @param path
+ * The path to open.
+ * @param flags
+ * Flags to pass to open().
+ */
+FILE *xfopen(const char *path, int flags);
+
+/**
+ * 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);
+
+// #include <stdlib.h>
+
+/**
+ * Wrapper for getprogname() or equivalent functionality.
+ *
+ * @return
+ * The basename of the currently running program.
+ */
+const char *xgetprogname(void);
+
+/**
+ * Process a yes/no prompt.
+ *
+ * @return 1 for yes, 0 for no, and -1 for unknown.
+ */
+int ynprompt(void);
+
+// #include <string.h>
+
+/**
+ * Get the length of the pure-ASCII prefix of a string.
+ */
+size_t asciilen(const char *str);
+
+/**
+ * Get the length of the pure-ASCII prefix of a string.
+ *
+ * @param str
+ * The string to check.
+ * @param n
+ * The maximum prefix length.
+ */
+size_t asciinlen(const char *str, size_t n);
+
+/**
+ * Allocate a copy of a region of memory.
+ *
+ * @param src
+ * The memory region to copy.
+ * @param size
+ * The size of the memory region.
+ * @return
+ * 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.
+ *
+ * @param dest
+ * The NUL terminator of the destination string, or `end` if it is
+ * already truncated.
+ * @param end
+ * The end of the destination buffer.
+ * @param src
+ * The string to copy from.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *xstpecpy(char *dest, char *end, const char *src);
+
+/**
+ * A nice string copying function.
+ *
+ * @param dest
+ * The NUL terminator of the destination string, or `end` if it is
+ * already truncated.
+ * @param end
+ * The end of the destination buffer.
+ * @param src
+ * The string to copy from.
+ * @param 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().
+ *
+ * @param errnum
+ * An error number.
+ * @return
+ * A string describing that error, which remains valid until the next
+ * xstrerror() call in the same thread.
+ */
+const char *xstrerror(int errnum);
+
+/**
+ * Format a mode like ls -l (e.g. -rw-r--r--).
+ *
+ * @param mode
+ * The mode to format.
+ * @param 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 <unistd.h>
+
+/**
+ * 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]);
+
+/**
+ * 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.
+ *
+ * @param fd
+ * The file descriptor to close.
+ */
+void close_quietly(int fd);
+
+/**
+ * close() wrapper that asserts the file descriptor is valid.
+ *
+ * @param fd
+ * The file descriptor to close.
+ * @return
+ * 0 on success, or -1 on error.
+ */
+int xclose(int fd);
+
+/**
+ * Wrapper for faccessat() that handles some portability issues.
+ */
+int xfaccessat(int fd, const char *path, int amode);
+
+/**
+ * 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);
+
+/**
+ * Wrapper for confstr() that allocates with malloc().
+ *
+ * @param name
+ * The ID of the confstr to look up.
+ * @return
+ * The value of the confstr, or NULL on failure.
+ */
+char *xconfstr(int name);
+
+/**
+ * Portability wrapper for strtofflags().
+ *
+ * @param str
+ * 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);
+
+#include <wchar.h>
+
+/**
+ * Error-recovering mbrtowc() wrapper.
+ *
+ * @param str
+ * The string to convert.
+ * @param i
+ * The current index.
+ * @param len
+ * The length of the string.
+ * @param mb
+ * The multi-byte decoding state.
+ * @return
+ * The wide character at index *i, or WEOF if decoding fails. In either
+ * case, *i will be advanced to the next multi-byte character.
+ */
+wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb);
+
+/**
+ * wcswidth() variant that works on narrow strings.
+ *
+ * @param 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.
+ *
+ * @param dest
+ * The destination string to fill.
+ * @param end
+ * The end of the destination buffer.
+ * @param src
+ * The string to escape.
+ * @param flags
+ * Controls which characters to escape.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags);
+
+/**
+ * Escape a string as a single shell word.
+ *
+ * @param dest
+ * The destination string to fill.
+ * @param end
+ * The end of the destination buffer.
+ * @param src
+ * The string to escape.
+ * @param n
+ * The maximum length of the string.
+ * @param flags
+ * Controls which characters to escape.
+ * @return
+ * The new NUL terminator of the destination, or `end` on truncation.
+ */
+char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags);
+
+#endif // BFS_BFSTD_H
diff --git a/src/bftw.c b/src/bftw.c
index 6f97bf6..c4d3c17 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* The bftw() implementation consists of the following components:
@@ -20,32 +7,186 @@
* - struct bftw_file: A file that has been encountered during the traversal.
* They have reference-counted links to their parents in the directory tree.
*
+ * - struct bftw_list: A linked list of bftw_file's.
+ *
+ * - struct bftw_queue: A multi-stage queue of bftw_file's.
+ *
* - struct bftw_cache: An LRU list of bftw_file's with open file descriptors,
* used for openat() to minimize the amount of path re-traversals.
*
- * - struct bftw_queue: The queue of bftw_file's left to explore. Implemented
- * as a simple circular buffer.
- *
* - struct bftw_state: Represents the current state of the traversal, allowing
* various helper functions to take fewer parameters.
*/
+#include "prelude.h"
#include "bftw.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "diag.h"
#include "dir.h"
-#include "darray.h"
#include "dstring.h"
+#include "ioq.h"
+#include "list.h"
#include "mtab.h"
#include "stat.h"
#include "trie.h"
-#include "util.h"
-#include <assert.h>
#include <errno.h>
#include <fcntl.h>
-#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
-#include <unistd.h>
+#include <sys/types.h>
+
+/** Initialize a bftw_stat cache. */
+static void bftw_stat_init(struct bftw_stat *bufs, struct bfs_stat *stat_buf, struct bfs_stat *lstat_buf) {
+ bufs->stat_buf = stat_buf;
+ bufs->lstat_buf = lstat_buf;
+ bufs->stat_err = -1;
+ bufs->lstat_err = -1;
+}
+
+/** Fill a bftw_stat cache from another one. */
+static void bftw_stat_fill(struct bftw_stat *dest, const struct bftw_stat *src) {
+ if (dest->stat_err < 0 && src->stat_err >= 0) {
+ dest->stat_buf = src->stat_buf;
+ dest->stat_err = src->stat_err;
+ }
+
+ if (dest->lstat_err < 0 && src->lstat_err >= 0) {
+ dest->lstat_buf = src->lstat_buf;
+ dest->lstat_err = src->lstat_err;
+ }
+}
+
+/** Cache a bfs_stat() result. */
+static void bftw_stat_cache(struct bftw_stat *bufs, enum bfs_stat_flags flags, const struct bfs_stat *buf, int err) {
+ if (flags & BFS_STAT_NOFOLLOW) {
+ bufs->lstat_buf = buf;
+ bufs->lstat_err = err;
+ if (err || !S_ISLNK(buf->mode)) {
+ // Non-link, so share stat info
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+ } else if (flags & BFS_STAT_TRYFOLLOW) {
+ if (err) {
+ bufs->stat_err = err;
+ } else if (S_ISLNK(buf->mode)) {
+ bufs->lstat_buf = buf;
+ bufs->lstat_err = err;
+ bufs->stat_err = ENOENT;
+ } else {
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+ } else {
+ bufs->stat_buf = buf;
+ bufs->stat_err = err;
+ }
+}
+
+/** Caching bfs_stat(). */
+static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+ struct bfs_stat *buf;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ buf = (struct bfs_stat *)bufs->lstat_buf;
+ if (bufs->lstat_err == 0) {
+ return buf;
+ } else if (bufs->lstat_err > 0) {
+ errno = bufs->lstat_err;
+ return NULL;
+ }
+ } else {
+ buf = (struct bfs_stat *)bufs->stat_buf;
+ if (bufs->stat_err == 0) {
+ return buf;
+ } else if (bufs->stat_err > 0) {
+ errno = bufs->stat_err;
+ return NULL;
+ }
+ }
+
+ struct bfs_stat *ret;
+ int err;
+ if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, buf) == 0) {
+ ret = buf;
+ err = 0;
+#ifdef S_IFWHT
+ } else if (errno == ENOENT && ftwbuf->type == BFS_WHT) {
+ // This matches the behavior of FTS_WHITEOUT on BSD
+ ret = memset(buf, 0, sizeof(*buf));
+ ret->mode = S_IFWHT;
+ err = 0;
+#endif
+ } else {
+ ret = NULL;
+ err = errno;
+ }
+
+ bftw_stat_cache(bufs, flags, ret, err);
+ return ret;
+}
+
+const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
+ const struct bfs_stat *ret;
+
+ if (flags & BFS_STAT_TRYFOLLOW) {
+ ret = bftw_stat_impl(mutbuf, BFS_STAT_FOLLOW);
+ if (!ret && errno_is_like(ENOENT)) {
+ ret = bftw_stat_impl(mutbuf, BFS_STAT_NOFOLLOW);
+ }
+ } else {
+ ret = bftw_stat_impl(mutbuf, flags);
+ }
+
+ return ret;
+}
+
+const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ const struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ if (bufs->lstat_err == 0) {
+ return bufs->lstat_buf;
+ }
+ } else if (bufs->stat_err == 0) {
+ return bufs->stat_buf;
+ } else if ((flags & BFS_STAT_TRYFOLLOW) && error_is_like(bufs->stat_err, ENOENT)) {
+ if (bufs->lstat_err == 0) {
+ return bufs->lstat_buf;
+ }
+ }
+
+ return NULL;
+}
+
+enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ if (flags & BFS_STAT_NOFOLLOW) {
+ if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
+ return ftwbuf->type;
+ }
+ } else if (flags & BFS_STAT_TRYFOLLOW) {
+ if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) {
+ return ftwbuf->type;
+ }
+ } else {
+ if (ftwbuf->type != BFS_LNK) {
+ return ftwbuf->type;
+ } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) {
+ return BFS_ERROR;
+ }
+ }
+
+ const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
+ if (statbuf) {
+ return bfs_mode_to_type(statbuf->mode);
+ } else {
+ return BFS_ERROR;
+ }
+}
/**
* A file.
@@ -55,21 +196,45 @@ struct bftw_file {
struct bftw_file *parent;
/** The root under which this file was found. */
struct bftw_file *root;
- /** The next file in the queue, if any. */
+
+ /**
+ * List node for:
+ *
+ * bftw_queue::buffer
+ * bftw_queue::waiting
+ * bftw_file_open()::parents
+ */
struct bftw_file *next;
- /** The previous file in the LRU list. */
- struct bftw_file *lru_prev;
- /** The next file in the LRU list. */
- struct bftw_file *lru_next;
+ /**
+ * List node for:
+ *
+ * bftw_queue::ready
+ * bftw_state::to_close
+ */
+ struct { struct bftw_file *next; } ready;
+
+ /**
+ * List node for bftw_cache.
+ */
+ struct {
+ struct bftw_file *prev;
+ struct bftw_file *next;
+ } lru;
/** This file's depth in the walk. */
size_t depth;
- /** Reference count. */
+ /** Reference count (for ->parent). */
size_t refcount;
+ /** Pin count (for ->fd). */
+ size_t pincount;
/** An open descriptor to this file, or -1. */
int fd;
+ /** Whether this file has a pending ioq request. */
+ bool ioqueued;
+ /** An open directory for this file, if any. */
+ struct bfs_dir *dir;
/** This file's type, if known. */
enum bfs_type type;
@@ -78,6 +243,9 @@ struct bftw_file {
/** The inode number, for cycle detection. */
ino_t ino;
+ /** Cached bfs_stat() info. */
+ struct bftw_stat stat_bufs;
+
/** The offset of this file in the full path. */
size_t nameoff;
/** The length of the file's name. */
@@ -87,145 +255,445 @@ struct bftw_file {
};
/**
+ * A linked list of bftw_file's.
+ */
+struct bftw_list {
+ struct bftw_file *head;
+ struct bftw_file **tail;
+};
+
+/**
+ * bftw_queue flags.
+ */
+enum bftw_qflags {
+ /** Track the sync/async service balance. */
+ BFTW_QBALANCE = 1 << 0,
+ /** Buffer files before adding them to the queue. */
+ BFTW_QBUFFER = 1 << 1,
+ /** Use LIFO (stack/DFS) ordering. */
+ BFTW_QLIFO = 1 << 2,
+ /** Maintain a strict order. */
+ BFTW_QORDER = 1 << 3,
+};
+
+/**
+ * A queue of bftw_file's that may be serviced asynchronously.
+ *
+ * A bftw_queue comprises three linked lists each tracking different stages.
+ * When BFTW_QBUFFER is set, files are initially pushed to the buffer:
+ *
+ * ╔═══╗ ╔═══╦═══╗
+ * buffer: ║ 𝘩 ║ ║ 𝘩 ║ 𝘪 ║
+ * ╠═══╬═══╦═══╗ ╠═══╬═══╬═══╗
+ * waiting: ║ e ║ f ║ g ║ → ║ e ║ f ║ g ║
+ * ╠═══╬═══╬═══╬═══╗ ╠═══╬═══╬═══╬═══╗
+ * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║
+ * ╚═══╩═══╩═══╩═══╝ ╚═══╩═══╩═══╩═══╝
+ *
+ * When bftw_queue_flush() is called, the files in the buffer are appended to
+ * the waiting list (or prepended, if BFTW_QLIFO is set):
+ *
+ * ╔═╗
+ * buffer: ║ ║
+ * ╠═╩═╦═══╦═══╦═══╦═══╗
+ * waiting: ║ e ║ f ║ g ║ h ║ i ║
+ * ╠═══╬═══╬═══╬═══╬═══╝
+ * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║
+ * ╚═══╩═══╩═══╩═══╝
+ *
+ * Using the buffer gives a more natural ordering for BFTW_QLIFO, and allows
+ * files to be sorted before adding them to the waiting list. If BFTW_QBUFFER
+ * is not set, files are pushed directly to the waiting list instead.
+ *
+ * Files on the waiting list are waiting to be "serviced" asynchronously by the
+ * ioq (for example, by an ioq_opendir() or ioq_stat() call). While they are
+ * being serviced, they are detached from the queue by bftw_queue_detach() and
+ * are not tracked by the queue at all:
+ *
+ * ╔═╗
+ * buffer: ║ ║
+ * ╠═╩═╦═══╦═══╗ ⎛ ┌───┬───┐ ⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ 𝓯 │ ⎟
+ * ╠═══╬═══╬═══╬═══╗ ⎝ └───┴───┘ ⎠
+ * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║
+ * ╚═══╩═══╩═══╩═══╝
+ *
+ * When their async service is complete, files are reattached to the queue by
+ * bftw_queue_attach(), this time on the ready list:
+ *
+ * ╔═╗
+ * buffer: ║ ║
+ * ╠═╩═╦═══╦═══╗ ⎛ ┌───┐ ⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ ⎟
+ * ╠═══╬═══╬═══╬═══╦═══╗ ⎝ └───┘ ⎠
+ * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ 𝕗 ║
+ * ╚═══╩═══╩═══╩═══╩═══╝
+ *
+ * Files are added to the ready list in the order they are finished by the ioq.
+ * bftw_queue_pop() pops a file from the ready list if possible. Otherwise, it
+ * pops from the waiting list, and the file must be serviced synchronously.
+ *
+ * However, if BFTW_QORDER is set, files must be popped in the exact order they
+ * are added to the waiting list (to maintain sorted order). In this case,
+ * files are added to the waiting and ready lists at the same time. The
+ * file->ioqueued flag is set while it is in-service, so that bftw() can wait
+ * for it to be truly ready before using it.
+ *
+ * ╔═╗
+ * buffer: ║ ║
+ * ╠═╩═╦═══╦═══╗ ⎛ ┌───┐ ⎞
+ * waiting: ║ g ║ h ║ i ║ ⎜ ioq: │ 𝓮 │ ⎟
+ * ╠═══╬═══╬═══╬═══╦═══╦═══╦═══╦═══╦═══╗ ⎝ └───┘ ⎠
+ * ready: ║ 𝕒 ║ 𝕓 ║ 𝕔 ║ 𝕕 ║ 𝓮 ║ 𝕗 ║ g ║ h ║ i ║
+ * ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝
+ *
+ * If BFTW_QBALANCE is set, queue->imbalance tracks the delta between async
+ * service (negative) and synchronous service (positive). The queue is
+ * considered "balanced" when this number is non-negative. Only a balanced
+ * queue will perform any async service, ensuring work is fairly distributed
+ * between the main thread and the ioq.
+ *
+ * BFTW_QBALANCE is only set for single-threaded ioqs. When an ioq has multiple
+ * threads, it is faster to wait for the ioq to complete an operation than it is
+ * to perform it on the main thread.
+ */
+struct bftw_queue {
+ /** Queue flags. */
+ enum bftw_qflags flags;
+ /** A buffer of files to be enqueued together. */
+ struct bftw_list buffer;
+ /** A list of files which are waiting to be serviced. */
+ struct bftw_list waiting;
+ /** A list of already-serviced files. */
+ struct bftw_list ready;
+ /** The current size of the queue. */
+ size_t size;
+ /** The number of files currently in-service. */
+ size_t ioqueued;
+ /** Tracks the imbalance between synchronous and async service. */
+ unsigned long imbalance;
+};
+
+/** Initialize a queue. */
+static void bftw_queue_init(struct bftw_queue *queue, enum bftw_qflags flags) {
+ queue->flags = flags;
+ SLIST_INIT(&queue->buffer);
+ SLIST_INIT(&queue->waiting);
+ SLIST_INIT(&queue->ready);
+ queue->size = 0;
+ queue->ioqueued = 0;
+ queue->imbalance = 0;
+}
+
+/** Add a file to the queue. */
+static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
+ if (queue->flags & BFTW_QBUFFER) {
+ SLIST_APPEND(&queue->buffer, file);
+ } else if (queue->flags & BFTW_QLIFO) {
+ SLIST_PREPEND(&queue->waiting, file);
+ if (queue->flags & BFTW_QORDER) {
+ SLIST_PREPEND(&queue->ready, file, ready);
+ }
+ } else {
+ SLIST_APPEND(&queue->waiting, file);
+ if (queue->flags & BFTW_QORDER) {
+ SLIST_APPEND(&queue->ready, file, ready);
+ }
+ }
+
+ ++queue->size;
+}
+
+/** Add any buffered files to the queue. */
+static void bftw_queue_flush(struct bftw_queue *queue) {
+ if (!(queue->flags & BFTW_QBUFFER)) {
+ return;
+ }
+
+ if (queue->flags & BFTW_QORDER) {
+ // When sorting, add files to the ready list at the same time
+ // (and in the same order) as they are added to the waiting list
+ struct bftw_file **cursor = (queue->flags & BFTW_QLIFO)
+ ? &queue->ready.head
+ : queue->ready.tail;
+ for_slist (struct bftw_file, file, &queue->buffer) {
+ cursor = SLIST_INSERT(&queue->ready, cursor, file, ready);
+ }
+ }
+
+ if (queue->flags & BFTW_QLIFO) {
+ SLIST_EXTEND(&queue->buffer, &queue->waiting);
+ }
+
+ SLIST_EXTEND(&queue->waiting, &queue->buffer);
+}
+
+/** Check if the queue is properly balanced for async work. */
+static bool bftw_queue_balanced(const struct bftw_queue *queue) {
+ if (queue->flags & BFTW_QBALANCE) {
+ return (long)queue->imbalance >= 0;
+ } else {
+ return true;
+ }
+}
+
+/** Update the queue balance for (a)sync service. */
+static void bftw_queue_rebalance(struct bftw_queue *queue, bool async) {
+ if (async) {
+ --queue->imbalance;
+ } else {
+ ++queue->imbalance;
+ }
+}
+
+/** Detatch the next waiting file. */
+static void bftw_queue_detach(struct bftw_queue *queue, struct bftw_file *file, bool async) {
+ bfs_assert(!file->ioqueued);
+
+ if (file == SLIST_HEAD(&queue->buffer)) {
+ // To maintain order, we can't detach any files until they're
+ // added to the waiting/ready lists
+ bfs_assert(!(queue->flags & BFTW_QORDER));
+ SLIST_POP(&queue->buffer);
+ } else if (file == SLIST_HEAD(&queue->waiting)) {
+ SLIST_POP(&queue->waiting);
+ } else {
+ bfs_bug("Detached file was not buffered or waiting");
+ }
+
+ if (async) {
+ file->ioqueued = true;
+ ++queue->ioqueued;
+ bftw_queue_rebalance(queue, true);
+ }
+}
+
+/** Reattach a serviced file to the queue. */
+static void bftw_queue_attach(struct bftw_queue *queue, struct bftw_file *file, bool async) {
+ if (async) {
+ bfs_assert(file->ioqueued);
+ file->ioqueued = false;
+ --queue->ioqueued;
+ } else {
+ bfs_assert(!file->ioqueued);
+ }
+
+ if (!(queue->flags & BFTW_QORDER)) {
+ SLIST_APPEND(&queue->ready, file, ready);
+ }
+}
+
+/** Make a file ready immediately. */
+static void bftw_queue_skip(struct bftw_queue *queue, struct bftw_file *file) {
+ bftw_queue_detach(queue, file, false);
+ bftw_queue_attach(queue, file, false);
+}
+
+/** Get the next waiting file. */
+static struct bftw_file *bftw_queue_waiting(const struct bftw_queue *queue) {
+ if (!(queue->flags & BFTW_QBUFFER)) {
+ return SLIST_HEAD(&queue->waiting);
+ }
+
+ if (queue->flags & BFTW_QORDER) {
+ // Don't detach files until they're on the waiting/ready lists
+ return SLIST_HEAD(&queue->waiting);
+ }
+
+ const struct bftw_list *prefix = &queue->waiting;
+ const struct bftw_list *suffix = &queue->buffer;
+ if (queue->flags & BFTW_QLIFO) {
+ prefix = &queue->buffer;
+ suffix = &queue->waiting;
+ }
+
+ struct bftw_file *file = SLIST_HEAD(prefix);
+ if (!file) {
+ file = SLIST_HEAD(suffix);
+ }
+ return file;
+}
+
+/** Get the next ready file. */
+static struct bftw_file *bftw_queue_ready(const struct bftw_queue *queue) {
+ return SLIST_HEAD(&queue->ready);
+}
+
+/** Pop a file from the queue. */
+static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) {
+ // Don't pop until we've had a chance to sort the buffer
+ bfs_assert(SLIST_EMPTY(&queue->buffer));
+
+ struct bftw_file *file = SLIST_POP(&queue->ready, ready);
+
+ if (!file || file == SLIST_HEAD(&queue->waiting)) {
+ // If no files are ready, try the waiting list. Or, if
+ // BFTW_QORDER is set, we may need to pop from both lists.
+ file = SLIST_POP(&queue->waiting);
+ }
+
+ if (file) {
+ --queue->size;
+ }
+
+ return file;
+}
+
+/**
* A cache of open directories.
*/
struct bftw_cache {
/** The head of the LRU list. */
struct bftw_file *head;
- /** The insertion target for the LRU list. */
- struct bftw_file *target;
/** The tail of the LRU list. */
struct bftw_file *tail;
+ /** The insertion target for the LRU list. */
+ struct bftw_file *target;
/** The remaining capacity of the LRU list. */
size_t capacity;
+
+ /** bftw_file arena. */
+ struct varena files;
+
+ /** bfs_dir arena. */
+ struct arena dirs;
+ /** Remaining bfs_dir capacity. */
+ int dir_limit;
+
+ /** bfs_stat arena. */
+ struct arena stat_bufs;
};
/** Initialize a cache. */
static void bftw_cache_init(struct bftw_cache *cache, size_t capacity) {
- cache->head = NULL;
+ LIST_INIT(cache);
cache->target = NULL;
- cache->tail = NULL;
cache->capacity = capacity;
-}
-/** Destroy a cache. */
-static void bftw_cache_destroy(struct bftw_cache *cache) {
- assert(!cache->tail);
- assert(!cache->target);
- assert(!cache->head);
-}
+ VARENA_INIT(&cache->files, struct bftw_file, name);
-/** Add a bftw_file to the cache. */
-static void bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
- assert(cache->capacity > 0);
- assert(file->fd >= 0);
- assert(!file->lru_prev);
- assert(!file->lru_next);
-
- if (cache->target) {
- file->lru_prev = cache->target;
- file->lru_next = cache->target->lru_next;
- } else {
- file->lru_next = cache->head;
- }
+ bfs_dir_arena(&cache->dirs);
- if (file->lru_prev) {
- file->lru_prev->lru_next = file;
+ if (cache->capacity > 1024) {
+ cache->dir_limit = 1024;
} else {
- cache->head = file;
+ cache->dir_limit = capacity - 1;
}
- if (file->lru_next) {
- file->lru_next->lru_prev = file;
- } else {
- cache->tail = file;
+ ARENA_INIT(&cache->stat_bufs, struct bfs_stat);
+}
+
+/** Allocate a directory. */
+static struct bfs_dir *bftw_allocdir(struct bftw_cache *cache, bool force) {
+ if (!force && cache->dir_limit <= 0) {
+ errno = ENOMEM;
+ return NULL;
}
- // Prefer to keep the root paths open by keeping them at the head of the list
- if (file->depth == 0) {
- cache->target = file;
+ struct bfs_dir *dir = arena_alloc(&cache->dirs);
+ if (dir) {
+ --cache->dir_limit;
}
+ return dir;
+}
- --cache->capacity;
+/** Free a directory. */
+static void bftw_freedir(struct bftw_cache *cache, struct bfs_dir *dir) {
+ ++cache->dir_limit;
+ arena_free(&cache->dirs, dir);
}
-/** Remove a bftw_file from the cache. */
-static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
+/** Remove a bftw_file from the LRU list. */
+static void bftw_lru_remove(struct bftw_cache *cache, struct bftw_file *file) {
if (cache->target == file) {
- cache->target = file->lru_prev;
- }
-
- if (file->lru_prev) {
- assert(cache->head != file);
- file->lru_prev->lru_next = file->lru_next;
- } else {
- assert(cache->head == file);
- cache->head = file->lru_next;
- }
-
- if (file->lru_next) {
- assert(cache->tail != file);
- file->lru_next->lru_prev = file->lru_prev;
- } else {
- assert(cache->tail == file);
- cache->tail = file->lru_prev;
+ cache->target = file->lru.prev;
}
- file->lru_prev = NULL;
- file->lru_next = NULL;
- ++cache->capacity;
+ LIST_REMOVE(cache, file, lru);
}
-/** Mark a cache entry as recently used. */
-static void bftw_cache_use(struct bftw_cache *cache, struct bftw_file *file) {
- bftw_cache_remove(cache, file);
- bftw_cache_add(cache, file);
+/** Remove a bftw_file from the cache. */
+static void bftw_cache_remove(struct bftw_cache *cache, struct bftw_file *file) {
+ bftw_lru_remove(cache, file);
+ ++cache->capacity;
}
/** Close a bftw_file. */
static void bftw_file_close(struct bftw_cache *cache, struct bftw_file *file) {
- assert(file->fd >= 0);
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount == 0);
- bftw_cache_remove(cache, file);
+ if (file->dir) {
+ bfs_closedir(file->dir);
+ bftw_freedir(cache, file->dir);
+ file->dir = NULL;
+ } else {
+ xclose(file->fd);
+ }
- xclose(file->fd);
file->fd = -1;
+ bftw_cache_remove(cache, file);
}
-/** Pop a directory from the cache. */
-static void bftw_cache_pop(struct bftw_cache *cache) {
- assert(cache->tail);
- bftw_file_close(cache, cache->tail);
-}
-
-/**
- * Shrink the cache, to recover from EMFILE.
- *
- * @param cache
- * The cache in question.
- * @param saved
- * A bftw_file that must be preserved.
- * @return
- * 0 if successfully shrunk, otherwise -1.
- */
-static int bftw_cache_shrink(struct bftw_cache *cache, const struct bftw_file *saved) {
+/** Pop the least recently used directory from the cache. */
+static int bftw_cache_pop(struct bftw_cache *cache) {
struct bftw_file *file = cache->tail;
if (!file) {
return -1;
}
- if (file == saved) {
- file = file->lru_prev;
- if (!file) {
- return -1;
- }
+ bftw_file_close(cache, file);
+ return 0;
+}
+
+/** Add a bftw_file to the LRU list. */
+static void bftw_lru_add(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ LIST_INSERT(cache, cache->target, file, lru);
+
+ // Prefer to keep the root paths open by keeping them at the head of the list
+ if (file->depth == 0) {
+ cache->target = file;
}
+}
- bftw_file_close(cache, file);
- cache->capacity = 0;
+/** Add a bftw_file to the cache. */
+static int bftw_cache_add(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ if (cache->capacity == 0 && bftw_cache_pop(cache) != 0) {
+ bftw_file_close(cache, file);
+ errno = EMFILE;
+ return -1;
+ }
+
+ bfs_assert(cache->capacity > 0);
+ --cache->capacity;
+
+ bftw_lru_add(cache, file);
return 0;
}
+/** Pin a cache entry so it won't be closed. */
+static void bftw_cache_pin(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+
+ if (file->pincount++ == 0) {
+ bftw_lru_remove(cache, file);
+ }
+}
+
+/** Unpin a cache entry. */
+static void bftw_cache_unpin(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount > 0);
+
+ if (--file->pincount == 0) {
+ bftw_lru_add(cache, file);
+ }
+}
+
/** Compute the name offset of a child path. */
static size_t bftw_child_nameoff(const struct bftw_file *parent) {
size_t ret = parent->nameoff + parent->namelen;
@@ -235,12 +703,20 @@ static size_t bftw_child_nameoff(const struct bftw_file *parent) {
return ret;
}
+/** Destroy a cache. */
+static void bftw_cache_destroy(struct bftw_cache *cache) {
+ bfs_assert(LIST_EMPTY(cache));
+ bfs_assert(!cache->target);
+
+ arena_destroy(&cache->stat_bufs);
+ arena_destroy(&cache->dirs);
+ varena_destroy(&cache->files);
+}
+
/** Create a new bftw_file. */
-static struct bftw_file *bftw_file_new(struct bftw_file *parent, const char *name) {
+static struct bftw_file *bftw_file_new(struct bftw_cache *cache, struct bftw_file *parent, const char *name) {
size_t namelen = strlen(name);
- size_t size = BFS_FLEX_SIZEOF(struct bftw_file, name, namelen + 1);
-
- struct bftw_file *file = malloc(size);
+ struct bftw_file *file = varena_alloc(&cache->files, namelen + 1);
if (!file) {
return NULL;
}
@@ -258,63 +734,403 @@ static struct bftw_file *bftw_file_new(struct bftw_file *parent, const char *nam
file->nameoff = 0;
}
- file->next = NULL;
-
- file->lru_prev = NULL;
- file->lru_next = NULL;
+ SLIST_ITEM_INIT(file);
+ SLIST_ITEM_INIT(file, ready);
+ LIST_ITEM_INIT(file, lru);
file->refcount = 1;
+ file->pincount = 0;
file->fd = -1;
+ file->ioqueued = false;
+ file->dir = NULL;
file->type = BFS_UNKNOWN;
file->dev = -1;
file->ino = -1;
+ bftw_stat_init(&file->stat_bufs, NULL, NULL);
+
file->namelen = namelen;
memcpy(file->name, name, namelen + 1);
return file;
}
+/** Associate an open directory with a bftw_file. */
+static void bftw_file_set_dir(struct bftw_cache *cache, struct bftw_file *file, struct bfs_dir *dir) {
+ bfs_assert(!file->dir);
+ file->dir = dir;
+
+ if (file->fd >= 0) {
+ bfs_assert(file->fd == bfs_dirfd(dir));
+ } else {
+ file->fd = bfs_dirfd(dir);
+ bftw_cache_add(cache, file);
+ }
+}
+
+/** Free a file's cached stat() buffers. */
+static void bftw_stat_recycle(struct bftw_cache *cache, struct bftw_file *file) {
+ struct bftw_stat *bufs = &file->stat_bufs;
+
+ struct bfs_stat *stat_buf = (struct bfs_stat *)bufs->stat_buf;
+ struct bfs_stat *lstat_buf = (struct bfs_stat *)bufs->lstat_buf;
+ if (stat_buf) {
+ arena_free(&cache->stat_bufs, stat_buf);
+ } else if (lstat_buf) {
+ arena_free(&cache->stat_bufs, lstat_buf);
+ }
+
+ bftw_stat_init(bufs, NULL, NULL);
+}
+
+/** Free a bftw_file. */
+static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) {
+ bfs_assert(file->refcount == 0);
+
+ if (file->fd >= 0) {
+ bftw_file_close(cache, file);
+ }
+
+ bftw_stat_recycle(cache, file);
+
+ varena_free(&cache->files, file, file->namelen + 1);
+}
+
/**
- * Open a bftw_file relative to another one.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The file to open.
- * @param base
- * The base directory for the relative path (may be NULL).
- * @param at_fd
- * The base file descriptor, AT_FDCWD if base == NULL.
- * @param at_path
- * The relative path to the file.
- * @return
- * The opened file descriptor, or negative on error.
+ * Holds the current state of the bftw() traversal.
*/
-static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, struct bftw_file *base, const char *at_path) {
- assert(file->fd < 0);
+struct bftw_state {
+ /** The path(s) to start from. */
+ const char **paths;
+ /** The number of starting paths. */
+ size_t npaths;
+ /** bftw() callback. */
+ bftw_callback *callback;
+ /** bftw() callback data. */
+ void *ptr;
+ /** bftw() flags. */
+ enum bftw_flags flags;
+ /** Search strategy. */
+ enum bftw_strategy strategy;
+ /** The mount table. */
+ const struct bfs_mtab *mtab;
+ /** bfs_opendir() flags. */
+ enum bfs_dir_flags dir_flags;
+
+ /** The appropriate errno value, if any. */
+ int error;
+
+ /** The cache of open directories. */
+ struct bftw_cache cache;
+
+ /** The async I/O queue. */
+ struct ioq *ioq;
+ /** The number of I/O threads. */
+ size_t nthreads;
+
+ /** The queue of unpinned directories to unwrap. */
+ struct bftw_list to_close;
+ /** The queue of files to visit. */
+ struct bftw_queue fileq;
+ /** The queue of directories to open/read. */
+ struct bftw_queue dirq;
+
+ /** The current path. */
+ dchar *path;
+ /** The current file. */
+ struct bftw_file *file;
+ /** The previous file. */
+ struct bftw_file *previous;
+
+ /** The currently open directory. */
+ struct bfs_dir *dir;
+ /** The current directory entry. */
+ struct bfs_dirent *de;
+ /** Storage for the directory entry. */
+ struct bfs_dirent de_storage;
+ /** Any error encountered while reading the directory. */
+ int direrror;
+
+ /** Extra data about the current file. */
+ struct BFTW ftwbuf;
+ /** stat() buffer storage. */
+ struct bfs_stat stat_buf;
+ /** lstat() buffer storage. */
+ struct bfs_stat lstat_buf;
+};
+
+/** Check if we have to buffer files before visiting them. */
+static bool bftw_must_buffer(const struct bftw_state *state) {
+ if (state->flags & BFTW_SORT) {
+ // Have to buffer the files to sort them
+ return true;
+ }
+
+ if (state->strategy == BFTW_DFS && state->nthreads == 0) {
+ // Without buffering, we would get a not-quite-depth-first
+ // ordering:
+ //
+ // a
+ // b
+ // a/c
+ // a/c/d
+ // b/e
+ // b/e/f
+ //
+ // This is okay for iterative deepening, since the caller only
+ // sees files at the target depth. We also deem it okay for
+ // parallel searches, since the order is unpredictable anyway.
+ return true;
+ }
+
+ if ((state->flags & BFTW_STAT) && state->nthreads > 1) {
+ // We will be buffering every file anyway for ioq_stat()
+ return true;
+ }
+
+ return false;
+}
+
+/** Initialize the bftw() state. */
+static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) {
+ state->paths = args->paths;
+ state->npaths = args->npaths;
+ state->callback = args->callback;
+ state->ptr = args->ptr;
+ state->flags = args->flags;
+ state->strategy = args->strategy;
+ state->mtab = args->mtab;
+ state->dir_flags = 0;
+ state->error = 0;
+
+ if (args->nopenfd < 2) {
+ errno = EMFILE;
+ return -1;
+ }
+
+ size_t nopenfd = args->nopenfd;
+ size_t qdepth = 4096;
+ size_t nthreads = args->nthreads;
+
+#if BFS_USE_LIBURING
+ // io_uring uses one fd per ring, ioq uses one ring per thread
+ if (nthreads >= nopenfd - 1) {
+ nthreads = nopenfd - 2;
+ }
+ nopenfd -= nthreads;
+#endif
+
+ bftw_cache_init(&state->cache, nopenfd);
+
+ if (nthreads > 0) {
+ state->ioq = ioq_create(qdepth, nthreads);
+ if (!state->ioq) {
+ return -1;
+ }
+ } else {
+ state->ioq = NULL;
+ }
+ state->nthreads = nthreads;
+
+ if (bftw_must_buffer(state)) {
+ state->flags |= BFTW_BUFFER;
+ }
+
+ if (state->flags & BFTW_WHITEOUTS) {
+ state->dir_flags |= BFS_DIR_WHITEOUTS;
+ }
+
+ SLIST_INIT(&state->to_close);
+
+ enum bftw_qflags qflags = 0;
+ if (state->strategy != BFTW_BFS) {
+ qflags |= BFTW_QBUFFER | BFTW_QLIFO;
+ }
+ if (state->flags & BFTW_BUFFER) {
+ qflags |= BFTW_QBUFFER;
+ }
+ if (state->flags & BFTW_SORT) {
+ qflags |= BFTW_QORDER;
+ } else if (nthreads == 1) {
+ qflags |= BFTW_QBALANCE;
+ }
+ bftw_queue_init(&state->fileq, qflags);
+
+ if (state->strategy == BFTW_BFS || (state->flags & BFTW_BUFFER)) {
+ // In breadth-first mode, or if we're already buffering files,
+ // directories can be queued in FIFO order
+ qflags &= ~(BFTW_QBUFFER | BFTW_QLIFO);
+ }
+ bftw_queue_init(&state->dirq, qflags);
+
+ state->path = NULL;
+ state->file = NULL;
+ state->previous = NULL;
+
+ state->dir = NULL;
+ state->de = NULL;
+ state->direrror = 0;
+
+ return 0;
+}
+
+/** Queue a directory for unwrapping. */
+static void bftw_delayed_unwrap(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->dir);
+
+ if (!SLIST_ATTACHED(&state->to_close, file, ready)) {
+ SLIST_APPEND(&state->to_close, file, ready);
+ }
+}
+
+/** Unpin a file's parent. */
+static void bftw_unpin_parent(struct bftw_state *state, struct bftw_file *file, bool unwrap) {
+ struct bftw_file *parent = file->parent;
+ if (!parent) {
+ return;
+ }
+
+ bftw_cache_unpin(&state->cache, parent);
+
+ if (unwrap && parent->dir && parent->pincount == 0) {
+ bftw_delayed_unwrap(state, parent);
+ }
+}
+
+/** Pop a response from the I/O queue. */
+static int bftw_ioq_pop(struct bftw_state *state, bool block) {
+ struct bftw_cache *cache = &state->cache;
+ struct ioq *ioq = state->ioq;
+ if (!ioq) {
+ return -1;
+ }
+
+ struct ioq_ent *ent = ioq_pop(ioq, block);
+ if (!ent) {
+ return -1;
+ }
+
+ struct bftw_file *file = ent->ptr;
+ if (file) {
+ bftw_unpin_parent(state, file, true);
+ }
+
+ enum ioq_op op = ent->op;
+ switch (op) {
+ case IOQ_CLOSE:
+ ++cache->capacity;
+ break;
+
+ case IOQ_CLOSEDIR:
+ ++cache->capacity;
+ bftw_freedir(cache, ent->closedir.dir);
+ break;
+
+ case IOQ_OPENDIR:
+ ++cache->capacity;
+
+ if (ent->result >= 0) {
+ bftw_file_set_dir(cache, file, ent->opendir.dir);
+ } else {
+ bftw_freedir(cache, ent->opendir.dir);
+ }
+
+ bftw_queue_attach(&state->dirq, file, true);
+ break;
+
+ case IOQ_STAT:
+ if (ent->result >= 0) {
+ bftw_stat_cache(&file->stat_bufs, ent->stat.flags, ent->stat.buf, 0);
+ } else {
+ arena_free(&cache->stat_bufs, ent->stat.buf);
+ bftw_stat_cache(&file->stat_bufs, ent->stat.flags, NULL, -ent->result);
+ }
+
+ bftw_queue_attach(&state->fileq, file, true);
+ break;
+ }
+
+ ioq_free(ioq, ent);
+ return op;
+}
+
+/** Try to reserve space in the I/O queue. */
+static int bftw_ioq_reserve(struct bftw_state *state) {
+ struct ioq *ioq = state->ioq;
+ if (!ioq) {
+ return -1;
+ }
+
+ if (ioq_capacity(ioq) > 0) {
+ return 0;
+ }
+
+ // With more than one background thread, it's faster to wait on
+ // background I/O than it is to do it on the main thread
+ bool block = state->nthreads > 1;
+ if (bftw_ioq_pop(state, block) < 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Try to reserve space in the cache. */
+static int bftw_cache_reserve(struct bftw_state *state) {
+ struct bftw_cache *cache = &state->cache;
+ if (cache->capacity > 0) {
+ return 0;
+ }
+
+ while (bftw_ioq_pop(state, true) >= 0) {
+ if (cache->capacity > 0) {
+ return 0;
+ }
+ }
+
+ if (bftw_cache_pop(cache) != 0) {
+ errno = EMFILE;
+ return -1;
+ }
+
+ bfs_assert(cache->capacity > 0);
+ return 0;
+}
+
+/** Open a bftw_file relative to another one. */
+static int bftw_file_openat(struct bftw_state *state, struct bftw_file *file, struct bftw_file *base, const char *at_path) {
+ bfs_assert(file->fd < 0);
+
+ struct bftw_cache *cache = &state->cache;
int at_fd = AT_FDCWD;
if (base) {
- bftw_cache_use(cache, base);
+ bftw_cache_pin(cache, base);
at_fd = base->fd;
}
+ int fd = -1;
+ if (bftw_cache_reserve(state) != 0) {
+ goto unpin;
+ }
+
int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY;
- int fd = openat(at_fd, at_path, flags);
+ fd = openat(at_fd, at_path, flags);
if (fd < 0 && errno == EMFILE) {
- if (bftw_cache_shrink(cache, base) == 0) {
+ if (bftw_cache_pop(cache) == 0) {
fd = openat(at_fd, at_path, flags);
}
+ cache->capacity = 1;
}
- if (fd >= 0) {
- if (cache->capacity == 0) {
- bftw_cache_pop(cache);
- }
+unpin:
+ if (base) {
+ bftw_cache_unpin(cache, base);
+ }
+ if (fd >= 0) {
file->fd = fd;
bftw_cache_add(cache, file);
}
@@ -322,19 +1138,8 @@ static int bftw_file_openat(struct bftw_cache *cache, struct bftw_file *file, st
return fd;
}
-/**
- * Open a bftw_file.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The file to open.
- * @param path
- * The full path to the file.
- * @return
- * The opened file descriptor, or negative on error.
- */
-static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
+/** Open a bftw_file. */
+static int bftw_file_open(struct bftw_state *state, struct bftw_file *file, const char *path) {
// Find the nearest open ancestor
struct bftw_file *base = file;
do {
@@ -346,335 +1151,424 @@ static int bftw_file_open(struct bftw_cache *cache, struct bftw_file *file, cons
at_path += bftw_child_nameoff(base);
}
- int fd = bftw_file_openat(cache, file, base, at_path);
- if (fd >= 0 || errno != ENAMETOOLONG) {
+ int fd = bftw_file_openat(state, file, base, at_path);
+ if (fd >= 0 || !errno_is_like(ENAMETOOLONG)) {
return fd;
}
// Handle ENAMETOOLONG by manually traversing the path component-by-component
+ struct bftw_list parents;
+ SLIST_INIT(&parents);
- // Use the ->next linked list to temporarily hold the reversed parent
- // chain between base and file
struct bftw_file *cur;
- for (cur = file; cur->parent != base; cur = cur->parent) {
- cur->parent->next = cur;
+ for (cur = file; cur != base; cur = cur->parent) {
+ SLIST_PREPEND(&parents, cur);
}
- // Open the files in the chain one by one
- for (base = cur; base; base = base->next) {
- fd = bftw_file_openat(cache, base, base->parent, base->name);
- if (fd < 0 || base == file) {
- break;
+ while ((cur = SLIST_POP(&parents))) {
+ if (!cur->parent || cur->parent->fd >= 0) {
+ bftw_file_openat(state, cur, cur->parent, cur->name);
}
}
- // Clear out the linked list
- for (struct bftw_file *next = cur->next; cur != file; cur = next, next = next->next) {
- cur->next = NULL;
+ return file->fd;
+}
+
+/** Close a directory, asynchronously if possible. */
+static int bftw_ioq_closedir(struct bftw_state *state, struct bfs_dir *dir) {
+ if (bftw_ioq_reserve(state) == 0) {
+ if (ioq_closedir(state->ioq, dir, NULL) == 0) {
+ return 0;
+ }
}
- return fd;
+ struct bftw_cache *cache = &state->cache;
+ int ret = bfs_closedir(dir);
+ bftw_freedir(cache, dir);
+ ++cache->capacity;
+ return ret;
}
-/**
- * Open a bftw_file as a directory.
- *
- * @param cache
- * The cache to hold the file.
- * @param file
- * The directory to open.
- * @param path
- * The full path to the directory.
- * @return
- * The opened directory, or NULL on error.
- */
-static struct bfs_dir *bftw_file_opendir(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
- int fd = bftw_file_open(cache, file, path);
- if (fd < 0) {
- return NULL;
+/** Close a file descriptor, asynchronously if possible. */
+static int bftw_ioq_close(struct bftw_state *state, int fd) {
+ if (bftw_ioq_reserve(state) == 0) {
+ if (ioq_close(state->ioq, fd, NULL) == 0) {
+ return 0;
+ }
}
- return bfs_opendir(fd, NULL);
+ struct bftw_cache *cache = &state->cache;
+ int ret = xclose(fd);
+ ++cache->capacity;
+ return ret;
}
-/** Free a bftw_file. */
-static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) {
- assert(file->refcount == 0);
+/** Close a file, asynchronously if possible. */
+static int bftw_close(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->fd >= 0);
+ bfs_assert(file->pincount == 0);
- if (file->fd >= 0) {
- bftw_file_close(cache, file);
- }
+ struct bfs_dir *dir = file->dir;
+ int fd = file->fd;
- free(file);
+ bftw_lru_remove(&state->cache, file);
+ file->dir = NULL;
+ file->fd = -1;
+
+ if (dir) {
+ return bftw_ioq_closedir(state, dir);
+ } else {
+ return bftw_ioq_close(state, fd);
+ }
}
-/**
- * A queue of bftw_file's to examine.
- */
-struct bftw_queue {
- /** The head of the queue. */
- struct bftw_file *head;
- /** The insertion target. */
- struct bftw_file **target;
-};
+/** Free an open directory. */
+static int bftw_unwrapdir(struct bftw_state *state, struct bftw_file *file) {
+ struct bfs_dir *dir = file->dir;
+ if (!dir) {
+ return 0;
+ }
-/** Initialize a bftw_queue. */
-static void bftw_queue_init(struct bftw_queue *queue) {
- queue->head = NULL;
- queue->target = &queue->head;
-}
+ struct bftw_cache *cache = &state->cache;
-/** Add a file to a bftw_queue. */
-static void bftw_queue_push(struct bftw_queue *queue, struct bftw_file *file) {
- assert(file->next == NULL);
+ // Try to keep an open fd if any children exist
+ bool reffed = file->refcount > 1;
+ // Keep the fd the same if it's pinned
+ bool pinned = file->pincount > 0;
- file->next = *queue->target;
- *queue->target = file;
- queue->target = &file->next;
+#if BFS_USE_UNWRAPDIR
+ if (reffed || pinned) {
+ bfs_unwrapdir(dir);
+ bftw_freedir(cache, dir);
+ file->dir = NULL;
+ return 0;
+ }
+#else
+ if (pinned) {
+ return -1;
+ }
+#endif
+
+ if (!reffed) {
+ return bftw_close(state, file);
+ }
+
+ // Make room for dup()
+ bftw_cache_pin(cache, file);
+ int ret = bftw_cache_reserve(state);
+ bftw_cache_unpin(cache, file);
+ if (ret != 0) {
+ return ret;
+ }
+
+ int fd = dup_cloexec(file->fd);
+ if (fd < 0) {
+ return -1;
+ }
+ --cache->capacity;
+
+ file->dir = NULL;
+ file->fd = fd;
+ return bftw_ioq_closedir(state, dir);
}
-/** Pop the next file from the head of the queue. */
-static struct bftw_file *bftw_queue_pop(struct bftw_queue *queue) {
- struct bftw_file *file = queue->head;
- queue->head = file->next;
- file->next = NULL;
- if (queue->target == &file->next) {
- queue->target = &queue->head;
+/** Try to pin a file's parent. */
+static int bftw_pin_parent(struct bftw_state *state, struct bftw_file *file) {
+ struct bftw_file *parent = file->parent;
+ if (!parent) {
+ return AT_FDCWD;
}
- return file;
+
+ int fd = parent->fd;
+ if (fd < 0) {
+ bfs_static_assert(AT_FDCWD != -1);
+ return -1;
+ }
+
+ bftw_cache_pin(&state->cache, parent);
+ return fd;
}
-/** The split phase of mergesort. */
-static struct bftw_file **bftw_sort_split(struct bftw_file **head, struct bftw_file **tail) {
- struct bftw_file **tortoise = head, **hare = head;
+/** Open a directory asynchronously. */
+static int bftw_ioq_opendir(struct bftw_state *state, struct bftw_file *file) {
+ struct bftw_cache *cache = &state->cache;
- while (*hare != *tail) {
- tortoise = &(*tortoise)->next;
- hare = &(*hare)->next;
- if (*hare != *tail) {
- hare = &(*hare)->next;
- }
+ if (bftw_ioq_reserve(state) != 0) {
+ goto fail;
+ }
+
+ int dfd = bftw_pin_parent(state, file);
+ if (dfd < 0 && dfd != AT_FDCWD) {
+ goto fail;
+ }
+
+ if (bftw_cache_reserve(state) != 0) {
+ goto unpin;
+ }
+
+ struct bfs_dir *dir = bftw_allocdir(cache, false);
+ if (!dir) {
+ goto unpin;
+ }
+
+ if (ioq_opendir(state->ioq, dir, dfd, file->name, state->dir_flags, file) != 0) {
+ goto free;
}
- return tortoise;
+ --cache->capacity;
+ return 0;
+
+free:
+ bftw_freedir(cache, dir);
+unpin:
+ bftw_unpin_parent(state, file, false);
+fail:
+ return -1;
}
-/** The merge phase of mergesort. */
-static struct bftw_file **bftw_sort_merge(struct bftw_file **head, struct bftw_file **mid, struct bftw_file **tail) {
- struct bftw_file *left = *head, *right = *mid, *end = *tail;
- *mid = NULL;
- *tail = NULL;
+/** Open a batch of directories asynchronously. */
+static void bftw_ioq_opendirs(struct bftw_state *state) {
+ while (bftw_queue_balanced(&state->dirq)) {
+ struct bftw_file *dir = bftw_queue_waiting(&state->dirq);
+ if (!dir) {
+ break;
+ }
- while (left || right) {
- struct bftw_file *next;
- if (left && (!right || strcoll(left->name, right->name) <= 0)) {
- next = left;
- left = left->next;
+ if (bftw_ioq_opendir(state, dir) == 0) {
+ bftw_queue_detach(&state->dirq, dir, true);
} else {
- next = right;
- right = right->next;
+ break;
}
-
- *head = next;
- head = &next->next;
}
+}
- *head = end;
- return head;
+/** Push a directory onto the queue. */
+static void bftw_push_dir(struct bftw_state *state, struct bftw_file *file) {
+ bfs_assert(file->type == BFS_DIR);
+ bftw_queue_push(&state->dirq, file);
+ bftw_ioq_opendirs(state);
}
-/**
- * Sort a (sub-)list of files.
- *
- * @param head
- * The head of the (sub-)list to sort.
- * @param tail
- * The tail of the (sub-)list to sort.
- * @return
- * The new tail of the (sub-)list.
- */
-static struct bftw_file **bftw_sort_files(struct bftw_file **head, struct bftw_file **tail) {
- struct bftw_file **mid = bftw_sort_split(head, tail);
- if (*mid == *head || *mid == *tail) {
- return tail;
+/** Pop a file from a queue, then activate it. */
+static bool bftw_pop(struct bftw_state *state, struct bftw_queue *queue) {
+ if (queue->size == 0) {
+ return false;
}
- mid = bftw_sort_files(head, mid);
- tail = bftw_sort_files(mid, tail);
+ while (!bftw_queue_ready(queue) && queue->ioqueued > 0) {
+ bool block = true;
+ if (bftw_queue_waiting(queue) && state->nthreads == 1) {
+ // With only one background thread, balance the work
+ // between it and the main thread
+ block = false;
+ }
+
+ if (bftw_ioq_pop(state, block) < 0) {
+ break;
+ }
+ }
- return bftw_sort_merge(head, mid, tail);
+ struct bftw_file *file = bftw_queue_pop(queue);
+ if (!file) {
+ return false;
+ }
+
+ while (file->ioqueued) {
+ bftw_ioq_pop(state, true);
+ }
+
+ state->file = file;
+ return true;
}
-/**
- * Holds the current state of the bftw() traversal.
- */
-struct bftw_state {
- /** bftw() callback. */
- bftw_callback *callback;
- /** bftw() callback data. */
- void *ptr;
- /** bftw() flags. */
- enum bftw_flags flags;
- /** Search strategy. */
- enum bftw_strategy strategy;
- /** The mount table. */
- const struct bfs_mtab *mtab;
+/** Pop a directory to read from the queue. */
+static bool bftw_pop_dir(struct bftw_state *state) {
+ bfs_assert(!state->file);
- /** The appropriate errno value, if any. */
- int error;
+ if (state->flags & BFTW_SORT) {
+ // Keep strict breadth-first order when sorting
+ if (state->strategy == BFTW_BFS && bftw_queue_ready(&state->fileq)) {
+ return false;
+ }
+ } else if (!bftw_queue_ready(&state->dirq)) {
+ // Don't block if we have files ready to visit
+ if (bftw_queue_ready(&state->fileq)) {
+ return false;
+ }
+ }
- /** The cache of open directories. */
- struct bftw_cache cache;
- /** The queue of directories left to explore. */
- struct bftw_queue queue;
- /** The start of the current batch of files. */
- struct bftw_file **batch;
+ return bftw_pop(state, &state->dirq);
+}
- /** The current path. */
- char *path;
- /** The current file. */
- struct bftw_file *file;
- /** The previous file. */
- struct bftw_file *previous;
+/** Figure out bfs_stat() flags. */
+static enum bfs_stat_flags bftw_stat_flags(const struct bftw_state *state, size_t depth) {
+ enum bftw_flags mask = BFTW_FOLLOW_ALL;
+ if (depth == 0) {
+ mask |= BFTW_FOLLOW_ROOTS;
+ }
- /** The currently open directory. */
- struct bfs_dir *dir;
- /** The current directory entry. */
- struct bfs_dirent *de;
- /** Storage for the directory entry. */
- struct bfs_dirent de_storage;
- /** Any error encountered while reading the directory. */
- int direrror;
+ if (state->flags & mask) {
+ return BFS_STAT_TRYFOLLOW;
+ } else {
+ return BFS_STAT_NOFOLLOW;
+ }
+}
- /** Extra data about the current file. */
- struct BFTW ftwbuf;
-};
+/** Check if a stat() call is necessary. */
+static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bfs_type type, const char *name) {
+ if (state->flags & BFTW_STAT) {
+ return true;
+ }
-/**
- * Initialize the bftw() state.
- */
-static int bftw_state_init(struct bftw_state *state, const struct bftw_args *args) {
- state->callback = args->callback;
- state->ptr = args->ptr;
- state->flags = args->flags;
- state->strategy = args->strategy;
- state->mtab = args->mtab;
+ switch (type) {
+ case BFS_UNKNOWN:
+ return true;
- state->error = 0;
+ case BFS_DIR:
+ return state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS);
- if (args->nopenfd < 1) {
- errno = EMFILE;
- return -1;
+ case BFS_LNK:
+ if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) {
+ return true;
+ }
+ fallthru;
+
+ default:
+#if __linux__
+ if (state->mtab && bfs_might_be_mount(state->mtab, name)) {
+ return true;
+ }
+#endif
+ return false;
}
+}
- state->path = dstralloc(0);
- if (!state->path) {
- return -1;
+/** stat() a file asynchronously. */
+static int bftw_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
+ if (bftw_ioq_reserve(state) != 0) {
+ goto fail;
}
- bftw_cache_init(&state->cache, args->nopenfd);
- bftw_queue_init(&state->queue);
- state->batch = NULL;
+ int dfd = bftw_pin_parent(state, file);
+ if (dfd < 0 && dfd != AT_FDCWD) {
+ goto fail;
+ }
- state->file = NULL;
- state->previous = NULL;
+ struct bftw_cache *cache = &state->cache;
+ struct bfs_stat *buf = arena_alloc(&cache->stat_bufs);
+ if (!buf) {
+ goto unpin;
+ }
- state->dir = NULL;
- state->de = NULL;
- state->direrror = 0;
+ enum bfs_stat_flags flags = bftw_stat_flags(state, file->depth);
+ if (ioq_stat(state->ioq, dfd, file->name, flags, buf, file) != 0) {
+ goto free;
+ }
return 0;
+
+free:
+ arena_free(&cache->stat_bufs, buf);
+unpin:
+ bftw_unpin_parent(state, file, false);
+fail:
+ return -1;
}
-/** Cached bfs_stat(). */
-static const struct bfs_stat *bftw_stat_impl(struct BFTW *ftwbuf, struct bftw_stat *cache, enum bfs_stat_flags flags) {
- if (!cache->buf) {
- if (cache->error) {
- errno = cache->error;
- } else if (bfs_stat(ftwbuf->at_fd, ftwbuf->at_path, flags, &cache->storage) == 0) {
- cache->buf = &cache->storage;
- } else {
- cache->error = errno;
- }
+/** Check if we should stat() a file asynchronously. */
+static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
+ // To avoid surprising users too much, process the roots in order
+ if (file->depth == 0) {
+ return false;
}
- return cache->buf;
+#ifdef S_IFWHT
+ // ioq_stat() does not do whiteout emulation like bftw_stat_impl()
+ if (file->type == BFS_WHT) {
+ return false;
+ }
+#endif
+
+ return bftw_must_stat(state, file->depth, file->type, file->name);
}
-const struct bfs_stat *bftw_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- struct BFTW *mutbuf = (struct BFTW *)ftwbuf;
- const struct bfs_stat *ret;
+/** Call stat() on files that need it. */
+static void bftw_stat_files(struct bftw_state *state) {
+ while (true) {
+ struct bftw_file *file = bftw_queue_waiting(&state->fileq);
+ if (!file) {
+ break;
+ }
- if (flags & BFS_STAT_NOFOLLOW) {
- ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW);
- if (ret && !S_ISLNK(ret->mode) && !mutbuf->stat_cache.buf) {
- // Non-link, so share stat info
- mutbuf->stat_cache.buf = ret;
+ if (!bftw_should_ioq_stat(state, file)) {
+ bftw_queue_skip(&state->fileq, file);
+ continue;
}
- } else {
- ret = bftw_stat_impl(mutbuf, &mutbuf->stat_cache, BFS_STAT_FOLLOW);
- if (!ret && (flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(errno)) {
- ret = bftw_stat_impl(mutbuf, &mutbuf->lstat_cache, BFS_STAT_NOFOLLOW);
+
+ if (!bftw_queue_balanced(&state->fileq)) {
+ break;
+ }
+
+ if (bftw_ioq_stat(state, file) == 0) {
+ bftw_queue_detach(&state->fileq, file, true);
+ } else {
+ break;
}
}
+}
- return ret;
+/** Push a file onto the queue. */
+static void bftw_push_file(struct bftw_state *state, struct bftw_file *file) {
+ bftw_queue_push(&state->fileq, file);
+ bftw_stat_files(state);
}
-const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- if (flags & BFS_STAT_NOFOLLOW) {
- return ftwbuf->lstat_cache.buf;
- } else if (ftwbuf->stat_cache.buf) {
- return ftwbuf->stat_cache.buf;
- } else if ((flags & BFS_STAT_TRYFOLLOW) && is_nonexistence_error(ftwbuf->stat_cache.error)) {
- return ftwbuf->lstat_cache.buf;
- } else {
- return NULL;
- }
+/** Pop a file to visit from the queue. */
+static bool bftw_pop_file(struct bftw_state *state) {
+ bfs_assert(!state->file);
+ return bftw_pop(state, &state->fileq);
}
-enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- if (flags & BFS_STAT_NOFOLLOW) {
- if (ftwbuf->type == BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- return ftwbuf->type;
- }
- } else if (flags & BFS_STAT_TRYFOLLOW) {
- if (ftwbuf->type != BFS_LNK || (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW)) {
- return ftwbuf->type;
- }
- } else {
- if (ftwbuf->type != BFS_LNK) {
- return ftwbuf->type;
- } else if (ftwbuf->stat_flags & BFS_STAT_TRYFOLLOW) {
- return BFS_ERROR;
- }
+/** Build the path to the current file. */
+static int bftw_build_path(struct bftw_state *state, const char *name) {
+ const struct bftw_file *file = state->file;
+
+ size_t pathlen = file ? file->nameoff + file->namelen : 0;
+ if (dstresize(&state->path, pathlen) != 0) {
+ state->error = errno;
+ return -1;
}
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, flags);
- if (statbuf) {
- return bfs_mode_to_type(statbuf->mode);
- } else {
- return BFS_ERROR;
+ // Try to find a common ancestor with the existing path
+ const struct bftw_file *ancestor = state->previous;
+ while (ancestor && ancestor->depth > file->depth) {
+ ancestor = ancestor->parent;
}
-}
-/**
- * Update the path for the current file.
- */
-static int bftw_update_path(struct bftw_state *state, const char *name) {
- const struct bftw_file *file = state->file;
- size_t length = file ? file->nameoff + file->namelen : 0;
+ // Build the path backwards
+ while (file && file != ancestor) {
+ if (file->nameoff > 0) {
+ state->path[file->nameoff - 1] = '/';
+ }
+ memcpy(state->path + file->nameoff, file->name, file->namelen);
- assert(dstrlen(state->path) >= length);
- dstresize(&state->path, length);
+ if (ancestor && ancestor->depth == file->depth) {
+ ancestor = ancestor->parent;
+ }
+ file = file->parent;
+ }
+
+ state->previous = state->file;
if (name) {
- if (length > 0 && state->path[length - 1] != '/') {
+ if (pathlen > 0 && state->path[pathlen - 1] != '/') {
if (dstrapp(&state->path, '/') != 0) {
+ state->error = errno;
return -1;
}
}
if (dstrcat(&state->path, name) != 0) {
+ state->error = errno;
return -1;
}
}
@@ -682,58 +1576,79 @@ static int bftw_update_path(struct bftw_state *state, const char *name) {
return 0;
}
-/** Check if a stat() call is needed for this visit. */
-static bool bftw_need_stat(const struct bftw_state *state) {
- if (state->flags & BFTW_STAT) {
- return true;
+/** Open a bftw_file as a directory. */
+static struct bfs_dir *bftw_file_opendir(struct bftw_state *state, struct bftw_file *file, const char *path) {
+ int fd = bftw_file_open(state, file, path);
+ if (fd < 0) {
+ return NULL;
}
- const struct BFTW *ftwbuf = &state->ftwbuf;
- if (ftwbuf->type == BFS_UNKNOWN) {
- return true;
+ struct bftw_cache *cache = &state->cache;
+ struct bfs_dir *dir = bftw_allocdir(cache, true);
+ if (!dir) {
+ return NULL;
}
- if (ftwbuf->type == BFS_LNK && !(ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- return true;
+ if (bfs_opendir(dir, fd, NULL, state->dir_flags) != 0) {
+ bftw_freedir(cache, dir);
+ return NULL;
}
- if (ftwbuf->type == BFS_DIR) {
- if (state->flags & (BFTW_DETECT_CYCLES | BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) {
- return true;
- }
-#if __linux__
- } else if (state->mtab) {
- // Linux fills in d_type from the underlying inode, even when
- // the directory entry is a bind mount point. In that case, we
- // need to stat() to get the correct type. We don't need to
- // check for directories because they can only be mounted over
- // by other directories.
- if (bfs_might_be_mount(state->mtab, ftwbuf->path)) {
- return true;
- }
-#endif
+ bftw_file_set_dir(cache, file, dir);
+ return dir;
+}
+
+/** Open the current directory. */
+static int bftw_opendir(struct bftw_state *state) {
+ bfs_assert(!state->dir);
+ bfs_assert(!state->de);
+
+ state->direrror = 0;
+
+ struct bftw_file *file = state->file;
+ state->dir = file->dir;
+ if (state->dir) {
+ goto pin;
}
- return false;
+ if (bftw_build_path(state, NULL) != 0) {
+ return -1;
+ }
+
+ bftw_queue_rebalance(&state->dirq, false);
+
+ state->dir = bftw_file_opendir(state, file, state->path);
+ if (!state->dir) {
+ state->direrror = errno;
+ return 0;
+ }
+
+pin:
+ bftw_cache_pin(&state->cache, file);
+ return 0;
}
-/** Initialize bftw_stat cache. */
-static void bftw_stat_init(struct bftw_stat *cache) {
- cache->buf = NULL;
- cache->error = 0;
+/** Read an entry from the current directory. */
+static int bftw_readdir(struct bftw_state *state) {
+ if (!state->dir) {
+ return -1;
+ }
+
+ int ret = bfs_readdir(state->dir, &state->de_storage);
+ if (ret > 0) {
+ state->de = &state->de_storage;
+ } else if (ret == 0) {
+ state->de = NULL;
+ } else {
+ state->de = NULL;
+ state->direrror = errno;
+ }
+
+ return ret;
}
-/**
- * Open a file if necessary.
- *
- * @param file
- * The file to open.
- * @param path
- * The path to that file or one of its descendants.
- * @return
- * The opened file descriptor, or -1 on error.
- */
-static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, const char *path) {
+/** Open a file if necessary. */
+static int bftw_ensure_open(struct bftw_state *state, struct bftw_file *file, const char *path) {
int ret = file->fd;
if (ret < 0) {
@@ -742,16 +1657,14 @@ static int bftw_ensure_open(struct bftw_cache *cache, struct bftw_file *file, co
return -1;
}
- ret = bftw_file_open(cache, file, copy);
+ ret = bftw_file_open(state, file, copy);
free(copy);
}
return ret;
}
-/**
- * Initialize the buffers with data about the current path.
- */
+/** Initialize the buffers with data about the current path. */
static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
struct bftw_file *file = state->file;
const struct bfs_dirent *de = state->de;
@@ -765,9 +1678,7 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
ftwbuf->error = state->direrror;
ftwbuf->at_fd = AT_FDCWD;
ftwbuf->at_path = ftwbuf->path;
- ftwbuf->stat_flags = BFS_STAT_NOFOLLOW;
- bftw_stat_init(&ftwbuf->lstat_cache);
- bftw_stat_init(&ftwbuf->stat_cache);
+ bftw_stat_init(&ftwbuf->stat_bufs, &state->stat_buf, &state->lstat_buf);
struct bftw_file *parent = NULL;
if (de) {
@@ -780,11 +1691,12 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
ftwbuf->depth = file->depth;
ftwbuf->type = file->type;
ftwbuf->nameoff = file->nameoff;
+ bftw_stat_fill(&ftwbuf->stat_bufs, &file->stat_bufs);
}
if (parent) {
// Try to ensure the immediate parent is open, to avoid ENAMETOOLONG
- if (bftw_ensure_open(&state->cache, parent, state->path) >= 0) {
+ if (bftw_ensure_open(state, parent, state->path) >= 0) {
ftwbuf->at_fd = parent->fd;
ftwbuf->at_path += ftwbuf->nameoff;
} else {
@@ -794,25 +1706,18 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
if (ftwbuf->depth == 0) {
// Compute the name offset for root paths like "foo/bar"
- ftwbuf->nameoff = xbasename(ftwbuf->path) - ftwbuf->path;
+ ftwbuf->nameoff = xbaseoff(ftwbuf->path);
}
+ ftwbuf->stat_flags = bftw_stat_flags(state, ftwbuf->depth);
+
if (ftwbuf->error != 0) {
ftwbuf->type = BFS_ERROR;
return;
}
- int follow_flags = BFTW_FOLLOW_ALL;
- if (ftwbuf->depth == 0) {
- follow_flags |= BFTW_FOLLOW_ROOTS;
- }
- bool follow = state->flags & follow_flags;
- if (follow) {
- ftwbuf->stat_flags = BFS_STAT_TRYFOLLOW;
- }
-
const struct bfs_stat *statbuf = NULL;
- if (bftw_need_stat(state)) {
+ if (bftw_must_stat(state, ftwbuf->depth, ftwbuf->type, ftwbuf->path + ftwbuf->nameoff)) {
statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (statbuf) {
ftwbuf->type = bfs_mode_to_type(statbuf->mode);
@@ -851,24 +1756,18 @@ static bool bftw_is_mount(struct bftw_state *state, const char *name) {
return statbuf && statbuf->dev != parent->dev;
}
-/** Fill file identity information from an ftwbuf. */
-static void bftw_fill_id(struct bftw_file *file, const struct BFTW *ftwbuf) {
- const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
- if (!statbuf || (ftwbuf->stat_flags & BFS_STAT_NOFOLLOW)) {
- statbuf = ftwbuf->lstat_cache.buf;
- }
- if (statbuf) {
- file->dev = statbuf->dev;
- file->ino = statbuf->ino;
- }
+/** Check if bfs_stat() was called from the main thread. */
+static bool bftw_stat_was_sync(const struct bftw_state *state, const struct bfs_stat *buf) {
+ return buf == &state->stat_buf || buf == &state->lstat_buf;
}
-/**
- * Visit a path, invoking the callback.
- */
-static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, enum bftw_visit visit) {
- if (bftw_update_path(state, name) != 0) {
- state->error = errno;
+/** Invoke the callback. */
+static enum bftw_action bftw_call_back(struct bftw_state *state, const char *name, enum bftw_visit visit) {
+ if (visit == BFTW_POST && !(state->flags & BFTW_POST_ORDER)) {
+ return BFTW_PRUNE;
+ }
+
+ if (bftw_build_path(state, name) != 0) {
return BFTW_STOP;
}
@@ -881,246 +1780,278 @@ static enum bftw_action bftw_visit(struct bftw_state *state, const char *name, e
return BFTW_STOP;
}
+ enum bftw_action ret = BFTW_PRUNE;
if ((state->flags & BFTW_SKIP_MOUNTS) && bftw_is_mount(state, name)) {
- return BFTW_PRUNE;
+ goto done;
}
- enum bftw_action ret = state->callback(ftwbuf, state->ptr);
+ ret = state->callback(ftwbuf, state->ptr);
switch (ret) {
case BFTW_CONTINUE:
+ if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) {
+ ret = BFTW_PRUNE;
+ } else if (state->flags & BFTW_PRUNE_MOUNTS) {
+ if (bftw_is_mount(state, name)) {
+ ret = BFTW_PRUNE;
+ }
+ }
break;
+
case BFTW_PRUNE:
case BFTW_STOP:
- goto done;
+ break;
+
default:
state->error = EINVAL;
return BFTW_STOP;
}
- if (visit != BFTW_PRE || ftwbuf->type != BFS_DIR) {
- ret = BFTW_PRUNE;
- goto done;
- }
-
- if ((state->flags & BFTW_PRUNE_MOUNTS) && bftw_is_mount(state, name)) {
- ret = BFTW_PRUNE;
- goto done;
- }
-
done:
- if (state->file && !name) {
- bftw_fill_id(state->file, ftwbuf);
+ if (state->fileq.flags & BFTW_QBALANCE) {
+ // Detect any main-thread stat() calls to rebalance the queue
+ const struct bfs_stat *buf = bftw_cached_stat(ftwbuf, BFS_STAT_FOLLOW);
+ const struct bfs_stat *lbuf = bftw_cached_stat(ftwbuf, BFS_STAT_NOFOLLOW);
+ if (bftw_stat_was_sync(state, buf) || bftw_stat_was_sync(state, lbuf)) {
+ bftw_queue_rebalance(&state->fileq, false);
+ }
}
return ret;
}
/**
- * Push a new file onto the queue.
+ * Flags controlling which files get visited when done with a directory.
*/
-static int bftw_push(struct bftw_state *state, const char *name, bool fill_id) {
- struct bftw_file *parent = state->file;
- struct bftw_file *file = bftw_file_new(parent, name);
- if (!file) {
- state->error = errno;
- return -1;
- }
+enum bftw_gc_flags {
+ /** Don't visit anything. */
+ BFTW_VISIT_NONE = 0,
+ /** Report directory errors. */
+ BFTW_VISIT_ERROR = 1 << 0,
+ /** Visit the file itself. */
+ BFTW_VISIT_FILE = 1 << 1,
+ /** Visit the file's ancestors. */
+ BFTW_VISIT_PARENTS = 1 << 2,
+ /** Visit both the file and its ancestors. */
+ BFTW_VISIT_ALL = BFTW_VISIT_ERROR | BFTW_VISIT_FILE | BFTW_VISIT_PARENTS,
+};
- if (state->de) {
- file->type = state->de->type;
- }
+/** Garbage collect the current file and its parents. */
+static int bftw_gc(struct bftw_state *state, enum bftw_gc_flags flags) {
+ int ret = 0;
- if (fill_id) {
- bftw_fill_id(file, &state->ftwbuf);
+ struct bftw_file *file = state->file;
+ if (file) {
+ if (state->dir) {
+ bftw_cache_unpin(&state->cache, file);
+ }
+ if (file->dir) {
+ bftw_delayed_unwrap(state, file);
+ }
}
+ state->dir = NULL;
+ state->de = NULL;
- bftw_queue_push(&state->queue, file);
-
- return 0;
-}
-
-/**
- * Build the path to the current file.
- */
-static int bftw_build_path(struct bftw_state *state) {
- const struct bftw_file *file = state->file;
-
- size_t pathlen = file->nameoff + file->namelen;
- if (dstresize(&state->path, pathlen) != 0) {
- state->error = errno;
- return -1;
+ if (state->direrror != 0) {
+ if (flags & BFTW_VISIT_ERROR) {
+ if (bftw_call_back(state, NULL, BFTW_PRE) == BFTW_STOP) {
+ ret = -1;
+ flags = 0;
+ }
+ } else {
+ state->error = state->direrror;
+ }
}
+ state->direrror = 0;
- // Try to find a common ancestor with the existing path
- const struct bftw_file *ancestor = state->previous;
- while (ancestor && ancestor->depth > file->depth) {
- ancestor = ancestor->parent;
+ while ((file = SLIST_POP(&state->to_close, ready))) {
+ bftw_unwrapdir(state, file);
}
- // Build the path backwards
- while (file && file != ancestor) {
- if (file->nameoff > 0) {
- state->path[file->nameoff - 1] = '/';
+ enum bftw_gc_flags visit = BFTW_VISIT_FILE;
+ while ((file = state->file)) {
+ if (--file->refcount > 0) {
+ state->file = NULL;
+ break;
}
- memcpy(state->path + file->nameoff, file->name, file->namelen);
- if (ancestor && ancestor->depth == file->depth) {
- ancestor = ancestor->parent;
+ if (flags & visit) {
+ if (bftw_call_back(state, NULL, BFTW_POST) == BFTW_STOP) {
+ ret = -1;
+ flags = 0;
+ }
}
- file = file->parent;
+ visit = BFTW_VISIT_PARENTS;
+
+ struct bftw_file *parent = file->parent;
+ if (state->previous == file) {
+ state->previous = parent;
+ }
+ state->file = parent;
+
+ if (file->fd >= 0) {
+ bftw_close(state, file);
+ }
+ bftw_file_free(&state->cache, file);
}
- state->previous = state->file;
- return 0;
+ return ret;
}
-/**
- * Pop the next file from the queue.
- */
-static int bftw_pop(struct bftw_state *state) {
- if (!state->queue.head) {
- return 0;
+/** Sort a bftw_list by filename. */
+static void bftw_list_sort(struct bftw_list *list) {
+ if (!list->head || !list->head->next) {
+ return;
}
- state->file = bftw_queue_pop(&state->queue);
+ struct bftw_list left, right;
+ SLIST_INIT(&left);
+ SLIST_INIT(&right);
- if (bftw_build_path(state) != 0) {
- return -1;
+ // Split
+ for (struct bftw_file *hare = list->head; hare && (hare = hare->next); hare = hare->next) {
+ struct bftw_file *tortoise = SLIST_POP(list);
+ SLIST_APPEND(&left, tortoise);
}
+ SLIST_EXTEND(&right, list);
- return 1;
-}
+ // Recurse
+ bftw_list_sort(&left);
+ bftw_list_sort(&right);
-/**
- * Open the current directory.
- */
-static void bftw_opendir(struct bftw_state *state) {
- assert(!state->dir);
- assert(!state->de);
-
- state->direrror = 0;
+ // Merge
+ while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) {
+ struct bftw_file *lf = left.head;
+ struct bftw_file *rf = right.head;
- state->dir = bftw_file_opendir(&state->cache, state->file, state->path);
- if (!state->dir) {
- state->direrror = errno;
+ if (strcoll(lf->name, rf->name) <= 0) {
+ SLIST_POP(&left);
+ SLIST_APPEND(list, lf);
+ } else {
+ SLIST_POP(&right);
+ SLIST_APPEND(list, rf);
+ }
}
+ SLIST_EXTEND(list, &left);
+ SLIST_EXTEND(list, &right);
}
-/**
- * Read an entry from the current directory.
- */
-static int bftw_readdir(struct bftw_state *state) {
- if (!state->dir) {
- return -1;
+/** Flush all the queue buffers. */
+static void bftw_flush(struct bftw_state *state) {
+ if (state->flags & BFTW_SORT) {
+ bftw_list_sort(&state->fileq.buffer);
}
+ bftw_queue_flush(&state->fileq);
+ bftw_stat_files(state);
- int ret = bfs_readdir(state->dir, &state->de_storage);
- if (ret > 0) {
- state->de = &state->de_storage;
- } else if (ret == 0) {
- state->de = NULL;
- } else {
- state->de = NULL;
- state->direrror = errno;
+ bftw_queue_flush(&state->dirq);
+ bftw_ioq_opendirs(state);
+}
+
+/** Close the current directory. */
+static int bftw_closedir(struct bftw_state *state) {
+ if (bftw_gc(state, BFTW_VISIT_ALL) != 0) {
+ return -1;
}
- return ret;
+ bftw_flush(state);
+ return 0;
}
-/**
- * Flags controlling which files get visited when done with a directory.
- */
-enum bftw_gc_flags {
- /** Don't visit anything. */
- BFTW_VISIT_NONE = 0,
- /** Visit the file itself. */
- BFTW_VISIT_FILE = 1 << 0,
- /** Visit the file's ancestors. */
- BFTW_VISIT_PARENTS = 1 << 1,
- /** Visit both the file and its ancestors. */
- BFTW_VISIT_ALL = BFTW_VISIT_FILE | BFTW_VISIT_PARENTS,
-};
-
-/**
- * Close the current directory.
- */
-static enum bftw_action bftw_closedir(struct bftw_state *state, enum bftw_gc_flags flags) {
- struct bftw_file *file = state->file;
- enum bftw_action ret = BFTW_CONTINUE;
+/** Fill file identity information from an ftwbuf. */
+static void bftw_save_ftwbuf(struct bftw_file *file, const struct BFTW *ftwbuf) {
+ file->type = ftwbuf->type;
- if (state->dir) {
- assert(file->fd >= 0);
+ const struct bfs_stat *statbuf = bftw_cached_stat(ftwbuf, ftwbuf->stat_flags);
+ if (statbuf) {
+ file->dev = statbuf->dev;
+ file->ino = statbuf->ino;
+ }
+}
- if (file->refcount > 1) {
- // Keep the fd around if any subdirectories exist
- file->fd = bfs_freedir(state->dir);
- } else {
- bfs_closedir(state->dir);
- file->fd = -1;
- }
+/** Check if we should buffer a file instead of visiting it. */
+static bool bftw_buffer_file(const struct bftw_state *state, const struct bftw_file *file, const char *name) {
+ if (!name) {
+ // Already buffered
+ return false;
+ }
- if (file->fd < 0) {
- bftw_cache_remove(&state->cache, file);
- }
+ if (state->flags & BFTW_BUFFER) {
+ return true;
}
- state->de = NULL;
- state->dir = NULL;
+ // If we need to call stat(), and can do it async, buffer this file
+ if (!state->ioq) {
+ return false;
+ }
- if (state->direrror != 0) {
- if (flags & BFTW_VISIT_FILE) {
- ret = bftw_visit(state, NULL, BFTW_PRE);
- } else {
- state->error = state->direrror;
- }
- state->direrror = 0;
+ if (!bftw_queue_balanced(&state->fileq)) {
+ // stat() would run synchronously anyway
+ return false;
}
- return ret;
+ size_t depth = file ? file->depth + 1 : 1;
+ enum bfs_type type = state->de ? state->de->type : BFS_UNKNOWN;
+ return bftw_must_stat(state, depth, type, name);
}
-/**
- * Finalize and free a file we're done with.
- */
-static enum bftw_action bftw_gc_file(struct bftw_state *state, enum bftw_gc_flags flags) {
- enum bftw_action ret = BFTW_CONTINUE;
+/** Visit and/or enqueue the current file. */
+static int bftw_visit(struct bftw_state *state, const char *name) {
+ struct bftw_cache *cache = &state->cache;
+ struct bftw_file *file = state->file;
+
+ if (bftw_buffer_file(state, file, name)) {
+ file = bftw_file_new(cache, file, name);
+ if (!file) {
+ state->error = errno;
+ return -1;
+ }
- if (!(state->flags & BFTW_POST_ORDER)) {
- flags = 0;
+ if (state->de) {
+ file->type = state->de->type;
+ }
+
+ bftw_push_file(state, file);
+ return 0;
}
- bool visit = flags & BFTW_VISIT_FILE;
- while (state->file) {
- struct bftw_file *file = state->file;
- if (--file->refcount > 0) {
+ switch (bftw_call_back(state, name, BFTW_PRE)) {
+ case BFTW_CONTINUE:
+ if (name) {
+ file = bftw_file_new(cache, state->file, name);
+ } else {
state->file = NULL;
- break;
}
+ if (!file) {
+ state->error = errno;
+ return -1;
+ }
+
+ bftw_save_ftwbuf(file, &state->ftwbuf);
+ bftw_stat_recycle(cache, file);
+ bftw_push_dir(state, file);
+ return 0;
- if (visit && bftw_visit(state, NULL, BFTW_POST) == BFTW_STOP) {
- ret = BFTW_STOP;
- flags &= ~BFTW_VISIT_PARENTS;
+ case BFTW_PRUNE:
+ if (file && !name) {
+ return bftw_gc(state, BFTW_VISIT_PARENTS);
+ } else {
+ return 0;
}
- visit = flags & BFTW_VISIT_PARENTS;
- struct bftw_file *parent = file->parent;
- if (state->previous == file) {
- state->previous = parent;
+ default:
+ if (file && !name) {
+ bftw_gc(state, BFTW_VISIT_NONE);
}
- bftw_file_free(&state->cache, file);
- state->file = parent;
+ return -1;
}
-
- return ret;
}
-/**
- * Drain all the entries from a bftw_queue.
- */
-static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue) {
- while (queue->head) {
- state->file = bftw_queue_pop(queue);
- bftw_gc_file(state, BFTW_VISIT_NONE);
+/** Drain a bftw_queue. */
+static void bftw_drain(struct bftw_state *state, struct bftw_queue *queue) {
+ bftw_queue_flush(queue);
+
+ while (bftw_pop(state, queue)) {
+ bftw_gc(state, BFTW_VISIT_NONE);
}
}
@@ -1133,10 +2064,18 @@ static void bftw_drain_queue(struct bftw_state *state, struct bftw_queue *queue)
static int bftw_state_destroy(struct bftw_state *state) {
dstrfree(state->path);
- bftw_closedir(state, BFTW_VISIT_NONE);
+ struct ioq *ioq = state->ioq;
+ if (ioq) {
+ ioq_cancel(ioq);
+ while (bftw_ioq_pop(state, true) >= 0);
+ state->ioq = NULL;
+ }
+
+ bftw_gc(state, BFTW_VISIT_NONE);
+ bftw_drain(state, &state->dirq);
+ bftw_drain(state, &state->fileq);
- bftw_gc_file(state, BFTW_VISIT_NONE);
- bftw_drain_queue(state, &state->queue);
+ ioq_destroy(ioq);
bftw_cache_destroy(&state->cache);
@@ -1144,152 +2083,63 @@ static int bftw_state_destroy(struct bftw_state *state) {
return state->error ? -1 : 0;
}
-/** Start a batch of files. */
-static void bftw_batch_start(struct bftw_state *state) {
- if (state->strategy == BFTW_DFS) {
- state->queue.target = &state->queue.head;
- }
- state->batch = state->queue.target;
-}
-
-/** Finish adding a batch of files. */
-static void bftw_batch_finish(struct bftw_state *state) {
- if (state->flags & BFTW_SORT) {
- state->queue.target = bftw_sort_files(state->batch, state->queue.target);
- }
-}
-
/**
- * Streaming mode: visit files as they are encountered.
+ * Shared implementation for all search strategies.
*/
-static int bftw_stream(const struct bftw_args *args) {
- struct bftw_state state;
- if (bftw_state_init(&state, args) != 0) {
- return -1;
- }
-
- assert(!(state.flags & (BFTW_SORT | BFTW_BUFFER)));
-
- bftw_batch_start(&state);
- for (size_t i = 0; i < args->npaths; ++i) {
- const char *path = args->paths[i];
-
- switch (bftw_visit(&state, path, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- continue;
- case BFTW_STOP:
- goto done;
- }
-
- if (bftw_push(&state, path, true) != 0) {
- goto done;
+static int bftw_impl(struct bftw_state *state) {
+ for (size_t i = 0; i < state->npaths; ++i) {
+ if (bftw_visit(state, state->paths[i]) != 0) {
+ return -1;
}
}
- bftw_batch_finish(&state);
-
- while (bftw_pop(&state) > 0) {
- bftw_opendir(&state);
+ bftw_flush(state);
- bftw_batch_start(&state);
- while (bftw_readdir(&state) > 0) {
- const char *name = state.de->name;
-
- switch (bftw_visit(&state, name, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- continue;
- case BFTW_STOP:
- goto done;
+ while (true) {
+ while (bftw_pop_dir(state)) {
+ if (bftw_opendir(state) != 0) {
+ return -1;
}
-
- if (bftw_push(&state, name, true) != 0) {
- goto done;
+ while (bftw_readdir(state) > 0) {
+ if (bftw_visit(state, state->de->name) != 0) {
+ return -1;
+ }
+ }
+ if (bftw_closedir(state) != 0) {
+ return -1;
}
}
- bftw_batch_finish(&state);
- if (bftw_closedir(&state, BFTW_VISIT_ALL) == BFTW_STOP) {
- goto done;
+ if (!bftw_pop_file(state)) {
+ break;
}
- if (bftw_gc_file(&state, BFTW_VISIT_ALL) == BFTW_STOP) {
- goto done;
+ if (bftw_visit(state, NULL) != 0) {
+ return -1;
}
+ bftw_flush(state);
}
-done:
- return bftw_state_destroy(&state);
+ return 0;
}
/**
- * Batching mode: queue up all children before visiting them.
+ * bftw() implementation for simple breadth-/depth-first search.
*/
-static int bftw_batch(const struct bftw_args *args) {
+static int bftw_walk(const struct bftw_args *args) {
struct bftw_state state;
if (bftw_state_init(&state, args) != 0) {
return -1;
}
- bftw_batch_start(&state);
- for (size_t i = 0; i < args->npaths; ++i) {
- if (bftw_push(&state, args->paths[i], false) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- while (bftw_pop(&state) > 0) {
- enum bftw_gc_flags gcflags = BFTW_VISIT_ALL;
-
- switch (bftw_visit(&state, NULL, BFTW_PRE)) {
- case BFTW_CONTINUE:
- break;
- case BFTW_PRUNE:
- gcflags &= ~BFTW_VISIT_FILE;
- goto next;
- case BFTW_STOP:
- goto done;
- }
-
- bftw_opendir(&state);
-
- bftw_batch_start(&state);
- while (bftw_readdir(&state) > 0) {
- if (bftw_push(&state, state.de->name, false) != 0) {
- goto done;
- }
- }
- bftw_batch_finish(&state);
-
- if (bftw_closedir(&state, gcflags) == BFTW_STOP) {
- goto done;
- }
-
- next:
- if (bftw_gc_file(&state, gcflags) == BFTW_STOP) {
- goto done;
- }
- }
-
-done:
+ bftw_impl(&state);
return bftw_state_destroy(&state);
}
-/** Select bftw_stream() or bftw_batch() appropriately. */
-static int bftw_auto(const struct bftw_args *args) {
- if (args->flags & (BFTW_SORT | BFTW_BUFFER)) {
- return bftw_batch(args);
- } else {
- return bftw_stream(args);
- }
-}
-
/**
* Iterative deepening search state.
*/
struct bftw_ids_state {
+ /** Nested walk state. */
+ struct bftw_state nested;
/** The wrapped callback. */
bftw_callback *delegate;
/** The wrapped callback arguments. */
@@ -1304,12 +2154,8 @@ struct bftw_ids_state {
size_t max_depth;
/** The set of pruned paths. */
struct trie pruned;
- /** An error code to report. */
- int error;
/** Whether the bottom has been found. */
bool bottom;
- /** Whether to quit the search. */
- bool quit;
};
/** Iterative deepening callback function. */
@@ -1353,17 +2199,17 @@ static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr)
ret = BFTW_PRUNE;
}
break;
+
case BFTW_PRUNE:
if (ftwbuf->type == BFS_DIR) {
if (!trie_insert_str(&state->pruned, ftwbuf->path)) {
- state->error = errno;
- state->quit = true;
+ state->nested.error = errno;
ret = BFTW_STOP;
}
}
break;
+
case BFTW_STOP:
- state->quit = true;
break;
}
@@ -1371,7 +2217,7 @@ static enum bftw_action bftw_ids_callback(const struct BFTW *ftwbuf, void *ptr)
}
/** Initialize iterative deepening state. */
-static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *state, struct bftw_args *ids_args) {
+static int bftw_ids_init(struct bftw_ids_state *state, const struct bftw_args *args) {
state->delegate = args->callback;
state->ptr = args->ptr;
state->visit = BFTW_PRE;
@@ -1379,31 +2225,19 @@ static void bftw_ids_init(const struct bftw_args *args, struct bftw_ids_state *s
state->min_depth = 0;
state->max_depth = 1;
trie_init(&state->pruned);
- state->error = 0;
state->bottom = false;
- state->quit = false;
- *ids_args = *args;
- ids_args->callback = bftw_ids_callback;
- ids_args->ptr = state;
- ids_args->flags &= ~BFTW_POST_ORDER;
- ids_args->strategy = BFTW_DFS;
+ struct bftw_args ids_args = *args;
+ ids_args.callback = bftw_ids_callback;
+ ids_args.ptr = state;
+ ids_args.flags &= ~BFTW_POST_ORDER;
+ return bftw_state_init(&state->nested, &ids_args);
}
/** Finish an iterative deepening search. */
-static int bftw_ids_finish(struct bftw_ids_state *state) {
- int ret = 0;
-
- if (state->error) {
- ret = -1;
- } else {
- state->error = errno;
- }
-
+static int bftw_ids_destroy(struct bftw_ids_state *state) {
trie_destroy(&state->pruned);
-
- errno = state->error;
- return ret;
+ return bftw_state_destroy(&state->nested);
}
/**
@@ -1411,15 +2245,15 @@ static int bftw_ids_finish(struct bftw_ids_state *state) {
*/
static int bftw_ids(const struct bftw_args *args) {
struct bftw_ids_state state;
- struct bftw_args ids_args;
- bftw_ids_init(args, &state, &ids_args);
+ if (bftw_ids_init(&state, args) != 0) {
+ return -1;
+ }
- while (!state.quit && !state.bottom) {
+ while (!state.bottom) {
state.bottom = true;
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
}
++state.min_depth;
@@ -1430,18 +2264,18 @@ static int bftw_ids(const struct bftw_args *args) {
state.visit = BFTW_POST;
state.force_visit = true;
- while (!state.quit && state.min_depth > 0) {
+ while (state.min_depth > 0) {
--state.max_depth;
--state.min_depth;
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
}
}
}
- return bftw_ids_finish(&state);
+done:
+ return bftw_ids_destroy(&state);
}
/**
@@ -1449,40 +2283,38 @@ static int bftw_ids(const struct bftw_args *args) {
*/
static int bftw_eds(const struct bftw_args *args) {
struct bftw_ids_state state;
- struct bftw_args ids_args;
- bftw_ids_init(args, &state, &ids_args);
+ if (bftw_ids_init(&state, args) != 0) {
+ return -1;
+ }
- while (!state.quit && !state.bottom) {
+ while (!state.bottom) {
state.bottom = true;
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- state.quit = true;
+ if (bftw_impl(&state.nested) != 0) {
+ goto done;
}
state.min_depth = state.max_depth;
state.max_depth *= 2;
}
- if (!state.quit && (args->flags & BFTW_POST_ORDER)) {
+ if (args->flags & BFTW_POST_ORDER) {
state.visit = BFTW_POST;
state.min_depth = 0;
- ids_args.flags |= BFTW_POST_ORDER;
+ state.nested.flags |= BFTW_POST_ORDER;
- if (bftw_auto(&ids_args) != 0) {
- state.error = errno;
- }
+ bftw_impl(&state.nested);
}
- return bftw_ids_finish(&state);
+done:
+ return bftw_ids_destroy(&state);
}
int bftw(const struct bftw_args *args) {
switch (args->strategy) {
case BFTW_BFS:
- return bftw_auto(args);
case BFTW_DFS:
- return bftw_batch(args);
+ return bftw_walk(args);
case BFTW_IDS:
return bftw_ids(args);
case BFTW_EDS:
diff --git a/src/bftw.h b/src/bftw.h
index c458e1b..8656ca7 100644
--- a/src/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().
@@ -39,12 +26,14 @@ enum bftw_visit {
* Cached bfs_stat() info for a file.
*/
struct bftw_stat {
- /** A pointer to the bfs_stat() buffer, if available. */
- const struct bfs_stat *buf;
- /** Storage for the bfs_stat() buffer, if needed. */
- struct bfs_stat storage;
- /** The cached error code, if any. */
- int error;
+ /** The bfs_stat(BFS_STAT_FOLLOW) buffer. */
+ const struct bfs_stat *stat_buf;
+ /** The bfs_stat(BFS_STAT_NOFOLLOW) buffer. */
+ const struct bfs_stat *lstat_buf;
+ /** The cached bfs_stat(BFS_STAT_FOLLOW) error. */
+ int stat_err;
+ /** The cached bfs_stat(BFS_STAT_NOFOLLOW) error. */
+ int lstat_err;
};
/**
@@ -65,7 +54,7 @@ struct BFTW {
/** The file type. */
enum bfs_type type;
- /** The errno that occurred, if type == BFTW_ERROR. */
+ /** The errno that occurred, if type == BFS_ERROR. */
int error;
/** A parent file descriptor for the *at() family of calls. */
@@ -75,10 +64,8 @@ struct BFTW {
/** Flags for bfs_stat(). */
enum bfs_stat_flags stat_flags;
- /** Cached bfs_stat() info for BFS_STAT_NOFOLLOW. */
- struct bftw_stat lstat_cache;
- /** Cached bfs_stat() info for BFS_STAT_FOLLOW. */
- struct bftw_stat stat_cache;
+ /** Cached bfs_stat() info. */
+ struct bftw_stat stat_bufs;
};
/**
@@ -117,7 +104,7 @@ const struct bfs_stat *bftw_cached_stat(const struct BFTW *ftwbuf, enum bfs_stat
* @param flags
* flags for bfs_stat(). Pass ftwbuf->stat_flags for the default flags.
* @return
- * The type of the file, or BFTW_ERROR if an error occurred.
+ * The type of the file, or BFS_ERROR if an error occurred.
*/
enum bfs_type bftw_type(const struct BFTW *ftwbuf, enum bfs_stat_flags flags);
@@ -169,6 +156,8 @@ enum bftw_flags {
BFTW_SORT = 1 << 8,
/** Read each directory into memory before processing its children. */
BFTW_BUFFER = 1 << 9,
+ /** Include whiteouts in the search results. */
+ BFTW_WHITEOUTS = 1 << 10,
};
/**
@@ -193,16 +182,22 @@ struct bftw_args {
const char **paths;
/** The number of starting paths. */
size_t npaths;
+
/** The callback to invoke. */
bftw_callback *callback;
/** A pointer which is passed to the callback. */
void *ptr;
+
/** The maximum number of file descriptors to keep open. */
int nopenfd;
+ /** The maximum number of threads to use. */
+ int nthreads;
+
/** Flags that control bftw() behaviour. */
enum bftw_flags flags;
/** The search strategy to use. */
enum bftw_strategy strategy;
+
/** The parsed mount table, if available. */
const struct bfs_mtab *mtab;
};
diff --git a/src/bit.h b/src/bit.h
new file mode 100644
index 0000000..17cfbcf
--- /dev/null
+++ b/src/bit.h
@@ -0,0 +1,401 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Bits & bytes.
+ */
+
+#ifndef BFS_BIT_H
+#define BFS_BIT_H
+
+#include "prelude.h"
+#include <limits.h>
+#include <stdint.h>
+
+#if __STDC_VERSION__ >= C23
+# include <stdbit.h>
+#endif
+
+// C23 polyfill: _WIDTH macros
+
+// The U*_MAX macros are of the form 2**n - 1, and we want to extract the n.
+// One way would be *_WIDTH = popcount(*_MAX). Alternatively, we can use
+// Hallvard B. Furuseth's technique from [1], which is shorter.
+//
+// [1]: https://groups.google.com/g/comp.lang.c/c/NfedEFBFJ0k
+
+// Let mask be of the form 2**m - 1, e.g. 0b111, and let n range over
+// [0b0, 0b1, 0b11, 0b111, 0b1111, ...]. Then we have
+//
+// n % 0b111
+// == [0b0, 0b1, 0b11, 0b0, 0b1, 0b11, ...]
+// n / (n % 0b111 + 1)
+// == [0b0 (x3), 0b111 (x3), 0b111111 (x3), ...]
+// n / (n % 0b111 + 1) / 0b111
+// == [0b0 (x3), 0b1 (x3), 0b1001 (x3), 0b1001001 (x3), ...]
+// n / (n % 0b111 + 1) / 0b111 % 0b111
+// == [0 (x3), 1 (x3), 2 (x3), ...]
+// == UMAX_CHUNK(n, 0b111)
+#define UMAX_CHUNK(n, mask) (n / (n % mask + 1) / mask % mask)
+
+// 8 * UMAX_CHUNK(n, 255) gives [0 (x8), 8 (x8), 16 (x8), ...]. To that we add
+// [0, 1, 2, ..., 6, 7, 0, 1, ...], which we get from a linear interpolation on
+// n % 255:
+//
+// n % 255
+// == [0, 1, 3, 7, 15, 31, 63, 127, 0, ...]
+// 86 / (n % 255 + 12)
+// == [7, 6, 5, 4, 3, 2, 1, 0, 7, ...]
+#define UMAX_INTERP(n) (7 - 86 / (n % 255 + 12))
+
+#define UMAX_WIDTH(n) (8 * UMAX_CHUNK(n, 255) + UMAX_INTERP(n))
+
+#ifndef CHAR_WIDTH
+# define CHAR_WIDTH CHAR_BIT
+#endif
+
+// See https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
+
+#ifndef USHRT_WIDTH
+# ifdef __SHRT_WIDTH__
+# define USHRT_WIDTH __SHRT_WIDTH__
+# else
+# define USHRT_WIDTH UMAX_WIDTH(USHRT_MAX)
+# endif
+#endif
+
+#ifndef UINT_WIDTH
+# ifdef __INT_WIDTH__
+# define UINT_WIDTH __INT_WIDTH__
+# else
+# define UINT_WIDTH UMAX_WIDTH(UINT_MAX)
+# endif
+#endif
+
+#ifndef ULONG_WIDTH
+# ifdef __LONG_WIDTH__
+# define ULONG_WIDTH __LONG_WIDTH__
+# else
+# define ULONG_WIDTH UMAX_WIDTH(ULONG_MAX)
+# endif
+#endif
+
+#ifndef ULLONG_WIDTH
+# ifdef __LONG_LONG_WIDTH__
+# define ULLONG_WIDTH __LONG_LONG_WIDTH__
+# elif defined(__LLONG_WIDTH__) // Clang
+# define ULLONG_WIDTH __LLONG_WIDTH__
+# else
+# define ULLONG_WIDTH UMAX_WIDTH(ULLONG_MAX)
+# endif
+#endif
+
+#ifndef SIZE_WIDTH
+# ifdef __SIZE_WIDTH__
+# define SIZE_WIDTH __SIZE_WIDTH__
+# else
+# define SIZE_WIDTH UMAX_WIDTH(SIZE_MAX)
+# endif
+#endif
+
+#ifndef PTRDIFF_WIDTH
+# ifdef __PTRDIFF_WIDTH__
+# define PTRDIFF_WIDTH __PTRDIFF_WIDTH__
+# else
+# define PTRDIFF_WIDTH UMAX_WIDTH(PTRDIFF_MAX)
+# endif
+#endif
+
+#ifndef UINTPTR_WIDTH
+# ifdef __INTPTR_WIDTH__
+# define UINTPTR_WIDTH __INTPTR_WIDTH__
+# else
+# define UINTPTR_WIDTH UMAX_WIDTH(UINTPTR_MAX)
+# endif
+#endif
+
+#ifndef UINTMAX_WIDTH
+# ifdef __INTMAX_WIDTH__
+# define UINTMAX_WIDTH __INTMAX_WIDTH__
+# else
+# define UINTMAX_WIDTH UMAX_WIDTH(UINTMAX_MAX)
+# endif
+#endif
+
+#ifndef UCHAR_WIDTH
+# define UCHAR_WIDTH CHAR_WIDTH
+#endif
+#ifndef SCHAR_WIDTH
+# define SCHAR_WIDTH CHAR_WIDTH
+#endif
+#ifndef SHRT_WIDTH
+# define SHRT_WIDTH USHRT_WIDTH
+#endif
+#ifndef INT_WIDTH
+# define INT_WIDTH UINT_WIDTH
+#endif
+#ifndef LONG_WIDTH
+# define LONG_WIDTH ULONG_WIDTH
+#endif
+#ifndef LLONG_WIDTH
+# define LLONG_WIDTH ULLONG_WIDTH
+#endif
+#ifndef INTPTR_WIDTH
+# define INTPTR_WIDTH UINTPTR_WIDTH
+#endif
+#ifndef INTMAX_WIDTH
+# define INTMAX_WIDTH UINTMAX_WIDTH
+#endif
+
+// C23 polyfill: byte order
+
+#ifdef __STDC_ENDIAN_LITTLE__
+# define ENDIAN_LITTLE __STDC_ENDIAN_LITTLE__
+#elif defined(__ORDER_LITTLE_ENDIAN__)
+# define ENDIAN_LITTLE __ORDER_LITTLE_ENDIAN__
+#else
+# define ENDIAN_LITTLE 1234
+#endif
+
+#ifdef __STDC_ENDIAN_BIG__
+# define ENDIAN_BIG __STDC_ENDIAN_BIG__
+#elif defined(__ORDER_BIG_ENDIAN__)
+# define ENDIAN_BIG __ORDER_BIG_ENDIAN__
+#else
+# define ENDIAN_BIG 4321
+#endif
+
+#ifdef __STDC_ENDIAN_NATIVE__
+# define ENDIAN_NATIVE __STDC_ENDIAN_NATIVE__
+#elif defined(__BYTE_ORDER__)
+# define ENDIAN_NATIVE __BYTE_ORDER__
+#else
+# define ENDIAN_NATIVE 0
+#endif
+
+#if __STDC_VERSION__ >= C23
+# define bswap_u16 stdc_memreverse8u16
+# define bswap_u32 stdc_memreverse8u32
+# define bswap_u64 stdc_memreverse8u64
+#elif __GNUC__
+# define bswap_u16 __builtin_bswap16
+# define bswap_u32 __builtin_bswap32
+# define bswap_u64 __builtin_bswap64
+#else
+
+static inline uint16_t bswap_u16(uint16_t n) {
+ return (n << 8) | (n >> 8);
+}
+
+static inline uint32_t bswap_u32(uint32_t n) {
+ return ((uint32_t)bswap_u16(n) << 16) | bswap_u16(n >> 16);
+}
+
+static inline uint64_t bswap_u64(uint64_t n) {
+ return ((uint64_t)bswap_u32(n) << 32) | bswap_u32(n >> 32);
+}
+
+#endif
+
+static inline uint8_t bswap_u8(uint8_t n) {
+ return n;
+}
+
+/**
+ * Reverse the byte order of an integer.
+ */
+#define bswap(n) \
+ _Generic((n), \
+ uint8_t: bswap_u8, \
+ uint16_t: bswap_u16, \
+ uint32_t: bswap_u32, \
+ uint64_t: bswap_u64)(n)
+
+// Define an overload for each unsigned type
+#define UINT_OVERLOADS(macro) \
+ macro(unsigned char, _uc, UCHAR_WIDTH) \
+ macro(unsigned short, _us, USHRT_WIDTH) \
+ macro(unsigned int, _ui, UINT_WIDTH) \
+ macro(unsigned long, _ul, ULONG_WIDTH) \
+ macro(unsigned long long, _ull, ULLONG_WIDTH)
+
+// Select an overload based on an unsigned integer type
+#define UINT_SELECT(n, name) \
+ _Generic((n), \
+ char: name##_uc, \
+ signed char: name##_uc, \
+ unsigned char: name##_uc, \
+ signed short: name##_us, \
+ unsigned short: name##_us, \
+ signed int: name##_ui, \
+ unsigned int: name##_ui, \
+ signed long: name##_ul, \
+ unsigned long: name##_ul, \
+ signed long long: name##_ull, \
+ unsigned long long: name##_ull)
+
+// C23 polyfill: bit utilities
+
+#if __STDC_VERSION__ >= C23
+# define count_ones stdc_count_ones
+# define count_zeros stdc_count_zeros
+# define rotate_left stdc_rotate_left
+# define rotate_right stdc_rotate_right
+# define leading_zeros stdc_leading_zeros
+# define leading_ones stdc_leading_ones
+# define trailing_zeros stdc_trailing_zeros
+# define trailing_ones stdc_trailing_ones
+# define first_leading_zero stdc_first_leading_zero
+# define first_leading_one stdc_first_leading_one
+# define first_trailing_zero stdc_first_trailing_zero
+# define first_trailing_one stdc_first_trailing_one
+# define has_single_bit stdc_has_single_bit
+# define bit_width stdc_bit_width
+# define bit_ceil stdc_bit_ceil
+# define bit_floor stdc_bit_floor
+#else
+
+#if __GNUC__
+
+// GCC provides builtins for unsigned {int,long,long long}, so promote char/short
+#define UINT_BUILTIN_uc(name) __builtin_##name
+#define UINT_BUILTIN_us(name) __builtin_##name
+#define UINT_BUILTIN_ui(name) __builtin_##name
+#define UINT_BUILTIN_ul(name) __builtin_##name##l
+#define UINT_BUILTIN_ull(name) __builtin_##name##ll
+#define UINT_BUILTIN(name, suffix) UINT_BUILTIN##suffix(name)
+
+#define BUILTIN_WIDTH_uc UINT_WIDTH
+#define BUILTIN_WIDTH_us UINT_WIDTH
+#define BUILTIN_WIDTH_ui UINT_WIDTH
+#define BUILTIN_WIDTH_ul ULONG_WIDTH
+#define BUILTIN_WIDTH_ull ULLONG_WIDTH
+#define BUILTIN_WIDTH(suffix) BUILTIN_WIDTH##suffix
+
+#define COUNT_ONES(type, suffix, width) \
+ static inline int count_ones##suffix(type n) { \
+ return UINT_BUILTIN(popcount, suffix)(n); \
+ }
+
+#define LEADING_ZEROS(type, suffix, width) \
+ static inline int leading_zeros##suffix(type n) { \
+ return n \
+ ? UINT_BUILTIN(clz, suffix)(n) - (BUILTIN_WIDTH(suffix) - width) \
+ : width; \
+ }
+
+#define TRAILING_ZEROS(type, suffix, width) \
+ static inline int trailing_zeros##suffix(type n) { \
+ return n ? UINT_BUILTIN(ctz, suffix)(n) : (int)width; \
+ }
+
+#define FIRST_TRAILING_ONE(type, suffix, width) \
+ static inline int first_trailing_one##suffix(type n) { \
+ return UINT_BUILTIN(ffs, suffix)(n); \
+ }
+
+#else // !__GNUC__
+
+#define COUNT_ONES(type, suffix, width) \
+ static inline int count_ones##suffix(type n) { \
+ int ret; \
+ for (ret = 0; n; ++ret) { \
+ n &= n - 1; \
+ } \
+ return ret; \
+ }
+
+#define LEADING_ZEROS(type, suffix, width) \
+ static inline int leading_zeros##suffix(type n) { \
+ type bit = (type)1 << (width - 1); \
+ int ret; \
+ for (ret = 0; bit && !(n & bit); ++ret, bit >>= 1); \
+ return ret; \
+ }
+
+#define TRAILING_ZEROS(type, suffix, width) \
+ static inline int trailing_zeros##suffix(type n) { \
+ type bit = 1; \
+ int ret; \
+ for (ret = 0; bit && !(n & bit); ++ret, bit <<= 1); \
+ return ret; \
+ }
+
+#define FIRST_TRAILING_ONE(type, suffix, width) \
+ static inline int first_trailing_one##suffix(type n) { \
+ return n ? trailing_zeros##suffix(n) + 1 : 0; \
+ }
+
+#endif // !__GNUC__
+
+UINT_OVERLOADS(COUNT_ONES)
+UINT_OVERLOADS(LEADING_ZEROS)
+UINT_OVERLOADS(TRAILING_ZEROS)
+UINT_OVERLOADS(FIRST_TRAILING_ONE)
+
+#define ROTATE_LEFT(type, suffix, width) \
+ static inline type rotate_left##suffix(type n, int c) { \
+ return (n << c) | (n >> ((width - c) % width)); \
+ }
+
+#define ROTATE_RIGHT(type, suffix, width) \
+ static inline type rotate_right##suffix(type n, int c) { \
+ return (n >> c) | (n << ((width - c) % width)); \
+ }
+
+#define FIRST_LEADING_ONE(type, suffix, width) \
+ static inline int first_leading_one##suffix(type n) { \
+ return width - leading_zeros##suffix(n); \
+ }
+
+#define HAS_SINGLE_BIT(type, suffix, width) \
+ static inline bool has_single_bit##suffix(type n) { \
+ /** Branchless n && !(n & (n - 1)) */ \
+ return n - 1 < (n ^ (n - 1)); \
+ }
+
+UINT_OVERLOADS(ROTATE_LEFT)
+UINT_OVERLOADS(ROTATE_RIGHT)
+UINT_OVERLOADS(FIRST_LEADING_ONE)
+UINT_OVERLOADS(HAS_SINGLE_BIT)
+
+#define count_ones(n) UINT_SELECT(n, count_ones)(n)
+#define count_zeros(n) UINT_SELECT(n, count_ones)(~(n))
+
+#define rotate_left(n, c) UINT_SELECT(n, rotate_left)(n, c)
+#define rotate_right(n, c) UINT_SELECT(n, rotate_right)(n, c)
+
+#define leading_zeros(n) UINT_SELECT(n, leading_zeros)(n)
+#define leading_ones(n) UINT_SELECT(n, leading_zeros)(~(n))
+
+#define trailing_zeros(n) UINT_SELECT(n, trailing_zeros)(n)
+#define trailing_ones(n) UINT_SELECT(n, trailing_zeros)(~(n))
+
+#define first_leading_one(n) UINT_SELECT(n, first_leading_one)(n)
+#define first_leading_zero(n) UINT_SELECT(n, first_leading_one)(~(n))
+
+#define first_trailing_one(n) UINT_SELECT(n, first_trailing_one)(n)
+#define first_trailing_zero(n) UINT_SELECT(n, first_trailing_one)(~(n))
+
+#define has_single_bit(n) UINT_SELECT(n, has_single_bit)(n)
+
+#define BIT_FLOOR(type, suffix, width) \
+ static inline type bit_floor##suffix(type n) { \
+ return n ? (type)1 << (first_leading_one##suffix(n) - 1) : 0; \
+ }
+
+#define BIT_CEIL(type, suffix, width) \
+ static inline type bit_ceil##suffix(type n) { \
+ return (type)1 << first_leading_one##suffix(n - !!n); \
+ }
+
+UINT_OVERLOADS(BIT_FLOOR)
+UINT_OVERLOADS(BIT_CEIL)
+
+#define bit_width(n) first_leading_one(n)
+#define bit_floor(n) UINT_SELECT(n, bit_floor)(n)
+#define bit_ceil(n) UINT_SELECT(n, bit_ceil)(n)
+
+#endif // __STDC_VERSION__ < C23
+
+#endif // BFS_BIT_H
diff --git a/src/color.c b/src/color.c
index c510fa8..f004bf2 100644
--- a/src/color.c
+++ b/src/color.c
@@ -1,197 +1,399 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "color.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "bftw.h"
+#include "diag.h"
#include "dir.h"
#include "dstring.h"
#include "expr.h"
#include "fsade.h"
#include "stat.h"
#include "trie.h"
-#include "util.h"
-#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
-#include <stdbool.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
+/**
+ * An escape sequence, which may contain embedded NUL bytes.
+ */
+struct esc_seq {
+ /** The length of the escape sequence. */
+ size_t len;
+ /** The escape sequence iteself, without a terminating NUL. */
+ char seq[];
+};
+
+/**
+ * A colored file extension, like `*.tar=01;31`.
+ */
+struct ext_color {
+ /** Priority, to disambiguate case-sensitive and insensitive matches. */
+ size_t priority;
+ /** The escape sequence associated with this extension. */
+ struct esc_seq *esc;
+ /** The length of the extension to match. */
+ size_t len;
+ /** Whether the comparison should be case-sensitive. */
+ bool case_sensitive;
+ /** The extension to match (NUL-terminated). */
+ char ext[];
+};
+
struct colors {
- char *reset;
- char *leftcode;
- char *rightcode;
- char *endcode;
- char *clear_to_eol;
-
- char *bold;
- char *gray;
- char *red;
- char *green;
- char *yellow;
- char *blue;
- char *magenta;
- char *cyan;
- char *white;
-
- char *warning;
- char *error;
-
- char *normal;
-
- char *file;
- char *multi_hard;
- char *executable;
- char *capable;
- char *setgid;
- char *setuid;
-
- char *directory;
- char *sticky;
- char *other_writable;
- char *sticky_other_writable;
-
- char *link;
- char *orphan;
- char *missing;
+ /** esc_seq allocator. */
+ struct varena esc_arena;
+ /** ext_color allocator. */
+ struct varena ext_arena;
+
+ // Known dircolors keys
+
+ struct esc_seq *reset;
+ struct esc_seq *leftcode;
+ struct esc_seq *rightcode;
+ struct esc_seq *endcode;
+ struct esc_seq *clear_to_eol;
+
+ struct esc_seq *bold;
+ struct esc_seq *gray;
+ struct esc_seq *red;
+ struct esc_seq *green;
+ struct esc_seq *yellow;
+ struct esc_seq *blue;
+ struct esc_seq *magenta;
+ struct esc_seq *cyan;
+ struct esc_seq *white;
+
+ struct esc_seq *warning;
+ struct esc_seq *error;
+
+ struct esc_seq *normal;
+
+ struct esc_seq *file;
+ struct esc_seq *multi_hard;
+ struct esc_seq *executable;
+ struct esc_seq *capable;
+ struct esc_seq *setgid;
+ struct esc_seq *setuid;
+
+ struct esc_seq *directory;
+ struct esc_seq *sticky;
+ struct esc_seq *other_writable;
+ struct esc_seq *sticky_other_writable;
+
+ struct esc_seq *link;
+ struct esc_seq *orphan;
+ struct esc_seq *missing;
bool link_as_target;
- char *blockdev;
- char *chardev;
- char *door;
- char *pipe;
- char *socket;
+ struct esc_seq *blockdev;
+ struct esc_seq *chardev;
+ struct esc_seq *door;
+ struct esc_seq *pipe;
+ struct esc_seq *socket;
/** A mapping from color names (fi, di, ln, etc.) to struct fields. */
struct trie names;
- /** A mapping from file extensions to colors. */
- struct trie ext_colors;
+ /** Number of extensions. */
+ size_t ext_count;
+ /** Longest extension. */
+ size_t ext_len;
+ /** Case-sensitive extension trie. */
+ struct trie ext_trie;
+ /** Case-insensitive extension trie. */
+ struct trie iext_trie;
};
+/** Allocate an escape sequence. */
+static struct esc_seq *new_esc(struct colors *colors, const char *seq, size_t len) {
+ struct esc_seq *esc = varena_alloc(&colors->esc_arena, len);
+ if (esc) {
+ esc->len = len;
+ memcpy(esc->seq, seq, len);
+ }
+ return esc;
+}
+
+/** Free an escape sequence. */
+static void free_esc(struct colors *colors, struct esc_seq *seq) {
+ varena_free(&colors->esc_arena, seq, seq->len);
+}
+
/** Initialize a color in the table. */
-static int init_color(struct colors *colors, const char *name, const char *value, char **field) {
+static int init_esc(struct colors *colors, const char *name, const char *value, struct esc_seq **field) {
+ struct esc_seq *esc = NULL;
if (value) {
- *field = dstrdup(value);
- if (!*field) {
+ esc = new_esc(colors, value, strlen(value));
+ if (!esc) {
return -1;
}
- } else {
- *field = NULL;
}
+ *field = esc;
+
struct trie_leaf *leaf = trie_insert_str(&colors->names, name);
- if (leaf) {
- leaf->value = field;
- return 0;
- } else {
+ if (!leaf) {
return -1;
}
+
+ leaf->value = field;
+ return 0;
}
-/** Get a color from the table. */
-static char **get_color(const struct colors *colors, const char *name) {
+/** Check if an escape sequence is equal to a string. */
+static bool esc_eq(const struct esc_seq *esc, const char *str, size_t len) {
+ return esc->len == len && memcmp(esc->seq, str, len) == 0;
+}
+
+/** Get an escape sequence from the table. */
+static struct esc_seq **get_esc(const struct colors *colors, const char *name) {
const struct trie_leaf *leaf = trie_find_str(&colors->names, name);
- if (leaf) {
- return (char **)leaf->value;
- } else {
- return NULL;
- }
+ return leaf ? leaf->value : NULL;
}
-/** Set the value of a color. */
-static int set_color(struct colors *colors, const char *name, char *value) {
- char **color = get_color(colors, name);
- if (color) {
- dstrfree(*color);
- *color = value;
+/** Set a named escape sequence. */
+static int set_esc(struct colors *colors, const char *name, char *value) {
+ struct esc_seq **field = get_esc(colors, name);
+ if (!field) {
return 0;
- } else {
- return -1;
}
+
+ if (*field) {
+ free_esc(colors, *field);
+ *field = NULL;
+ }
+
+ if (value) {
+ *field = new_esc(colors, value, dstrlen(value));
+ if (!*field) {
+ return -1;
+ }
+ }
+
+ return 0;
}
-/**
- * Transform a file extension for fast lookups, by reversing and lowercasing it.
- */
-static void extxfrm(char *ext) {
- size_t len = strlen(ext);
- for (size_t i = 0; i < len - i; ++i) {
- char a = ext[i];
- char b = ext[len - i - 1];
+/** Reverse a string, to turn suffix matches into prefix matches. */
+static void ext_reverse(char *ext, size_t len) {
+ for (size_t i = 0, j = len - 1; len && i < j; ++i, --j) {
+ char c = ext[i];
+ ext[i] = ext[j];
+ ext[j] = c;
+ }
+}
+
+/** Convert a string to lowercase for case-insensitive matching. */
+static void ext_tolower(char *ext, size_t len) {
+ for (size_t i = 0; i < len; ++i) {
+ char c = ext[i];
// What's internationalization? Doesn't matter, this is what
// GNU ls does. Luckily, since there's no standard C way to
// casefold. Not using tolower() here since it respects the
// current locale, which GNU ls doesn't do.
- if (a >= 'A' && a <= 'Z') {
- a += 'a' - 'A';
- }
- if (b >= 'A' && b <= 'Z') {
- b += 'a' - 'A';
+ if (c >= 'A' && c <= 'Z') {
+ c += 'a' - 'A';
}
- ext[i] = b;
- ext[len - i - 1] = a;
+ ext[i] = c;
}
}
/**
- * Set the color for an extension.
+ * The "smart case" algorithm.
+ *
+ * @param ext
+ * The current extension being added.
+ * @param prev
+ * The previous case-sensitive match, if any, for the same extension.
+ * @param iprev
+ * The previous case-insensitive match, if any, for the same extension.
+ * @return
+ * Whether this extension should become case-sensitive.
*/
-static int set_ext_color(struct colors *colors, char *key, const char *value) {
- extxfrm(key);
+static bool ext_case_sensitive(struct ext_color *ext, struct ext_color *prev, struct ext_color *iprev) {
+ // This is the first case-insensitive occurrence of this extension, e.g.
+ //
+ // *.gz=01;31:*.tar.gz=01;33
+ if (!iprev) {
+ bfs_assert(!prev);
+ return false;
+ }
- // 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);
+ // If the last version of this extension is already case-sensitive,
+ // this one should be too, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;32:*.TAR.GZ=01;33
+ if (iprev->case_sensitive) {
+ return true;
}
- struct trie_leaf *leaf = trie_insert_str(&colors->ext_colors, key);
- if (leaf) {
- leaf->value = (char *)value;
- return 0;
- } else {
+ // The case matches the last occurrence exactly, e.g.
+ //
+ // *.tar.gz=01;31:*.tar.gz=01;33
+ if (iprev == prev) {
+ return false;
+ }
+
+ // Different case, but same value, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;31
+ if (esc_eq(iprev->esc, ext->esc->seq, ext->esc->len)) {
+ return false;
+ }
+
+ // Different case, different value, e.g.
+ //
+ // *.tar.gz=01;31:*.TAR.GZ=01;33
+ return true;
+}
+
+/** Set the color for an extension. */
+static int set_ext(struct colors *colors, char *key, char *value) {
+ size_t len = dstrlen(key);
+ 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;
+ }
+
+ key = memcpy(ext->ext, key, len + 1);
+
+ // Reverse the extension (`*.y.x` -> `x.y.*`) so we can use trie_find_prefix()
+ ext_reverse(key, len);
+
+ // Find any pre-existing exact match
+ struct ext_color *prev = NULL;
+ struct trie_leaf *leaf = trie_find_str(&colors->ext_trie, key);
+ if (leaf) {
+ prev = leaf->value;
+ trie_remove(&colors->ext_trie, leaf);
+ }
+
+ // A later *.x should override any earlier *.x, *.y.x, etc.
+ while ((leaf = trie_find_postfix(&colors->ext_trie, key))) {
+ trie_remove(&colors->ext_trie, leaf);
+ }
+
+ // Insert the extension into the case-sensitive trie
+ leaf = trie_insert_str(&colors->ext_trie, key);
+ if (!leaf) {
+ goto fail;
+ }
+ leaf->value = ext;
+
+ // "Smart case": if the same extension is given with two different
+ // capitalizations (e.g. `*.y.x=31:*.Y.Z=32:`), make it case-sensitive
+ ext_tolower(key, len);
+ leaf = trie_insert_str(&colors->iext_trie, key);
+ if (!leaf) {
+ goto fail;
+ }
+
+ struct ext_color *iprev = leaf->value;
+ if (ext_case_sensitive(ext, prev, iprev)) {
+ iprev->case_sensitive = true;
+ ext->case_sensitive = true;
+ }
+ leaf->value = ext;
+
+ return 0;
+
+fail:
+ if (ext->esc) {
+ free_esc(colors, ext->esc);
+ }
+ varena_free(&colors->ext_arena, ext, len + 1);
+ return -1;
+}
+
+/** Rebuild the case-insensitive trie after all extensions have been parsed. */
+static int build_iext_trie(struct colors *colors) {
+ trie_clear(&colors->iext_trie);
+
+ for_trie (leaf, &colors->ext_trie) {
+ size_t len = leaf->length - 1;
+ if (colors->ext_len < len) {
+ colors->ext_len = len;
+ }
+
+ struct ext_color *ext = leaf->value;
+ if (ext->case_sensitive) {
+ continue;
+ }
+
+ // set_ext() already reversed and lowercased the extension
+ struct trie_leaf *ileaf;
+ while ((ileaf = trie_find_postfix(&colors->iext_trie, ext->ext))) {
+ trie_remove(&colors->iext_trie, ileaf);
+ }
+
+ ileaf = trie_insert_str(&colors->iext_trie, ext->ext);
+ if (!ileaf) {
+ return -1;
+ }
+ ileaf->value = ext;
+ }
+
+ return 0;
}
/**
* Find a color by an extension.
*/
-static const char *get_ext_color(const struct colors *colors, const char *filename) {
- char *xfrm = strdup(filename);
- if (!xfrm) {
- return NULL;
+static const struct esc_seq *get_ext(const struct colors *colors, const char *filename) {
+ size_t ext_len = colors->ext_len;
+ size_t name_len = strlen(filename);
+ if (name_len < ext_len) {
+ ext_len = name_len;
}
- extxfrm(xfrm);
+ const char *suffix = filename + name_len - ext_len;
- const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_colors, xfrm);
- free(xfrm);
- if (leaf) {
- return leaf->value;
+ char buf[256];
+ char *copy;
+ if (ext_len < sizeof(buf)) {
+ copy = memcpy(buf, suffix, ext_len + 1);
} else {
- return NULL;
+ copy = strndup(suffix, ext_len);
+ if (!copy) {
+ return NULL;
+ }
+ }
+
+ ext_reverse(copy, ext_len);
+ const struct trie_leaf *leaf = trie_find_prefix(&colors->ext_trie, copy);
+ const struct ext_color *ext = leaf ? leaf->value : NULL;
+
+ ext_tolower(copy, ext_len);
+ const struct trie_leaf *ileaf = trie_find_prefix(&colors->iext_trie, copy);
+ const struct ext_color *iext = ileaf ? ileaf->value : NULL;
+
+ if (iext && (!ext || ext->priority < iext->priority)) {
+ ext = iext;
+ }
+
+ if (copy != buf) {
+ free(copy);
}
+
+ return ext ? ext->esc : NULL;
}
/**
@@ -215,6 +417,8 @@ static const char *get_ext_color(const struct colors *colors, const char *filena
*
* See man dir_colors.
*
+ * @param str
+ * A dstring to fill with the unescaped chunk.
* @param value
* The value to parse.
* @param end
@@ -222,16 +426,18 @@ static const char *get_ext_color(const struct colors *colors, const char *filena
* @param[out] next
* Will be set to the next chunk.
* @return
- * The parsed chunk as a dstring.
+ * 0 on success, -1 on failure.
*/
-static char *unescape(const char *value, char end, const char **next) {
+static int unescape(char **str, const char *value, char end, const char **next) {
+ *next = NULL;
+
if (!value) {
- goto fail;
+ errno = EINVAL;
+ return -1;
}
- char *str = dstralloc(0);
- if (!str) {
- goto fail_str;
+ if (dstresize(str, 0) != 0) {
+ return -1;
}
const char *i;
@@ -308,7 +514,8 @@ static char *unescape(const char *value, char end, const char **next) {
break;
case '\0':
- goto fail_str;
+ errno = EINVAL;
+ return -1;
default:
c = *i;
@@ -322,7 +529,8 @@ static char *unescape(const char *value, char end, const char **next) {
c = '\177';
break;
case '\0':
- goto fail_str;
+ errno = EINVAL;
+ return -1;
default:
// CTRL masks bits 6 and 7
c = *i & 0x1F;
@@ -335,177 +543,179 @@ static char *unescape(const char *value, char end, const char **next) {
break;
}
- if (dstrapp(&str, c) != 0) {
- goto fail_str;
+ if (dstrapp(str, c) != 0) {
+ return -1;
}
}
if (*i) {
*next = i + 1;
- } else {
- *next = NULL;
}
- return str;
-
-fail_str:
- dstrfree(str);
-fail:
- *next = NULL;
- return NULL;
+ return 0;
}
/** Parse the GNU $LS_COLORS format. */
-static void parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) {
+static int parse_gnu_ls_colors(struct colors *colors, const char *ls_colors) {
+ int ret = -1;
+ dchar *key = NULL;
+ dchar *value = NULL;
+
for (const char *chunk = ls_colors, *next; chunk; chunk = next) {
if (chunk[0] == '*') {
- char *key = unescape(chunk + 1, '=', &next);
- if (!key) {
- continue;
+ if (unescape(&key, chunk + 1, '=', &next) != 0) {
+ goto fail;
}
-
- char *value = unescape(next, ':', &next);
- if (value) {
- if (set_ext_color(colors, key, value) != 0) {
- dstrfree(value);
- }
+ if (unescape(&value, next, ':', &next) != 0) {
+ goto fail;
+ }
+ if (set_ext(colors, key, value) != 0) {
+ goto fail;
}
-
- dstrfree(key);
} else {
const char *equals = strchr(chunk, '=');
if (!equals) {
break;
}
- char *value = unescape(equals + 1, ':', &next);
- if (!value) {
- continue;
+ if (dstrncpy(&key, chunk, equals - chunk) != 0) {
+ goto fail;
}
-
- char *key = strndup(chunk, equals - chunk);
- if (!key) {
- dstrfree(value);
- continue;
+ if (unescape(&value, equals + 1, ':', &next) != 0) {
+ goto fail;
}
// All-zero values should be treated like NULL, to fall
// back on any other relevant coloring for that file
+ char *esc = value;
if (strspn(value, "0") == strlen(value)
&& strcmp(key, "rs") != 0
&& strcmp(key, "lc") != 0
&& strcmp(key, "rc") != 0
&& strcmp(key, "ec") != 0) {
- dstrfree(value);
- value = NULL;
+ esc = NULL;
}
- if (set_color(colors, key, value) != 0) {
- dstrfree(value);
+ if (set_esc(colors, key, esc) != 0) {
+ goto fail;
}
- free(key);
}
}
+
+ ret = 0;
+fail:
+ dstrfree(value);
+ dstrfree(key);
+ return ret;
}
-struct colors *parse_colors() {
- struct colors *colors = malloc(sizeof(struct colors));
+struct colors *parse_colors(void) {
+ struct colors *colors = ALLOC(struct colors);
if (!colors) {
return NULL;
}
+ VARENA_INIT(&colors->esc_arena, struct esc_seq, seq);
+ VARENA_INIT(&colors->ext_arena, struct ext_color, ext);
trie_init(&colors->names);
- trie_init(&colors->ext_colors);
+ colors->ext_count = 0;
+ colors->ext_len = 0;
+ trie_init(&colors->ext_trie);
+ trie_init(&colors->iext_trie);
- int ret = 0;
+ bool fail = false;
// From man console_codes
- ret |= init_color(colors, "rs", "0", &colors->reset);
- ret |= init_color(colors, "lc", "\033[", &colors->leftcode);
- ret |= init_color(colors, "rc", "m", &colors->rightcode);
- ret |= init_color(colors, "ec", NULL, &colors->endcode);
- ret |= init_color(colors, "cl", "\033[K", &colors->clear_to_eol);
-
- ret |= init_color(colors, "bld", "01;39", &colors->bold);
- ret |= init_color(colors, "gry", "01;30", &colors->gray);
- ret |= init_color(colors, "red", "01;31", &colors->red);
- ret |= init_color(colors, "grn", "01;32", &colors->green);
- ret |= init_color(colors, "ylw", "01;33", &colors->yellow);
- ret |= init_color(colors, "blu", "01;34", &colors->blue);
- ret |= init_color(colors, "mag", "01;35", &colors->magenta);
- ret |= init_color(colors, "cyn", "01;36", &colors->cyan);
- ret |= init_color(colors, "wht", "01;37", &colors->white);
-
- ret |= init_color(colors, "wrn", "01;33", &colors->warning);
- ret |= init_color(colors, "err", "01;31", &colors->error);
+ fail = fail || init_esc(colors, "rs", "0", &colors->reset);
+ fail = fail || init_esc(colors, "lc", "\033[", &colors->leftcode);
+ fail = fail || init_esc(colors, "rc", "m", &colors->rightcode);
+ fail = fail || init_esc(colors, "ec", NULL, &colors->endcode);
+ fail = fail || init_esc(colors, "cl", "\033[K", &colors->clear_to_eol);
+
+ fail = fail || init_esc(colors, "bld", "01;39", &colors->bold);
+ fail = fail || init_esc(colors, "gry", "01;30", &colors->gray);
+ fail = fail || init_esc(colors, "red", "01;31", &colors->red);
+ fail = fail || init_esc(colors, "grn", "01;32", &colors->green);
+ fail = fail || init_esc(colors, "ylw", "01;33", &colors->yellow);
+ fail = fail || init_esc(colors, "blu", "01;34", &colors->blue);
+ fail = fail || init_esc(colors, "mag", "01;35", &colors->magenta);
+ fail = fail || init_esc(colors, "cyn", "01;36", &colors->cyan);
+ fail = fail || init_esc(colors, "wht", "01;37", &colors->white);
+
+ fail = fail || init_esc(colors, "wrn", "01;33", &colors->warning);
+ fail = fail || init_esc(colors, "err", "01;31", &colors->error);
// Defaults from man dir_colors
+ // "" means fall back to ->normal
- ret |= init_color(colors, "no", NULL, &colors->normal);
+ fail = fail || init_esc(colors, "no", NULL, &colors->normal);
- ret |= init_color(colors, "fi", NULL, &colors->file);
- ret |= init_color(colors, "mh", NULL, &colors->multi_hard);
- ret |= init_color(colors, "ex", "01;32", &colors->executable);
- ret |= init_color(colors, "ca", NULL, &colors->capable);
- ret |= init_color(colors, "sg", "30;43", &colors->setgid);
- ret |= init_color(colors, "su", "37;41", &colors->setuid);
+ fail = fail || init_esc(colors, "fi", "", &colors->file);
+ fail = fail || init_esc(colors, "mh", NULL, &colors->multi_hard);
+ fail = fail || init_esc(colors, "ex", "01;32", &colors->executable);
+ fail = fail || init_esc(colors, "ca", NULL, &colors->capable);
+ fail = fail || init_esc(colors, "sg", "30;43", &colors->setgid);
+ fail = fail || init_esc(colors, "su", "37;41", &colors->setuid);
- ret |= init_color(colors, "di", "01;34", &colors->directory);
- ret |= init_color(colors, "st", "37;44", &colors->sticky);
- ret |= init_color(colors, "ow", "34;42", &colors->other_writable);
- ret |= init_color(colors, "tw", "30;42", &colors->sticky_other_writable);
+ fail = fail || init_esc(colors, "di", "01;34", &colors->directory);
+ fail = fail || init_esc(colors, "st", "37;44", &colors->sticky);
+ fail = fail || init_esc(colors, "ow", "34;42", &colors->other_writable);
+ fail = fail || init_esc(colors, "tw", "30;42", &colors->sticky_other_writable);
- ret |= init_color(colors, "ln", "01;36", &colors->link);
- ret |= init_color(colors, "or", NULL, &colors->orphan);
- ret |= init_color(colors, "mi", NULL, &colors->missing);
+ fail = fail || init_esc(colors, "ln", "01;36", &colors->link);
+ fail = fail || init_esc(colors, "or", NULL, &colors->orphan);
+ fail = fail || init_esc(colors, "mi", NULL, &colors->missing);
colors->link_as_target = false;
- ret |= init_color(colors, "bd", "01;33", &colors->blockdev);
- ret |= init_color(colors, "cd", "01;33", &colors->chardev);
- ret |= init_color(colors, "do", "01;35", &colors->door);
- ret |= init_color(colors, "pi", "33", &colors->pipe);
- ret |= init_color(colors, "so", "01;35", &colors->socket);
+ fail = fail || init_esc(colors, "bd", "01;33", &colors->blockdev);
+ fail = fail || init_esc(colors, "cd", "01;33", &colors->chardev);
+ fail = fail || init_esc(colors, "do", "01;35", &colors->door);
+ fail = fail || init_esc(colors, "pi", "33", &colors->pipe);
+ fail = fail || init_esc(colors, "so", "01;35", &colors->socket);
- if (ret) {
- free_colors(colors);
- return NULL;
+ if (fail) {
+ goto fail;
}
- parse_gnu_ls_colors(colors, getenv("LS_COLORS"));
- parse_gnu_ls_colors(colors, getenv("BFS_COLORS"));
+ if (parse_gnu_ls_colors(colors, getenv("LS_COLORS")) != 0) {
+ goto fail;
+ }
+ if (parse_gnu_ls_colors(colors, getenv("BFS_COLORS")) != 0) {
+ goto fail;
+ }
+ if (build_iext_trie(colors) != 0) {
+ goto fail;
+ }
- if (colors->link && strcmp(colors->link, "target") == 0) {
+ if (colors->link && esc_eq(colors->link, "target", strlen("target"))) {
colors->link_as_target = true;
- dstrfree(colors->link);
- colors->link = NULL;
+ colors->link->len = 0;
}
return colors;
+
+fail:
+ free_colors(colors);
+ return NULL;
}
void free_colors(struct colors *colors) {
- if (colors) {
- struct trie_leaf *leaf;
- while ((leaf = trie_first_leaf(&colors->ext_colors))) {
- dstrfree(leaf->value);
- trie_remove(&colors->ext_colors, leaf);
- }
- trie_destroy(&colors->ext_colors);
+ if (!colors) {
+ return;
+ }
- while ((leaf = trie_first_leaf(&colors->names))) {
- char **field = leaf->value;
- dstrfree(*field);
- trie_remove(&colors->names, leaf);
- }
- trie_destroy(&colors->names);
+ trie_destroy(&colors->iext_trie);
+ trie_destroy(&colors->ext_trie);
+ trie_destroy(&colors->names);
+ varena_destroy(&colors->ext_arena);
+ varena_destroy(&colors->esc_arena);
- free(colors);
- }
+ free(colors);
}
CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) {
- CFILE *cfile = malloc(sizeof(*cfile));
+ CFILE *cfile = ALLOC(CFILE);
if (!cfile) {
return NULL;
}
@@ -517,6 +727,7 @@ CFILE *cfwrap(FILE *file, const struct colors *colors, bool close) {
}
cfile->file = file;
+ cfile->need_reset = false;
cfile->close = close;
if (isatty(fileno(file))) {
@@ -553,15 +764,20 @@ static bool is_link_broken(const struct BFTW *ftwbuf) {
}
}
+bool colors_need_stat(const struct colors *colors) {
+ return colors->setuid || colors->setgid || colors->executable || colors->multi_hard
+ || colors->sticky_other_writable || colors->other_writable || colors->sticky;
+}
+
/** Get the color for a file. */
-static const char *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+static const struct esc_seq *file_color(const struct colors *colors, const char *filename, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
enum bfs_type type = bftw_type(ftwbuf, flags);
if (type == BFS_ERROR) {
goto error;
}
const struct bfs_stat *statbuf = NULL;
- const char *color = NULL;
+ const struct esc_seq *color = NULL;
switch (type) {
case BFS_REG:
@@ -585,7 +801,7 @@ static const char *file_color(const struct colors *colors, const char *filename,
}
if (!color) {
- color = get_ext_color(colors, filename);
+ color = get_ext(colors, filename);
}
if (!color) {
@@ -642,7 +858,7 @@ static const char *file_color(const struct colors *colors, const char *filename,
break;
}
- if (!color) {
+ if (color && color->len == 0) {
color = colors->normal;
}
@@ -656,17 +872,29 @@ error:
}
}
+/** Print an escape sequence chunk. */
+static int print_esc_chunk(CFILE *cfile, const struct esc_seq *esc) {
+ return dstrxcat(&cfile->buffer, esc->seq, esc->len);
+}
+
/** Print an ANSI escape sequence. */
-static int print_esc(CFILE *cfile, const char *esc) {
+static int print_esc(CFILE *cfile, const struct esc_seq *esc) {
+ if (!esc) {
+ return 0;
+ }
+
const struct colors *colors = cfile->colors;
+ if (esc != colors->reset) {
+ cfile->need_reset = true;
+ }
- if (dstrdcat(&cfile->buffer, colors->leftcode) != 0) {
+ if (print_esc_chunk(cfile, cfile->colors->leftcode) != 0) {
return -1;
}
- if (dstrdcat(&cfile->buffer, esc) != 0) {
+ if (print_esc_chunk(cfile, esc) != 0) {
return -1;
}
- if (dstrdcat(&cfile->buffer, colors->rightcode) != 0) {
+ if (print_esc_chunk(cfile, cfile->colors->rightcode) != 0) {
return -1;
}
@@ -675,29 +903,37 @@ static int print_esc(CFILE *cfile, const char *esc) {
/** Reset after an ANSI escape sequence. */
static int print_reset(CFILE *cfile) {
- const struct colors *colors = cfile->colors;
+ if (!cfile->need_reset) {
+ return 0;
+ }
+ cfile->need_reset = false;
+ const struct colors *colors = cfile->colors;
if (colors->endcode) {
- return dstrdcat(&cfile->buffer, colors->endcode);
+ return print_esc_chunk(cfile, colors->endcode);
} else {
return print_esc(cfile, colors->reset);
}
}
+/** Print a shell-escaped string. */
+static int print_wordesc(CFILE *cfile, const char *str, size_t n, enum wesc_flags flags) {
+ return dstrnescat(&cfile->buffer, str, n, flags);
+}
+
/** Print a string with an optional color. */
-static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t len) {
- if (esc) {
- if (print_esc(cfile, esc) != 0) {
- return -1;
- }
+static int print_colored(CFILE *cfile, const struct esc_seq *esc, const char *str, size_t len) {
+ if (print_esc(cfile, esc) != 0) {
+ return -1;
}
- if (dstrncat(&cfile->buffer, str, len) != 0) {
+
+ // Don't let the string itself interfere with the colors
+ if (print_wordesc(cfile, str, len, WESC_TTY) != 0) {
return -1;
}
- if (esc) {
- if (print_reset(cfile) != 0) {
- return -1;
- }
+
+ if (print_reset(cfile) != 0) {
+ return -1;
}
return 0;
@@ -706,13 +942,13 @@ static int print_colored(CFILE *cfile, const char *esc, const char *str, size_t
/** Find the offset of the first broken path component. */
static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t max) {
ssize_t ret = max;
- assert(ret >= 0);
+ bfs_assert(ret >= 0);
if (bftw_type(ftwbuf, flags) != BFS_ERROR) {
goto out;
}
- char *at_path;
+ dchar *at_path;
int at_fd;
if (path == ftwbuf->path) {
if (ftwbuf->depth == 0) {
@@ -750,8 +986,10 @@ static ssize_t first_broken_offset(const char *path, const struct BFTW *ftwbuf,
while (ret && at_path[len - 1] == '/') {
--len, --ret;
}
- while (ret && at_path[len - 1] != '/') {
- --len, --ret;
+ if (errno != ENOTDIR) {
+ while (ret && at_path[len - 1] != '/') {
+ --len, --ret;
+ }
}
dstresize(&at_path, len);
@@ -763,27 +1001,48 @@ out:
return ret;
}
-/** Print the directories leading up to a file. */
-static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, size_t nameoff) {
- const struct colors *colors = cfile->colors;
+/** Print a path with colors. */
+static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
+ size_t nameoff;
+ if (path == ftwbuf->path) {
+ nameoff = ftwbuf->nameoff;
+ } else {
+ nameoff = xbaseoff(path);
+ }
+
+ const char *name = path + nameoff;
+ size_t pathlen = nameoff + strlen(name);
ssize_t broken = first_broken_offset(path, ftwbuf, flags, nameoff);
if (broken < 0) {
return -1;
}
+ size_t split = broken;
- if (broken > 0) {
- if (print_colored(cfile, colors->directory, path, broken) != 0) {
- return -1;
+ const struct colors *colors = cfile->colors;
+ const struct esc_seq *dirs_color = colors->directory;
+ const struct esc_seq *name_color;
+
+ if (split < nameoff) {
+ name_color = colors->missing;
+ if (!name_color) {
+ name_color = colors->orphan;
+ }
+ } else {
+ name_color = file_color(cfile->colors, path + nameoff, ftwbuf, flags);
+ if (name_color == dirs_color) {
+ split = pathlen;
}
}
- if ((size_t)broken < nameoff) {
- const char *color = colors->missing;
- if (!color) {
- color = colors->orphan;
+ if (split > 0) {
+ if (print_colored(cfile, dirs_color, path, split) != 0) {
+ return -1;
}
- if (print_colored(cfile, color, path + broken, nameoff - broken) != 0) {
+ }
+
+ if (split < pathlen) {
+ if (print_colored(cfile, name_color, path + split, pathlen - split) != 0) {
return -1;
}
}
@@ -793,24 +1052,8 @@ static int print_dirs_colored(CFILE *cfile, const char *path, const struct BFTW
/** Print a file name with colors. */
static int print_name_colored(CFILE *cfile, const char *name, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- const char *color = file_color(cfile->colors, name, ftwbuf, flags);
- return print_colored(cfile, color, name, strlen(name));
-}
-
-/** Print a path with colors. */
-static int print_path_colored(CFILE *cfile, const char *path, const struct BFTW *ftwbuf, enum bfs_stat_flags flags) {
- size_t nameoff;
- if (path == ftwbuf->path) {
- nameoff = ftwbuf->nameoff;
- } else {
- nameoff = xbasename(path) - path;
- }
-
- if (print_dirs_colored(cfile, path, ftwbuf, flags, nameoff) != 0) {
- return -1;
- }
-
- return print_name_colored(cfile, path + nameoff, ftwbuf, flags);
+ const struct esc_seq *esc = file_color(cfile->colors, name, ftwbuf, flags);
+ return print_colored(cfile, esc, name, strlen(name));
}
/** Print the name of a file with the appropriate colors. */
@@ -867,33 +1110,35 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) {
}
/** Format some colored output to the buffer. */
-BFS_FORMATTER(2, 3)
+attr(printf(2, 3))
static int cbuff(CFILE *cfile, const char *format, ...);
/** Dump a parsed expression tree, for debugging. */
-static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) {
- if (dstrcat(&cfile->buffer, "(") != 0) {
- return -1;
+static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) {
+ if (depth >= 2) {
+ return dstrcat(&cfile->buffer, "(...)");
}
- const struct bfs_expr *lhs = NULL;
- const struct bfs_expr *rhs = NULL;
+ if (!expr) {
+ return dstrcat(&cfile->buffer, "(null)");
+ }
- if (bfs_expr_has_children(expr)) {
- lhs = expr->lhs;
- rhs = expr->rhs;
+ if (dstrcat(&cfile->buffer, "(") != 0) {
+ return -1;
+ }
- if (cbuff(cfile, "${red}%s${rs}", expr->argv[0]) < 0) {
+ if (bfs_expr_is_parent(expr)) {
+ if (cbuff(cfile, "${red}%pq${rs}", expr->argv[0]) < 0) {
return -1;
}
} else {
- if (cbuff(cfile, "${blu}%s${rs}", expr->argv[0]) < 0) {
+ if (cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]) < 0) {
return -1;
}
}
for (size_t i = 1; i < expr->argc; ++i) {
- if (cbuff(cfile, " ${bld}%s${rs}", expr->argv[i]) < 0) {
+ if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) {
return -1;
}
}
@@ -901,30 +1146,29 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) {
if (verbose) {
double rate = 0.0, time = 0.0;
if (expr->evaluations) {
- rate = 100.0*expr->successes/expr->evaluations;
- time = (1.0e9*expr->elapsed.tv_sec + expr->elapsed.tv_nsec)/expr->evaluations;
+ rate = 100.0 * expr->successes / expr->evaluations;
+ time = (1.0e9 * expr->elapsed.tv_sec + expr->elapsed.tv_nsec) / expr->evaluations;
}
if (cbuff(cfile, " [${ylw}%zu${rs}/${ylw}%zu${rs}=${ylw}%g%%${rs}; ${ylw}%gns${rs}]",
- expr->successes, expr->evaluations, rate, time)) {
+ expr->successes, expr->evaluations, rate, time)) {
return -1;
}
}
- if (lhs) {
+ int count = 0;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
if (dstrcat(&cfile->buffer, " ") != 0) {
return -1;
}
- if (print_expr(cfile, lhs, verbose) != 0) {
- return -1;
- }
- }
-
- if (rhs) {
- if (dstrcat(&cfile->buffer, " ") != 0) {
- return -1;
- }
- if (print_expr(cfile, rhs, verbose) != 0) {
- return -1;
+ if (++count >= 3) {
+ if (dstrcat(&cfile->buffer, "...") != 0) {
+ return -1;
+ }
+ break;
+ } else {
+ if (print_expr(cfile, child, verbose, depth + 1) != 0) {
+ return -1;
+ }
}
}
@@ -935,17 +1179,24 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose) {
return 0;
}
+attr(printf(2, 0))
static int cvbuff(CFILE *cfile, const char *format, va_list args) {
const struct colors *colors = cfile->colors;
int error = errno;
+ // Color specifier (e.g. ${blu}) state
+ struct esc_seq **esc;
+ const char *end;
+ size_t len;
+ char name[4];
+
for (const char *i = format; *i; ++i) {
size_t verbatim = strcspn(i, "%$");
if (dstrncat(&cfile->buffer, i, verbatim) != 0) {
return -1;
}
-
i += verbatim;
+
switch (*i) {
case '%':
switch (*++i) {
@@ -990,13 +1241,24 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
break;
case 'm':
- if (dstrcat(&cfile->buffer, strerror(error)) != 0) {
+ if (dstrcat(&cfile->buffer, xstrerror(error)) != 0) {
return -1;
}
break;
case 'p':
switch (*++i) {
+ case 'q':
+ if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_SHELL | WESC_TTY) != 0) {
+ return -1;
+ }
+ break;
+ case 'Q':
+ if (print_wordesc(cfile, va_arg(args, const char *), SIZE_MAX, WESC_TTY) != 0) {
+ return -1;
+ }
+ break;
+
case 'F':
if (print_name(cfile, va_arg(args, const struct BFTW *)) != 0) {
return -1;
@@ -1016,12 +1278,12 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
break;
case 'e':
- if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false) != 0) {
+ if (print_expr(cfile, va_arg(args, const struct bfs_expr *), false, 0) != 0) {
return -1;
}
break;
case 'E':
- if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true) != 0) {
+ if (print_expr(cfile, va_arg(args, const struct bfs_expr *), true, 0) != 0) {
return -1;
}
break;
@@ -1045,9 +1307,9 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
}
break;
- case '{': {
+ case '{':
++i;
- const char *end = strchr(i, '}');
+ end = strchr(i, '}');
if (!end) {
goto invalid;
}
@@ -1056,16 +1318,22 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
break;
}
- size_t len = end - i;
- char name[len + 1];
+ len = end - i;
+ if (len >= sizeof(name)) {
+ goto invalid;
+ }
memcpy(name, i, len);
name[len] = '\0';
- char **esc = get_color(colors, name);
- if (!esc) {
- goto invalid;
- }
- if (*esc) {
+ if (strcmp(name, "rs") == 0) {
+ if (print_reset(cfile) != 0) {
+ return -1;
+ }
+ } else {
+ esc = get_esc(colors, name);
+ if (!esc) {
+ goto invalid;
+ }
if (print_esc(cfile, *esc) != 0) {
return -1;
}
@@ -1073,7 +1341,6 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
i = end;
break;
- }
default:
goto invalid;
@@ -1088,7 +1355,7 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
return 0;
invalid:
- assert(!"Invalid format string");
+ bfs_bug("Invalid format string '%s'", format);
errno = EINVAL;
return -1;
}
@@ -1102,7 +1369,7 @@ static int cbuff(CFILE *cfile, const char *format, ...) {
}
int cvfprintf(CFILE *cfile, const char *format, va_list args) {
- assert(dstrlen(cfile->buffer) == 0);
+ bfs_assert(dstrlen(cfile->buffer) == 0);
int ret = -1;
if (cvbuff(cfile, format, args) == 0) {
diff --git a/src/color.h b/src/color.h
index edf1ef7..3278cd6 100644
--- a/src/color.h
+++ b/src/color.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Utilities for colored output on ANSI terminals.
@@ -21,9 +8,8 @@
#ifndef BFS_COLOR_H
#define BFS_COLOR_H
-#include "util.h"
-#include <stdarg.h>
-#include <stdbool.h>
+#include "prelude.h"
+#include "dstring.h"
#include <stdio.h>
/**
@@ -32,17 +18,17 @@
struct colors;
/**
- * Parse a color table.
- *
- * @return The parsed color table.
+ * Parse the color table from the environment.
*/
struct colors *parse_colors(void);
/**
+ * Check if stat() info is required to color a file correctly.
+ */
+bool colors_need_stat(const struct colors *colors);
+
+/**
* Free a color table.
- *
- * @param colors
- * The color table to free.
*/
void free_colors(struct colors *colors);
@@ -55,7 +41,9 @@ typedef struct CFILE {
/** The color table to use, if any. */
const struct colors *colors;
/** A buffer for colored formatting. */
- char *buffer;
+ dchar *buffer;
+ /** Whether the next ${rs} is actually necessary. */
+ bool need_reset;
/** Whether to close the underlying stream. */
bool close;
} CFILE;
@@ -98,6 +86,8 @@ int cfclose(CFILE *cfile);
* %s: A string
* %zu: A size_t
* %m: strerror(errno)
+ * %pq: A shell-escaped string, like bash's printf %q
+ * %pQ: A TTY-escaped string.
* %pF: A colored file name, from a const struct BFTW * argument
* %pP: A colored file path, from a const struct BFTW * argument
* %pL: A colored link target, from a const struct BFTW * argument
@@ -109,12 +99,13 @@ int cfclose(CFILE *cfile);
* @return
* 0 on success, -1 on failure.
*/
-BFS_FORMATTER(2, 3)
+attr(printf(2, 3))
int cfprintf(CFILE *cfile, const char *format, ...);
/**
* cfprintf() variant that takes a va_list.
*/
+attr(printf(2, 0))
int cvfprintf(CFILE *cfile, const char *format, va_list args);
#endif // BFS_COLOR_H
diff --git a/src/ctx.c b/src/ctx.c
index 8ba2f38..aa73b35 100644
--- a/src/ctx.c
+++ b/src/ctx.c
@@ -1,139 +1,78 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#include "ctx.h"
+#include "alloc.h"
#include "color.h"
-#include "darray.h"
#include "diag.h"
#include "expr.h"
+#include "list.h"
#include "mtab.h"
#include "pwcache.h"
#include "stat.h"
#include "trie.h"
-#include <assert.h>
+#include "xtime.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
+#include <unistd.h>
-const char *debug_flag_name(enum debug_flags flag) {
- switch (flag) {
- case DEBUG_COST:
- return "cost";
- case DEBUG_EXEC:
- return "exec";
- case DEBUG_OPT:
- return "opt";
- case DEBUG_RATES:
- return "rates";
- case DEBUG_SEARCH:
- return "search";
- case DEBUG_STAT:
- return "stat";
- case DEBUG_TREE:
- return "tree";
-
- case DEBUG_ALL:
- break;
+/** Get the initial value for ctx->threads (-j). */
+static int bfs_nproc(void) {
+ long nproc = sysconf(_SC_NPROCESSORS_ONLN);
+
+ if (nproc < 1) {
+ nproc = 1;
+ } else if (nproc > 8) {
+ // Not much speedup after 8 threads
+ nproc = 8;
}
- assert(!"Unrecognized debug flag");
- return "???";
+ return nproc;
}
struct bfs_ctx *bfs_ctx_new(void) {
- struct bfs_ctx *ctx = malloc(sizeof(*ctx));
+ struct bfs_ctx *ctx = ZALLOC(struct bfs_ctx);
if (!ctx) {
return NULL;
}
- ctx->argv = NULL;
- ctx->paths = NULL;
- ctx->expr = NULL;
- ctx->exclude = NULL;
+ SLIST_INIT(&ctx->expr_list);
+ ARENA_INIT(&ctx->expr_arena, struct bfs_expr);
- ctx->mindepth = 0;
ctx->maxdepth = INT_MAX;
ctx->flags = BFTW_RECOVER;
ctx->strategy = BFTW_BFS;
+ ctx->threads = bfs_nproc();
ctx->optlevel = 3;
- ctx->debug = 0;
- ctx->ignore_races = false;
- ctx->posixly_correct = false;
- ctx->status = false;
- ctx->unique = false;
- ctx->warn = false;
- ctx->xargs_safe = false;
-
- ctx->colors = NULL;
- ctx->colors_error = 0;
- ctx->cout = NULL;
- ctx->cerr = NULL;
-
- ctx->users = NULL;
- ctx->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;
- }
+ if (getrlimit(RLIMIT_NOFILE, &ctx->orig_nofile) != 0) {
+ goto fail;
}
+ ctx->cur_nofile = ctx->orig_nofile;
- return mut->users;
-}
+ ctx->users = bfs_users_new();
+ if (!ctx->users) {
+ goto fail;
+ }
-const struct bfs_groups *bfs_ctx_groups(const struct bfs_ctx *ctx) {
- struct bfs_ctx *mut = (struct bfs_ctx *)ctx;
+ ctx->groups = bfs_groups_new();
+ if (!ctx->groups) {
+ goto fail;
+ }
- if (mut->groups_error) {
- errno = mut->groups_error;
- } else if (!mut->groups) {
- mut->groups = bfs_groups_parse();
- if (!mut->groups) {
- mut->groups_error = errno;
- }
+ if (xgettime(&ctx->now) != 0) {
+ goto fail;
}
- return mut->groups;
+ return ctx;
+
+fail:
+ bfs_ctx_free(ctx);
+ return NULL;
}
const struct bfs_mtab *bfs_ctx_mtab(const struct bfs_ctx *ctx) {
@@ -159,6 +98,8 @@ struct bfs_ctx_file {
CFILE *cfile;
/** The path to the file (for diagnostics). */
const char *path;
+ /** Remembers I/O errors, to propagate them to the exit status. */
+ int error;
};
CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
@@ -181,7 +122,7 @@ CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
return ctx_file->cfile;
}
- leaf->value = ctx_file = malloc(sizeof(*ctx_file));
+ leaf->value = ctx_file = ALLOC(struct bfs_ctx_file);
if (!ctx_file) {
trie_remove(&ctx->files, leaf);
return NULL;
@@ -189,6 +130,7 @@ CFILE *bfs_ctx_dedup(struct bfs_ctx *ctx, CFILE *cfile, const char *path) {
ctx_file->cfile = cfile;
ctx_file->path = path;
+ ctx_file->error = 0;
if (cfile != ctx->cout && cfile != ctx->cerr) {
++ctx->nfiles;
@@ -202,10 +144,28 @@ void bfs_ctx_flush(const struct bfs_ctx *ctx) {
// - the user sees everything relevant before an -ok[dir] prompt
// - output from commands is interleaved consistently with bfs
// - executed commands can rely on I/O from other bfs actions
- //
- // We do not check errors here, but they will be caught at cleanup time
- // with ferror().
- fflush(NULL);
+ for_trie (leaf, &ctx->files) {
+ struct bfs_ctx_file *ctx_file = leaf->value;
+ CFILE *cfile = ctx_file->cfile;
+ if (fflush(cfile->file) == 0) {
+ continue;
+ }
+
+ ctx_file->error = errno;
+ clearerr(cfile->file);
+
+ const char *path = ctx_file->path;
+ if (path) {
+ bfs_error(ctx, "'%s': %m.\n", path);
+ } else if (cfile == ctx->cout) {
+ bfs_error(ctx, "(standard output): %m.\n");
+ }
+ }
+
+ // 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. */
@@ -262,33 +222,33 @@ int bfs_ctx_free(struct bfs_ctx *ctx) {
CFILE *cout = ctx->cout;
CFILE *cerr = ctx->cerr;
- bfs_expr_free(ctx->exclude);
- bfs_expr_free(ctx->expr);
-
bfs_mtab_free(ctx->mtab);
bfs_groups_free(ctx->groups);
bfs_users_free(ctx->users);
- struct trie_leaf *leaf;
- while ((leaf = trie_first_leaf(&ctx->files))) {
+ for_trie (leaf, &ctx->files) {
struct bfs_ctx_file *ctx_file = leaf->value;
+ if (ctx_file->error) {
+ // An error was previously reported during bfs_ctx_flush()
+ ret = -1;
+ }
+
if (bfs_ctx_fclose(ctx, ctx_file) != 0) {
if (cerr) {
- bfs_error(ctx, "'%s': %m.\n", ctx_file->path);
+ bfs_error(ctx, "%pq: %m.\n", ctx_file->path);
}
ret = -1;
}
free(ctx_file);
- trie_remove(&ctx->files, leaf);
}
trie_destroy(&ctx->files);
if (cout && bfs_ctx_fflush(cout) != 0) {
if (cerr) {
- bfs_error(ctx, "standard output: %m.\n");
+ bfs_error(ctx, "(standard output): %m.\n");
}
ret = -1;
}
@@ -298,10 +258,15 @@ int bfs_ctx_free(struct bfs_ctx *ctx) {
free_colors(ctx->colors);
- for (size_t i = 0; i < darray_length(ctx->paths); ++i) {
+ for_slist (struct bfs_expr, expr, &ctx->expr_list, freelist) {
+ bfs_expr_clear(expr);
+ }
+ arena_destroy(&ctx->expr_arena);
+
+ for (size_t i = 0; i < ctx->npaths; ++i) {
free((char *)ctx->paths[i]);
}
- darray_free(ctx->paths);
+ free(ctx->paths);
free(ctx->argv);
free(ctx);
diff --git a/src/ctx.h b/src/ctx.h
index 3ad7f85..fc3020c 100644
--- a/src/ctx.h
+++ b/src/ctx.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* bfs execution context.
@@ -21,37 +8,17 @@
#ifndef BFS_CTX_H
#define BFS_CTX_H
+#include "prelude.h"
+#include "alloc.h"
#include "bftw.h"
+#include "diag.h"
+#include "expr.h"
#include "trie.h"
-#include <stdbool.h>
+#include <stddef.h>
#include <sys/resource.h>
+#include <time.h>
-/**
- * Various debugging flags.
- */
-enum debug_flags {
- /** Print cost estimates. */
- DEBUG_COST = 1 << 0,
- /** Print executed command details. */
- DEBUG_EXEC = 1 << 1,
- /** Print optimization details. */
- DEBUG_OPT = 1 << 2,
- /** Print rate information. */
- DEBUG_RATES = 1 << 3,
- /** Trace the filesystem traversal. */
- DEBUG_SEARCH = 1 << 4,
- /** Trace all stat() calls. */
- DEBUG_STAT = 1 << 5,
- /** Print the parse tree. */
- DEBUG_TREE = 1 << 6,
- /** All debug flags. */
- DEBUG_ALL = (1 << 7) - 1,
-};
-
-/**
- * Convert a debug flag to a string.
- */
-const char *debug_flag_name(enum debug_flags flag);
+struct CFILE;
/**
* The execution context for bfs.
@@ -64,10 +31,17 @@ struct bfs_ctx {
/** The root paths. */
const char **paths;
+ /** The number of root paths. */
+ size_t npaths;
+
/** The main command line expression. */
struct bfs_expr *expr;
/** An expression for files to filter out. */
struct bfs_expr *exclude;
+ /** A list of allocated expressions. */
+ struct bfs_exprs expr_list;
+ /** bfs_expr arena. */
+ struct arena expr_arena;
/** -mindepth option. */
int mindepth;
@@ -79,6 +53,8 @@ struct bfs_ctx {
/** bftw() search strategy. */
enum bftw_strategy strategy;
+ /** Threads (-j). */
+ int threads;
/** Optimization level (-O). */
int optlevel;
/** Debugging flags (-D). */
@@ -105,10 +81,8 @@ struct bfs_ctx {
/** Colored stderr. */
struct CFILE *cerr;
- /** User table. */
+ /** User cache. */
struct bfs_users *users;
- /** The error that occurred parsing the user table, if any. */
- int users_error;
/** Group table. */
struct bfs_groups *groups;
/** The error that occurred parsing the group table, if any. */
@@ -124,10 +98,13 @@ struct bfs_ctx {
/** The number of files owned by the context. */
int nfiles;
- /** The initial RLIMIT_NOFILE soft limit. */
- rlim_t nofile_soft;
- /** The initial RLIMIT_NOFILE hard limit. */
- rlim_t nofile_hard;
+ /** The initial RLIMIT_NOFILE limits. */
+ struct rlimit orig_nofile;
+ /** The current RLIMIT_NOFILE limits. */
+ struct rlimit cur_nofile;
+
+ /** The current time. */
+ struct timespec now;
};
/**
@@ -137,26 +114,6 @@ 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
diff --git a/src/darray.c b/src/darray.c
deleted file mode 100644
index 6585d30..0000000
--- a/src/darray.c
+++ /dev/null
@@ -1,103 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "darray.h"
-#include <stdlib.h>
-#include <string.h>
-
-/**
- * The darray header.
- */
-struct darray {
- /** The current capacity of the array, as a count of elements. */
- size_t capacity;
- /** The current length of the array. */
- size_t length;
-
- // The array elements are stored after this header in memory. Not using
- // a flexible array member to avoid worrying about strict aliasing. We
- // assume that 2*sizeof(size_t) keeps any memory allocation suitably
- // aligned for the element type.
-};
-
-/** Get the header for a darray. */
-static struct darray *darray_header(const void *da) {
- return (struct darray *)da - 1;
-}
-
-/** Get the array from a darray header. */
-static char *darray_data(struct darray *header) {
- return (char *)(header + 1);
-}
-
-size_t darray_length(const void *da) {
- if (da) {
- return darray_header(da)->length;
- } else {
- return 0;
- }
-}
-
-void *darray_push(void *da, const void *item, size_t size) {
- struct darray *header;
- if (da) {
- header = darray_header(da);
- } else {
- header = malloc(sizeof(*header) + size);
- if (!header) {
- return NULL;
- }
- header->capacity = 1;
- header->length = 0;
- }
-
- size_t capacity = header->capacity;
- size_t i = header->length++;
- if (i >= capacity) {
- capacity *= 2;
- header = realloc(header, sizeof(*header) + capacity*size);
- if (!header) {
- // This failure will be detected by darray_check()
- return da;
- }
- header->capacity = capacity;
- }
-
- char *data = darray_data(header);
- memcpy(data + i*size, item, size);
- return data;
-}
-
-int darray_check(void *da) {
- if (!da) {
- return -1;
- }
-
- struct darray *header = darray_header(da);
- if (header->length <= header->capacity) {
- return 0;
- } else {
- // realloc() failed in darray_push(), so reset the length and report the failure
- header->length = header->capacity;
- return -1;
- }
-}
-
-void darray_free(void *da) {
- if (da) {
- free(darray_header(da));
- }
-}
diff --git a/src/darray.h b/src/darray.h
deleted file mode 100644
index 4464381..0000000
--- a/src/darray.h
+++ /dev/null
@@ -1,110 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * A dynamic array library.
- *
- * darrays are represented by a simple pointer to the array element type, like
- * any other array. Behind the scenes, the capacity and current length of the
- * array are stored along with it. NULL is a valid way to initialize an empty
- * darray:
- *
- * int *darray = NULL;
- *
- * To append an element to a darray, use the DARRAY_PUSH macro:
- *
- * int e = 42;
- * if (DARRAY_PUSH(&darray, &e) != 0) {
- * // Report the error...
- * }
- *
- * The length can be retrieved by darray_length(). Iterating over the array
- * works like normal arrays:
- *
- * for (size_t i = 0; i < darray_length(darray); ++i) {
- * printf("%d\n", darray[i]);
- * }
- *
- * To free a darray, use darray_free():
- *
- * darray_free(darray);
- */
-
-#ifndef BFS_DARRAY_H
-#define BFS_DARRAY_H
-
-#include <stddef.h>
-
-/**
- * Get the length of a darray.
- *
- * @param da
- * The array in question.
- * @return
- * The length of the array.
- */
-size_t darray_length(const void *da);
-
-/**
- * @internal Use DARRAY_PUSH().
- *
- * Push an element into a darray.
- *
- * @param da
- * The array to append to.
- * @param item
- * The item to append.
- * @param size
- * The size of the item.
- * @return
- * The (new) location of the array.
- */
-void *darray_push(void *da, const void *item, size_t size);
-
-/**
- * @internal Use DARRAY_PUSH().
- *
- * Check if the last darray_push() call failed.
- *
- * @param da
- * The darray to check.
- * @return
- * 0 on success, -1 on failure.
- */
-int darray_check(void *da);
-
-/**
- * Free a darray.
- *
- * @param da
- * The darray to free.
- */
-void darray_free(void *da);
-
-/**
- * Push an item into a darray.
- *
- * @param da
- * The array to append to.
- * @param item
- * A pointer to the item to append.
- * @return
- * 0 on success, -1 on failure.
- */
-#define DARRAY_PUSH(da, item) \
- (darray_check(*(da) = darray_push(*(da), (item), sizeof(**(da) = *(item)))))
-
-#endif // BFS_DARRAY_H
diff --git a/src/diag.c b/src/diag.c
index 27848f1..deb6f26 100644
--- a/src/diag.c
+++ b/src/diag.c
@@ -1,40 +1,80 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "diag.h"
-#include "ctx.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "color.h"
+#include "ctx.h"
+#include "dstring.h"
#include "expr.h"
-#include "util.h"
-#include <assert.h>
#include <errno.h>
#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/** bfs_diagf() implementation. */
+attr(printf(2, 0))
+static void bfs_vdiagf(const struct bfs_loc *loc, const char *format, va_list args) {
+ fprintf(stderr, "%s: %s@%s:%d: ", xgetprogname(), loc->func, loc->file, loc->line);
+ vfprintf(stderr, format, args);
+ fprintf(stderr, "\n");
+}
+
+void bfs_diagf(const struct bfs_loc *loc, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_vdiagf(loc, format, args);
+ va_end(args);
+}
+
+noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ bfs_vdiagf(loc, format, args);
+ va_end(args);
+
+ abort();
+}
+
+const char *debug_flag_name(enum debug_flags flag) {
+ switch (flag) {
+ case DEBUG_COST:
+ return "cost";
+ case DEBUG_EXEC:
+ return "exec";
+ case DEBUG_OPT:
+ return "opt";
+ case DEBUG_RATES:
+ return "rates";
+ case DEBUG_SEARCH:
+ return "search";
+ case DEBUG_STAT:
+ return "stat";
+ case DEBUG_TREE:
+ return "tree";
+
+ case DEBUG_ALL:
+ break;
+ }
+
+ bfs_bug("Unrecognized debug flag");
+ return "???";
+}
void bfs_perror(const struct bfs_ctx *ctx, const char *str) {
bfs_error(ctx, "%s: %m.\n", str);
}
-void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) {
+void bfs_error(const struct bfs_ctx *ctx, const char *format, ...) {
va_list args;
va_start(args, format);
bfs_verror(ctx, format, args);
va_end(args);
}
-bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) {
+bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) {
va_list args;
va_start(args, format);
bool ret = bfs_vwarning(ctx, format, args);
@@ -42,7 +82,7 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...) {
return ret;
}
-bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) {
+bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...) {
va_list args;
va_start(args, format);
bool ret = bfs_vdebug(ctx, flag, format, args);
@@ -83,13 +123,18 @@ bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *fo
}
}
+/** Get the command name without any leading directories. */
+static const char *bfs_cmd(const struct bfs_ctx *ctx) {
+ return ctx->argv[0] + xbaseoff(ctx->argv[0]);
+}
+
void bfs_error_prefix(const struct bfs_ctx *ctx) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", xbasename(ctx->argv[0]));
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${err}error:${rs} ", bfs_cmd(ctx));
}
bool bfs_warning_prefix(const struct bfs_ctx *ctx) {
if (ctx->warn) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", xbasename(ctx->argv[0]));
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${wrn}warning:${rs} ", bfs_cmd(ctx));
return true;
} else {
return false;
@@ -98,7 +143,7 @@ bool bfs_warning_prefix(const struct bfs_ctx *ctx) {
bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) {
if (ctx->debug & flag) {
- cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", xbasename(ctx->argv[0]), debug_flag_name(flag));
+ cfprintf(ctx->cerr, "${bld}%s:${rs} ${cyn}-D %s${rs}: ", bfs_cmd(ctx), debug_flag_name(flag));
return true;
} else {
return false;
@@ -106,32 +151,33 @@ bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag) {
}
/** Recursive part of highlight_expr(). */
-static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) {
+static bool highlight_expr_recursive(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) {
if (!expr) {
return false;
}
bool ret = false;
- if (!expr->synthetic) {
- size_t i = expr->argv - ctx->argv;
- for (size_t j = 0; j < expr->argc; ++j) {
- assert(i + j < ctx->argc);
- args[i + j] = true;
- ret = true;
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (&ctx->argv[i] == expr->argv) {
+ for (size_t j = 0; j < expr->argc; ++j) {
+ bfs_assert(i + j < ctx->argc);
+ args[i + j] = true;
+ ret = true;
+ }
+ break;
}
}
- if (bfs_expr_has_children(expr)) {
- ret |= highlight_expr_recursive(ctx, expr->lhs, args);
- ret |= highlight_expr_recursive(ctx, expr->rhs, args);
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ ret |= highlight_expr_recursive(ctx, child, args);
}
return ret;
}
/** Highlight an expression in the command line. */
-static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool *args) {
+static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *expr, bool args[]) {
for (size_t i = 0; i < ctx->argc; ++i) {
args[i] = false;
}
@@ -140,13 +186,24 @@ static bool highlight_expr(const struct bfs_ctx *ctx, const struct bfs_expr *exp
}
/** Print a highlighted portion of the command line. */
-static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warning) {
+static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool args[], bool warning) {
if (warning) {
bfs_warning_prefix(ctx);
} else {
bfs_error_prefix(ctx);
}
+ dchar **argv = ZALLOC_ARRAY(dchar *, ctx->argc);
+ if (!argv) {
+ return;
+ }
+
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (dstrescat(&argv[i], ctx->argv[i], WESC_SHELL | WESC_TTY) != 0) {
+ goto done;
+ }
+ }
+
size_t max_argc = 0;
for (size_t i = 0; i < ctx->argc; ++i) {
if (i > 0) {
@@ -155,9 +212,9 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn
if (args[i]) {
max_argc = i + 1;
- cfprintf(ctx->cerr, "${bld}%s${rs}", ctx->argv[i]);
+ cfprintf(ctx->cerr, "${bld}%s${rs}", argv[i]);
} else {
- cfprintf(ctx->cerr, "%s", ctx->argv[i]);
+ cfprintf(ctx->cerr, "%s", argv[i]);
}
}
@@ -186,7 +243,7 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn
}
}
- size_t len = xstrwidth(ctx->argv[i]);
+ size_t len = xstrwidth(argv[i]);
for (size_t j = 0; j < len; ++j) {
if (args[i]) {
cfprintf(ctx->cerr, "~");
@@ -201,9 +258,15 @@ static void bfs_argv_diag(const struct bfs_ctx *ctx, const bool *args, bool warn
}
cfprintf(ctx->cerr, "\n");
+
+done:
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ dstrfree(argv[i]);
+ }
+ free(argv);
}
-void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args) {
+void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]) {
bfs_argv_diag(ctx, args, false);
}
@@ -214,7 +277,7 @@ void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr) {
}
}
-bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args) {
+bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]) {
if (!ctx->warn) {
return false;
}
diff --git a/src/diag.h b/src/diag.h
index 39129cc..2b13609 100644
--- a/src/diag.h
+++ b/src/diag.h
@@ -1,42 +1,141 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Formatters for diagnostic messages.
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Diagnostic messages.
*/
#ifndef BFS_DIAG_H
#define BFS_DIAG_H
-#include "ctx.h"
-#include "util.h"
+#include "prelude.h"
#include <stdarg.h>
-#include <stdbool.h>
+/**
+ * static_assert() with an optional second argument.
+ */
+#if __STDC_VERSION__ >= C23
+# define bfs_static_assert static_assert
+#else
+# define bfs_static_assert(...) bfs_static_assert_(__VA_ARGS__, #__VA_ARGS__, )
+# define bfs_static_assert_(expr, msg, ...) _Static_assert(expr, msg)
+#endif
+
+/**
+ * A source code location.
+ */
+struct bfs_loc {
+ const char *file;
+ int line;
+ const char *func;
+};
+
+#define BFS_LOC_INIT { .file = __FILE__, .line = __LINE__, .func = __func__ }
+
+/**
+ * Get the current source code location.
+ */
+#if __STDC_VERSION__ >= C23
+# define bfs_location() (&(static const struct bfs_loc)BFS_LOC_INIT)
+#else
+# define bfs_location() (&(const struct bfs_loc)BFS_LOC_INIT)
+#endif
+
+/**
+ * Print a low-level diagnostic message to standard error, formatted like
+ *
+ * bfs: func@src/file.c:0: Message
+ */
+attr(printf(2, 3))
+void bfs_diagf(const struct bfs_loc *loc, const char *format, ...);
+
+/**
+ * Unconditional diagnostic message.
+ */
+#define bfs_diag(...) bfs_diagf(bfs_location(), __VA_ARGS__)
+
+/**
+ * Print a message to standard error and abort.
+ */
+attr(cold, printf(2, 3))
+noreturn void bfs_abortf(const struct bfs_loc *loc, const char *format, ...);
+
+/**
+ * Unconditional abort with a message.
+ */
+#define bfs_abort(...) bfs_abortf(bfs_location(), __VA_ARGS__)
+
+/**
+ * Abort in debug builds; no-op in release builds.
+ */
+#ifdef NDEBUG
+# define bfs_bug(...) ((void)0)
+#else
+# define bfs_bug bfs_abort
+#endif
+
+/**
+ * Unconditional assert.
+ */
+#define bfs_verify(...) \
+ bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", "")
+
+#define bfs_verify_(str, cond, format, ...) \
+ ((cond) ? (void)0 : bfs_abort( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s%s" \
+ : "Assertion failed: `%s`%s", \
+ str, __VA_ARGS__))
+
+/**
+ * Assert in debug builds; no-op in release builds.
+ */
+#ifdef NDEBUG
+# define bfs_assert(...) ((void)0)
+#else
+# define bfs_assert bfs_verify
+#endif
+
+struct bfs_ctx;
struct bfs_expr;
/**
+ * Various debugging flags.
+ */
+enum debug_flags {
+ /** Print cost estimates. */
+ DEBUG_COST = 1 << 0,
+ /** Print executed command details. */
+ DEBUG_EXEC = 1 << 1,
+ /** Print optimization details. */
+ DEBUG_OPT = 1 << 2,
+ /** Print rate information. */
+ DEBUG_RATES = 1 << 3,
+ /** Trace the filesystem traversal. */
+ DEBUG_SEARCH = 1 << 4,
+ /** Trace all stat() calls. */
+ DEBUG_STAT = 1 << 5,
+ /** Print the parse tree. */
+ DEBUG_TREE = 1 << 6,
+ /** All debug flags. */
+ DEBUG_ALL = (1 << 7) - 1,
+};
+
+/**
+ * Convert a debug flag to a string.
+ */
+const char *debug_flag_name(enum debug_flags flag);
+
+/**
* Like perror(), but decorated like bfs_error().
*/
+attr(cold)
void bfs_perror(const struct bfs_ctx *ctx, const char *str);
/**
* Shorthand for printing error messages.
*/
-BFS_FORMATTER(2, 3)
+attr(cold, printf(2, 3))
void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
/**
@@ -44,7 +143,7 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
*
* @return Whether a warning was printed.
*/
-BFS_FORMATTER(2, 3)
+attr(cold, printf(2, 3))
bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
/**
@@ -52,57 +151,67 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
*
* @return Whether a debug message was printed.
*/
-BFS_FORMATTER(3, 4)
+attr(cold, printf(3, 4))
bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...);
/**
* bfs_error() variant that takes a va_list.
*/
+attr(cold, printf(2, 0))
void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args);
/**
* bfs_warning() variant that takes a va_list.
*/
+attr(cold, printf(2, 0))
bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args);
/**
* bfs_debug() variant that takes a va_list.
*/
+attr(cold, printf(3, 0))
bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args);
/**
* Print the error message prefix.
*/
+attr(cold)
void bfs_error_prefix(const struct bfs_ctx *ctx);
/**
* Print the warning message prefix.
*/
+attr(cold)
bool bfs_warning_prefix(const struct bfs_ctx *ctx);
/**
* Print the debug message prefix.
*/
+attr(cold)
bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag);
/**
* Highlight parts of the command line in an error message.
*/
-void bfs_argv_error(const struct bfs_ctx *ctx, const bool *args);
+attr(cold)
+void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]);
/**
* Highlight parts of an expression in an error message.
*/
+attr(cold)
void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
/**
* Highlight parts of the command line in a warning message.
*/
-bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool *args);
+attr(cold)
+bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]);
/**
* Highlight parts of an expression in a warning message.
*/
+attr(cold)
bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
#endif // BFS_DIAG_H
diff --git a/src/dir.c b/src/dir.c
index 024e767..cfbbca4 100644
--- a/src/dir.c
+++ b/src/dir.c
@@ -1,33 +1,55 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2021-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "dir.h"
-#include "util.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+#include "trie.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
-#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
-#if __linux__
-# include <sys/syscall.h>
-#endif // __linux__
+#if BFS_USE_GETDENTS
+# if BFS_HAS_GETDENTS64_SYSCALL
+# include <sys/syscall.h>
+# endif
+
+/** getdents() syscall wrapper. */
+static ssize_t bfs_getdents(int fd, void *buf, size_t size) {
+ sanitize_uninit(buf, size);
+
+#if BFS_HAS_GETDENTS
+ ssize_t ret = getdents(fd, buf, size);
+#elif BFS_HAS_GETDENTS64
+ ssize_t ret = getdents64(fd, buf, size);
+#elif BFS_HAS_GETDENTS64_SYSCALL
+ ssize_t ret = syscall(SYS_getdents64, fd, buf, size);
+#else
+# error "No getdents() implementation"
+#endif
+
+ if (ret > 0) {
+ sanitize_init(buf, ret);
+ }
+
+ return ret;
+}
+
+#endif // BFS_USE_GETDENTS
+
+#if BFS_USE_GETDENTS && !BFS_HAS_GETDENTS
+/** Directory entry type for bfs_getdents() */
+typedef struct dirent64 sys_dirent;
+#else
+typedef struct dirent sys_dirent;
+#endif
enum bfs_type bfs_mode_to_type(mode_t mode) {
switch (mode & S_IFMT) {
@@ -77,227 +99,265 @@ enum bfs_type bfs_mode_to_type(mode_t mode) {
}
}
-#if __linux__
/**
- * This is not defined in the kernel headers for some reason, callers have to
- * define it themselves.
+ * Private directory flags.
*/
-struct linux_dirent64 {
- ino64_t d_ino;
- off64_t d_off;
- unsigned short d_reclen;
- unsigned char d_type;
- char d_name[];
+enum {
+ /** We've reached the end of the directory. */
+ BFS_DIR_EOF = BFS_DIR_PRIVATE << 0,
+ /** This directory is a union mount we need to dedup manually. */
+ BFS_DIR_UNION = BFS_DIR_PRIVATE << 1,
};
-// Make the whole allocation 64k
-#define BUF_SIZE ((64 << 10) - 8)
-#endif
-
struct bfs_dir {
-#if __linux__
+ unsigned int flags;
+
+#if BFS_USE_GETDENTS
int fd;
unsigned short pos;
unsigned short size;
+# if __FreeBSD__
+ struct trie trie;
+# endif
+ alignas(sys_dirent) char buf[];
#else
DIR *dir;
struct dirent *de;
#endif
};
-struct bfs_dir *bfs_opendir(int at_fd, const char *at_path) {
-#if __linux__
- struct bfs_dir *dir = malloc(sizeof(*dir) + BUF_SIZE);
+#if BFS_USE_GETDENTS
+# define DIR_SIZE (64 << 10)
+# define BUF_SIZE (DIR_SIZE - sizeof(struct bfs_dir))
#else
- struct bfs_dir *dir = malloc(sizeof(*dir));
+# define DIR_SIZE sizeof(struct bfs_dir)
#endif
- if (!dir) {
- return NULL;
- }
+struct bfs_dir *bfs_allocdir(void) {
+ return malloc(DIR_SIZE);
+}
+
+void bfs_dir_arena(struct arena *arena) {
+ arena_init(arena, alignof(struct bfs_dir), DIR_SIZE);
+}
+
+int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags) {
int fd;
if (at_path) {
fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
+ if (fd < 0) {
+ return -1;
+ }
} else if (at_fd >= 0) {
fd = at_fd;
} else {
- free(dir);
errno = EBADF;
- return NULL;
+ return -1;
}
- if (fd < 0) {
- free(dir);
- return NULL;
- }
+ dir->flags = flags;
-#if __linux__
+#if BFS_USE_GETDENTS
dir->fd = fd;
dir->pos = 0;
dir->size = 0;
-#else
+
+# if __FreeBSD__ && defined(F_ISUNIONSTACK)
+ if (fcntl(fd, F_ISUNIONSTACK) > 0) {
+ dir->flags |= BFS_DIR_UNION;
+ trie_init(&dir->trie);
+ }
+# endif
+#else // !BFS_USE_GETDENTS
dir->dir = fdopendir(fd);
if (!dir->dir) {
if (at_path) {
close_quietly(fd);
}
- free(dir);
- return NULL;
+ return -1;
}
-
dir->de = NULL;
-#endif // __linux__
+#endif
- return dir;
+ return 0;
}
int bfs_dirfd(const struct bfs_dir *dir) {
-#if __linux__
+#if BFS_USE_GETDENTS
return dir->fd;
#else
return dirfd(dir->dir);
#endif
}
-/** Convert a dirent type to a bfs_type. */
-static enum bfs_type translate_type(int d_type) {
- switch (d_type) {
-#ifdef DT_BLK
- case DT_BLK:
- return BFS_BLK;
-#endif
-#ifdef DT_CHR
- case DT_CHR:
- return BFS_CHR;
-#endif
-#ifdef DT_DIR
- case DT_DIR:
- return BFS_DIR;
-#endif
-#ifdef DT_DOOR
- case DT_DOOR:
- return BFS_DOOR;
-#endif
-#ifdef DT_FIFO
- case DT_FIFO:
- return BFS_FIFO;
-#endif
-#ifdef DT_LNK
- case DT_LNK:
- return BFS_LNK;
-#endif
-#ifdef DT_PORT
- case DT_PORT:
- return BFS_PORT;
-#endif
-#ifdef DT_REG
- case DT_REG:
- return BFS_REG;
-#endif
-#ifdef DT_SOCK
- case DT_SOCK:
- return BFS_SOCK;
-#endif
-#ifdef DT_WHT
- case DT_WHT:
- return BFS_WHT;
-#endif
+int bfs_polldir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
+ if (dir->pos < dir->size) {
+ return 1;
+ } else if (dir->flags & BFS_DIR_EOF) {
+ return 0;
}
- return BFS_UNKNOWN;
+ char *buf = (char *)(dir + 1);
+ ssize_t size = bfs_getdents(dir->fd, buf, BUF_SIZE);
+ if (size == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ return 0;
+ } else if (size < 0) {
+ return -1;
+ }
+
+ dir->pos = 0;
+ dir->size = size;
+
+ // Like read(), getdents() doesn't indicate EOF until another call returns zero.
+ // Check that eagerly here to hopefully avoid a syscall in the last bfs_readdir().
+ size_t rest = BUF_SIZE - size;
+ if (rest >= sizeof(sys_dirent)) {
+ size = bfs_getdents(dir->fd, buf + size, rest);
+ if (size > 0) {
+ dir->size += size;
+ } else if (size == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ }
+ }
+
+ return 1;
+#else // !BFS_USE_GETDENTS
+ if (dir->de) {
+ return 1;
+ } else if (dir->flags & BFS_DIR_EOF) {
+ return 0;
+ }
+
+ errno = 0;
+ dir->de = readdir(dir->dir);
+ if (dir->de) {
+ return 1;
+ } else if (errno == 0) {
+ dir->flags |= BFS_DIR_EOF;
+ return 0;
+ } else {
+ return -1;
+ }
+#endif
}
-#if !__linux__
-/** Get the type from a struct dirent if it exists, and convert it. */
-static enum bfs_type dirent_type(const struct dirent *de) {
-#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN)
- return translate_type(de->d_type);
+/** Read a single directory entry. */
+static int bfs_getdent(struct bfs_dir *dir, const sys_dirent **de) {
+ int ret = bfs_polldir(dir);
+ if (ret > 0) {
+#if BFS_USE_GETDENTS
+ char *buf = (char *)(dir + 1);
+ *de = (const sys_dirent *)(buf + dir->pos);
+ dir->pos += (*de)->d_reclen;
#else
- return BFS_UNKNOWN;
+ *de = dir->de;
+ dir->de = NULL;
#endif
+ }
+ return ret;
}
-#endif
-/** Check if a name is . or .. */
-static bool is_dot(const char *name) {
+/** Skip ".", "..", and deleted/empty dirents. */
+static int bfs_skipdent(struct bfs_dir *dir, const sys_dirent *de) {
+#if BFS_USE_GETDENTS
+# if __FreeBSD__
+ // Union mounts on FreeBSD have to be de-duplicated in userspace
+ if (dir->flags & BFS_DIR_UNION) {
+ struct trie_leaf *leaf = trie_insert_str(&dir->trie, de->d_name);
+ if (!leaf) {
+ return -1;
+ } else if (leaf->value) {
+ return 1;
+ } else {
+ leaf->value = leaf;
+ }
+ }
+
+ // NFS mounts on FreeBSD can return empty dirents with inode number 0
+ if (de->d_ino == 0) {
+ return 1;
+ }
+# endif
+
+# ifdef DT_WHT
+ if (de->d_type == DT_WHT && !(dir->flags & BFS_DIR_WHITEOUTS)) {
+ return 1;
+ }
+# endif
+#endif // BFS_USE_GETDENTS
+
+ const char *name = de->d_name;
return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
}
-int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) {
- while (true) {
-#if __linux__
- char *buf = (char *)(dir + 1);
-
- if (dir->pos >= dir->size) {
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // Make sure msan knows the buffer is initialized
- memset(buf, 0, BUF_SIZE);
+/** Convert de->d_type to a bfs_type, if it exists. */
+static enum bfs_type bfs_d_type(const sys_dirent *de) {
+#ifdef DTTOIF
+ return bfs_mode_to_type(DTTOIF(de->d_type));
+#else
+ return BFS_UNKNOWN;
#endif
+}
- ssize_t size = syscall(__NR_getdents64, dir->fd, buf, BUF_SIZE);
- if (size <= 0) {
- return size;
- }
- dir->pos = 0;
- dir->size = size;
+int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) {
+ while (true) {
+ const sys_dirent *sysde;
+ int ret = bfs_getdent(dir, &sysde);
+ if (ret <= 0) {
+ return ret;
}
- const struct linux_dirent64 *lde = (void *)(buf + dir->pos);
- dir->pos += lde->d_reclen;
-
- if (is_dot(lde->d_name)) {
+ int skip = bfs_skipdent(dir, sysde);
+ if (skip < 0) {
+ return skip;
+ } else if (skip) {
continue;
}
if (de) {
- de->type = translate_type(lde->d_type);
- de->name = lde->d_name;
+ de->type = bfs_d_type(sysde);
+ de->name = sysde->d_name;
}
return 1;
-#else // !__linux__
- errno = 0;
- dir->de = readdir(dir->dir);
- if (dir->de) {
- if (is_dot(dir->de->d_name)) {
- continue;
- }
- if (de) {
- de->type = dirent_type(dir->de);
- de->name = dir->de->d_name;
- }
- return 1;
- } else if (errno != 0) {
- return -1;
- } else {
- return 0;
- }
-#endif // !__linux__
}
}
+static void bfs_destroydir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS && __FreeBSD__
+ if (dir->flags & BFS_DIR_UNION) {
+ trie_destroy(&dir->trie);
+ }
+#endif
+
+ sanitize_uninit(dir, DIR_SIZE);
+}
+
int bfs_closedir(struct bfs_dir *dir) {
-#if __linux__
+#if BFS_USE_GETDENTS
int ret = xclose(dir->fd);
#else
int ret = closedir(dir->dir);
+ if (ret != 0) {
+ bfs_verify(errno != EBADF);
+ }
#endif
- free(dir);
+
+ bfs_destroydir(dir);
return ret;
}
-int bfs_freedir(struct bfs_dir *dir) {
-#if __linux__
+#if BFS_USE_UNWRAPDIR
+int bfs_unwrapdir(struct bfs_dir *dir) {
+#if BFS_USE_GETDENTS
int ret = dir->fd;
- free(dir);
- return ret;
-#elif __FreeBSD__
+#elif BFS_HAS_FDCLOSEDIR
int ret = fdclosedir(dir->dir);
- free(dir);
- return ret;
-#else
- int ret = dup_cloexec(dirfd(dir->dir));
- bfs_closedir(dir);
- return ret;
#endif
+
+ bfs_destroydir(dir);
+ return ret;
}
+#endif
diff --git a/src/dir.h b/src/dir.h
index 69344c6..6d5c9c5 100644
--- a/src/dir.h
+++ b/src/dir.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Directories and their contents.
@@ -21,9 +8,18 @@
#ifndef BFS_DIR_H
#define BFS_DIR_H
+#include "prelude.h"
#include <sys/types.h>
/**
+ * Whether the implementation uses the getdents() syscall directly, rather than
+ * libc's readdir().
+ */
+#if !defined(BFS_USE_GETDENTS) && (__linux__ || __FreeBSD__)
+# define BFS_USE_GETDENTS (BFS_HAS_GETDENTS || BFS_HAS_GETDENTS64 | BFS_HAS_GETDENTS64_SYSCALL)
+#endif
+
+/**
* A directory.
*/
struct bfs_dir;
@@ -74,17 +70,49 @@ struct bfs_dirent {
};
/**
+ * Allocate space for a directory.
+ *
+ * @return
+ * An allocated, unopen directory, or NULL on failure.
+ */
+struct bfs_dir *bfs_allocdir(void);
+
+struct arena;
+
+/**
+ * Initialize an arena for directories.
+ *
+ * @param arena
+ * The arena to initialize.
+ */
+void bfs_dir_arena(struct arena *arena);
+
+/**
+ * bfs_opendir() flags.
+ */
+enum bfs_dir_flags {
+ /** Include whiteouts in the results. */
+ BFS_DIR_WHITEOUTS = 1 << 0,
+ /** @internal Start of private flags. */
+ BFS_DIR_PRIVATE = 1 << 1,
+};
+
+/**
* Open a directory.
*
+ * @param dir
+ * The allocated directory.
* @param at_fd
* The base directory for path resolution.
* @param at_path
* The path of the directory to open, relative to at_fd. Pass NULL to
* open at_fd itself.
+ * @param flags
+ * Flags that control which directory entries are listed.
* @return
- * The opened directory, or NULL on failure.
+ * 0 on success, or -1 on failure.
*/
-struct bfs_dir *bfs_opendir(int at_fd, const char *at_path);
+int bfs_opendir(struct bfs_dir *dir, int at_fd, const char *at_path, enum bfs_dir_flags flags);
/**
* Get the file descriptor for a directory.
@@ -92,6 +120,16 @@ struct bfs_dir *bfs_opendir(int at_fd, const char *at_path);
int bfs_dirfd(const struct bfs_dir *dir);
/**
+ * Performs any I/O necessary for the next bfs_readdir() call.
+ *
+ * @param dir
+ * The directory to poll.
+ * @return
+ * 1 on success, 0 on EOF, or -1 on failure.
+ */
+int bfs_polldir(struct bfs_dir *dir);
+
+/**
* Read a directory entry.
*
* @param dir
@@ -112,13 +150,22 @@ int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de);
int bfs_closedir(struct bfs_dir *dir);
/**
- * Free a directory, keeping an open file descriptor to it.
+ * Whether the bfs_unwrapdir() function is supported.
+ */
+#ifndef BFS_USE_UNWRAPDIR
+# define BFS_USE_UNWRAPDIR (BFS_USE_GETDENTS || BFS_HAS_FDCLOSEDIR)
+#endif
+
+#if BFS_USE_UNWRAPDIR
+/**
+ * Detach the file descriptor from an open directory.
*
* @param dir
- * The directory to free.
+ * The directory to detach.
* @return
- * The file descriptor on success, or -1 on failure.
+ * The file descriptor of the directory.
*/
-int bfs_freedir(struct bfs_dir *dir);
+int bfs_unwrapdir(struct bfs_dir *dir);
+#endif
#endif // BFS_DIR_H
diff --git a/src/dstring.c b/src/dstring.c
index f344d09..b5bf3d3 100644
--- a/src/dstring.c
+++ b/src/dstring.c
@@ -1,147 +1,184 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "dstring.h"
-#include <assert.h>
+#include "alloc.h"
+#include "bit.h"
+#include "diag.h"
#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
- * The memory representation of a dynamic string. Users get a pointer to data.
+ * The memory representation of a dynamic string. Users get a pointer to str.
*/
struct dstring {
- size_t capacity;
- size_t length;
- char data[];
+ /** Capacity of the string, *including* the terminating NUL. */
+ size_t cap;
+ /** Length of the string, *excluding* the terminating NUL. */
+ size_t len;
+ /** The string itself. */
+ alignas(dchar) char str[];
};
-/** Get the string header from the string data pointer. */
-static struct dstring *dstrheader(const char *dstr) {
- return (struct dstring *)(dstr - offsetof(struct dstring, data));
+#define DSTR_OFFSET offsetof(struct dstring, str)
+
+/** Back up to the header from a pointer to dstring::str. */
+static struct dstring *dstrheader(const dchar *dstr) {
+ return (struct dstring *)(dstr - DSTR_OFFSET);
}
-/** Get the correct size for a dstring with the given capacity. */
-static size_t dstrsize(size_t capacity) {
- return BFS_FLEX_SIZEOF(struct dstring, data, capacity + 1);
+/**
+ * In some provenance models, the expression `header->str` has its provenance
+ * restricted to just the `str` field itself, making a future dstrheader()
+ * illegal. This alternative is guaranteed to preserve provenance for the entire
+ * allocation.
+ *
+ * - https://stackoverflow.com/q/25296019
+ * - https://mastodon.social/@void_friend@tech.lgbt/111144859908104311
+ */
+static dchar *dstrdata(struct dstring *header) {
+ return (char *)header + DSTR_OFFSET;
}
/** Allocate a dstring with the given contents. */
-static char *dstralloc_impl(size_t capacity, size_t length, const char *data) {
+static dchar *dstralloc_impl(size_t cap, size_t len, const char *str) {
// Avoid reallocations for small strings
- if (capacity < 7) {
- capacity = 7;
+ if (cap < DSTR_OFFSET) {
+ cap = DSTR_OFFSET;
}
- struct dstring *header = malloc(dstrsize(capacity));
+ struct dstring *header = ALLOC_FLEX(struct dstring, str, cap);
if (!header) {
return NULL;
}
- header->capacity = capacity;
- header->length = length;
+ header->cap = cap;
+ header->len = len;
- memcpy(header->data, data, length);
- header->data[length] = '\0';
- return header->data;
+ char *ret = dstrdata(header);
+ memcpy(ret, str, len);
+ ret[len] = '\0';
+ return ret;
}
-char *dstralloc(size_t capacity) {
- return dstralloc_impl(capacity, 0, "");
+dchar *dstralloc(size_t cap) {
+ return dstralloc_impl(cap + 1, 0, "");
}
-char *dstrdup(const char *str) {
- size_t len = strlen(str);
- return dstralloc_impl(len, len, str);
+dchar *dstrdup(const char *str) {
+ return dstrxdup(str, strlen(str));
}
-char *dstrndup(const char *str, size_t n) {
- size_t len = strnlen(str, n);
- return dstralloc_impl(len, len, str);
+dchar *dstrndup(const char *str, size_t n) {
+ return dstrxdup(str, strnlen(str, n));
}
-size_t dstrlen(const char *dstr) {
- return dstrheader(dstr)->length;
+dchar *dstrddup(const dchar *dstr) {
+ return dstrxdup(dstr, dstrlen(dstr));
}
-int dstreserve(char **dstr, size_t capacity) {
- struct dstring *header = dstrheader(*dstr);
+dchar *dstrxdup(const char *str, size_t len) {
+ return dstralloc_impl(len + 1, len, str);
+}
- if (capacity > header->capacity) {
- capacity *= 2;
+size_t dstrlen(const dchar *dstr) {
+ return dstrheader(dstr)->len;
+}
- header = realloc(header, dstrsize(capacity));
- if (!header) {
- return -1;
- }
- header->capacity = capacity;
+int dstreserve(dchar **dstr, size_t cap) {
+ if (!*dstr) {
+ *dstr = dstralloc(cap);
+ return *dstr ? 0 : -1;
+ }
- *dstr = header->data;
+ struct dstring *header = dstrheader(*dstr);
+ size_t old_cap = header->cap;
+ size_t new_cap = cap + 1; // Terminating NUL
+ if (old_cap >= new_cap) {
+ return 0;
}
+ new_cap = bit_ceil(new_cap);
+ header = REALLOC_FLEX(struct dstring, str, header, old_cap, new_cap);
+ if (!header) {
+ return -1;
+ }
+
+ header->cap = new_cap;
+ *dstr = dstrdata(header);
return 0;
}
-int dstresize(char **dstr, size_t length) {
- if (dstreserve(dstr, length) != 0) {
+int dstresize(dchar **dstr, size_t len) {
+ if (dstreserve(dstr, len) != 0) {
return -1;
}
struct dstring *header = dstrheader(*dstr);
- header->length = length;
- header->data[length] = '\0';
-
+ header->len = len;
+ header->str[len] = '\0';
return 0;
}
-/** Common implementation of dstr{cat,ncat,app}. */
-static int dstrcat_impl(char **dest, const char *src, size_t srclen) {
+int dstrcat(dchar **dest, const char *src) {
+ return dstrxcat(dest, src, strlen(src));
+}
+
+int dstrncat(dchar **dest, const char *src, size_t n) {
+ return dstrxcat(dest, src, strnlen(src, n));
+}
+
+int dstrdcat(dchar **dest, const dchar *src) {
+ return dstrxcat(dest, src, dstrlen(src));
+}
+
+int dstrxcat(dchar **dest, const char *src, size_t len) {
size_t oldlen = dstrlen(*dest);
- size_t newlen = oldlen + srclen;
+ size_t newlen = oldlen + len;
if (dstresize(dest, newlen) != 0) {
return -1;
}
- memcpy(*dest + oldlen, src, srclen);
+ memcpy(*dest + oldlen, src, len);
return 0;
}
-int dstrcat(char **dest, const char *src) {
- return dstrcat_impl(dest, src, strlen(src));
+int dstrapp(dchar **str, char c) {
+ return dstrxcat(str, &c, 1);
}
-int dstrncat(char **dest, const char *src, size_t n) {
- return dstrcat_impl(dest, src, strnlen(src, n));
+int dstrcpy(dchar **dest, const char *src) {
+ return dstrxcpy(dest, src, strlen(src));
}
-int dstrdcat(char **dest, const char *src) {
- return dstrcat_impl(dest, src, dstrlen(src));
+int dstrncpy(dchar **dest, const char *src, size_t n) {
+ return dstrxcpy(dest, src, strnlen(src, n));
}
-int dstrapp(char **str, char c) {
- return dstrcat_impl(str, &c, 1);
+int dstrdcpy(dchar **dest, const dchar *src) {
+ return dstrxcpy(dest, src, dstrlen(src));
+}
+
+int dstrxcpy(dchar **dest, const char *src, size_t len) {
+ if (dstresize(dest, len) != 0) {
+ return -1;
+ }
+
+ memcpy(*dest, src, len);
+ return 0;
}
char *dstrprintf(const char *format, ...) {
va_list args;
va_start(args, format);
- char *str = dstrvprintf(format, args);
+ dchar *str = dstrvprintf(format, args);
va_end(args);
return str;
@@ -149,7 +186,7 @@ char *dstrprintf(const char *format, ...) {
char *dstrvprintf(const char *format, va_list args) {
// Guess a capacity to try to avoid reallocating
- char *str = dstralloc(2*strlen(format));
+ dchar *str = dstralloc(2 * strlen(format));
if (!str) {
return NULL;
}
@@ -162,7 +199,7 @@ char *dstrvprintf(const char *format, va_list args) {
return str;
}
-int dstrcatf(char **str, const char *format, ...) {
+int dstrcatf(dchar **str, const char *format, ...) {
va_list args;
va_start(args, format);
@@ -172,48 +209,70 @@ int dstrcatf(char **str, const char *format, ...) {
return ret;
}
-int dstrvcatf(char **str, const char *format, va_list args) {
+int dstrvcatf(dchar **str, const char *format, va_list args) {
// Guess a capacity to try to avoid calling vsnprintf() twice
size_t len = dstrlen(*str);
- dstreserve(str, len + 2*strlen(format));
- size_t cap = dstrheader(*str)->capacity;
+ dstreserve(str, len + 2 * strlen(format));
+ size_t cap = dstrheader(*str)->cap;
va_list copy;
va_copy(copy, args);
char *tail = *str + len;
- int ret = vsnprintf(tail, cap - len + 1, format, args);
+ size_t tail_cap = cap - len;
+ int ret = vsnprintf(tail, tail_cap, format, args);
if (ret < 0) {
goto fail;
}
size_t tail_len = ret;
- if (tail_len > cap - len) {
- cap = len + tail_len;
- if (dstreserve(str, cap) != 0) {
+ if (tail_len >= tail_cap) {
+ if (dstreserve(str, len + tail_len) != 0) {
goto fail;
}
tail = *str + len;
ret = vsnprintf(tail, tail_len + 1, format, copy);
if (ret < 0 || (size_t)ret != tail_len) {
- assert(!"Length of formatted string changed");
+ bfs_bug("Length of formatted string changed");
goto fail;
}
}
va_end(copy);
- struct dstring *header = dstrheader(*str);
- header->length += tail_len;
+ dstrheader(*str)->len += tail_len;
return 0;
fail:
+ va_end(copy);
*tail = '\0';
return -1;
}
-void dstrfree(char *dstr) {
+int dstrescat(dchar **dest, const char *str, enum wesc_flags flags) {
+ return dstrnescat(dest, str, SIZE_MAX, flags);
+}
+
+int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags) {
+ size_t len = *dest ? dstrlen(*dest) : 0;
+
+ // Worst case growth is `ccc...` => $'\xCC\xCC\xCC...'
+ n = strnlen(str, n);
+ size_t cap = len + 4 * n + 3;
+ if (dstreserve(dest, cap) != 0) {
+ return -1;
+ }
+
+ char *cur = *dest + len;
+ char *end = *dest + cap + 1;
+ cur = wordnesc(cur, end, str, n, flags);
+ bfs_assert(cur != end, "wordesc() result truncated");
+
+ return dstresize(dest, cur - *dest);
+}
+
+void dstrfree(dchar *dstr) {
if (dstr) {
free(dstrheader(dstr));
}
diff --git a/src/dstring.h b/src/dstring.h
index 54106f3..9ea7eb9 100644
--- a/src/dstring.h
+++ b/src/dstring.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A dynamic string library.
@@ -21,17 +8,41 @@
#ifndef BFS_DSTRING_H
#define BFS_DSTRING_H
-#include "util.h"
+#include "prelude.h"
+#include "bfstd.h"
#include <stdarg.h>
#include <stddef.h>
+/** Marker type for dynamic strings. */
+#if BFS_LINT && __clang__
+// Abuse __attribute__(aligned) to make a type that allows
+//
+// dchar * -> char *
+//
+// conversions, but warns (with Clang's -Walign-mismatch) on
+//
+// char * -> dchar *
+typedef __attribute__((aligned(alignof(size_t)))) char dchar;
+#else
+typedef char dchar;
+#endif
+
+/**
+ * Free a dynamic string.
+ *
+ * @param dstr
+ * The string to free.
+ */
+void dstrfree(dchar *dstr);
+
/**
* Allocate a dynamic string.
*
- * @param capacity
+ * @param cap
* The initial capacity of the string.
*/
-char *dstralloc(size_t capacity);
+attr(malloc(dstrfree, 1))
+dchar *dstralloc(size_t cap);
/**
* Create a dynamic copy of a string.
@@ -39,7 +50,8 @@ char *dstralloc(size_t capacity);
* @param str
* The NUL-terminated string to copy.
*/
-char *dstrdup(const char *str);
+attr(malloc(dstrfree, 1))
+dchar *dstrdup(const char *str);
/**
* Create a length-limited dynamic copy of a string.
@@ -49,38 +61,62 @@ char *dstrdup(const char *str);
* @param n
* The maximum number of characters to copy from str.
*/
-char *dstrndup(const char *str, size_t n);
+attr(malloc(dstrfree, 1))
+dchar *dstrndup(const char *str, size_t n);
+
+/**
+ * Create a dynamic copy of a dynamic string.
+ *
+ * @param dstr
+ * The dynamic string to copy.
+ */
+attr(malloc(dstrfree, 1))
+dchar *dstrddup(const dchar *dstr);
+
+/**
+ * Create an exact-sized dynamic copy of a string.
+ *
+ * @param str
+ * The string to copy.
+ * @param len
+ * The length of the string, which may include internal NUL bytes.
+ */
+attr(malloc(dstrfree, 1))
+dchar *dstrxdup(const char *str, size_t len);
/**
* Get a dynamic string's length.
*
* @param dstr
* The string to measure.
- * @return The length of dstr.
+ * @return
+ * The length of dstr.
*/
-size_t dstrlen(const char *dstr);
+size_t dstrlen(const dchar *dstr);
/**
* Reserve some capacity in a dynamic string.
*
* @param dstr
* The dynamic string to preallocate.
- * @param capacity
+ * @param cap
* The new capacity for the string.
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
-int dstreserve(char **dstr, size_t capacity);
+int dstreserve(dchar **dstr, size_t cap);
/**
* Resize a dynamic string.
*
* @param dstr
* The dynamic string to resize.
- * @param length
+ * @param len
* The new length for the dynamic string.
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
-int dstresize(char **dstr, size_t length);
+int dstresize(dchar **dstr, size_t len);
/**
* Append to a dynamic string.
@@ -91,7 +127,7 @@ int dstresize(char **dstr, size_t length);
* The string to append.
* @return 0 on success, -1 on failure.
*/
-int dstrcat(char **dest, const char *src);
+int dstrcat(dchar **dest, const char *src);
/**
* Append to a dynamic string.
@@ -102,9 +138,10 @@ int dstrcat(char **dest, const char *src);
* The string to append.
* @param n
* The maximum number of characters to take from src.
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
-int dstrncat(char **dest, const char *src, size_t n);
+int dstrncat(dchar **dest, const char *src, size_t n);
/**
* Append a dynamic string to another dynamic string.
@@ -116,7 +153,21 @@ int dstrncat(char **dest, const char *src, size_t n);
* @return
* 0 on success, -1 on failure.
*/
-int dstrdcat(char **dest, const char *src);
+int dstrdcat(dchar **dest, const dchar *src);
+
+/**
+ * Append to a dynamic string.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param src
+ * The string to append.
+ * @param len
+ * The exact number of characters to take from src.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int dstrxcat(dchar **dest, const char *src, size_t len);
/**
* Append a single character to a dynamic string.
@@ -125,9 +176,62 @@ int dstrdcat(char **dest, const char *src);
* The string to append to.
* @param c
* The character to append.
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int dstrapp(dchar **str, char c);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param src
+ * The string to copy.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+int dstrcpy(dchar **dest, const char *str);
+
+/**
+ * Copy a dynamic string into another one.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param src
+ * The dynamic string to copy.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+int dstrdcpy(dchar **dest, const dchar *str);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param src
+ * The dynamic string to copy.
+ * @param n
+ * The maximum number of characters to take from src.
+ * @returns
+ * 0 on success, -1 on failure.
+ */
+int dstrncpy(dchar **dest, const char *str, size_t n);
+
+/**
+ * Copy a string into a dynamic string.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param src
+ * The dynamic string to copy.
+ * @param len
+ * The exact number of characters to take from src.
+ * @returns
+ * 0 on success, -1 on failure.
*/
-int dstrapp(char **str, char c);
+int dstrxcpy(dchar **dest, const char *str, size_t len);
/**
* Create a dynamic string from a format string.
@@ -139,7 +243,7 @@ int dstrapp(char **str, char c);
* @return
* The created string, or NULL on failure.
*/
-BFS_FORMATTER(1, 2)
+attr(printf(1, 2))
char *dstrprintf(const char *format, ...);
/**
@@ -152,6 +256,7 @@ char *dstrprintf(const char *format, ...);
* @return
* The created string, or NULL on failure.
*/
+attr(printf(1, 0))
char *dstrvprintf(const char *format, va_list args);
/**
@@ -166,8 +271,8 @@ char *dstrvprintf(const char *format, va_list args);
* @return
* 0 on success, -1 on failure.
*/
-BFS_FORMATTER(2, 3)
-int dstrcatf(char **str, const char *format, ...);
+attr(printf(2, 3))
+int dstrcatf(dchar **str, const char *format, ...);
/**
* Format some text from a va_list onto the end of a dynamic string.
@@ -181,14 +286,37 @@ int dstrcatf(char **str, const char *format, ...);
* @return
* 0 on success, -1 on failure.
*/
-int dstrvcatf(char **str, const char *format, va_list args);
+attr(printf(2, 0))
+int dstrvcatf(dchar **str, const char *format, va_list args);
/**
- * Free a dynamic string.
+ * Concatenate while shell-escaping.
*
- * @param dstr
- * The string to free.
+ * @param dest
+ * The destination dynamic string.
+ * @param str
+ * The string to escape.
+ * @param flags
+ * Flags for wordesc().
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int dstrescat(dchar **dest, const char *str, enum wesc_flags flags);
+
+/**
+ * Concatenate while shell-escaping.
+ *
+ * @param dest
+ * The destination dynamic string.
+ * @param str
+ * The string to escape.
+ * @param n
+ * The maximum length of the string.
+ * @param flags
+ * Flags for wordesc().
+ * @return
+ * 0 on success, -1 on failure.
*/
-void dstrfree(char *dstr);
+int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags);
#endif // BFS_DSTRING_H
diff --git a/src/eval.c b/src/eval.c
index d685ab0..49028b7 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -1,29 +1,17 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Implementation of all the primary expressions.
*/
+#include "prelude.h"
#include "eval.h"
#include "bar.h"
+#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
-#include "darray.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
@@ -33,12 +21,10 @@
#include "mtab.h"
#include "printf.h"
#include "pwcache.h"
+#include "sanity.h"
#include "stat.h"
#include "trie.h"
-#include "util.h"
#include "xregex.h"
-#include "xtime.h"
-#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
@@ -49,8 +35,9 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <strings.h>
#include <sys/resource.h>
-#include <sys/stat.h>
+#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
@@ -71,7 +58,7 @@ struct bfs_eval {
/**
* Print an error message.
*/
-BFS_FORMATTER(2, 3)
+attr(printf(2, 3))
static void eval_error(struct bfs_eval *state, const char *format, ...) {
// By POSIX, any errors should be accompanied by a non-zero exit status
*state->ret = EXIT_FAILURE;
@@ -94,7 +81,7 @@ static void eval_error(struct bfs_eval *state, const char *format, ...) {
*/
static bool eval_should_ignore(const struct bfs_eval *state, int error) {
return state->ctx->ignore_races
- && is_nonexistence_error(error)
+ && error_is_like(error, ENOENT)
&& state->ftwbuf->depth > 0;
}
@@ -108,6 +95,20 @@ static void eval_report_error(struct bfs_eval *state) {
}
/**
+ * 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': %m.\n", expr->path);
+ } else {
+ eval_error(state, "(standard output): %m.\n");
+ }
+
+ // Don't report the error again in bfs_ctx_free()
+ clearerr(expr->cfile->file);
+}
+
+/**
* Perform a bfs_stat() call if necessary.
*/
static const struct bfs_stat *eval_stat(struct bfs_eval *state) {
@@ -140,10 +141,24 @@ bool bfs_expr_cmp(const struct bfs_expr *expr, long long n) {
return n > expr->num;
}
- assert(!"Invalid comparison mode");
+ bfs_bug("Invalid comparison mode");
return false;
}
+/** Common code for fnmatch() tests. */
+static bool eval_fnmatch(const struct bfs_expr *expr, const char *str) {
+ if (expr->literal) {
+#ifdef FNM_CASEFOLD
+ if (expr->fnm_flags & FNM_CASEFOLD) {
+ return strcasecmp(expr->pattern, str) == 0;
+ }
+#endif
+ return strcmp(expr->pattern, str) == 0;
+ } else {
+ return fnmatch(expr->pattern, str, expr->fnm_flags) == 0;
+ }
+}
+
/**
* -true test.
*/
@@ -193,6 +208,21 @@ bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state) {
}
/**
+ * -context test.
+ */
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state) {
+ char *con = bfs_getfilecon(state->ftwbuf);
+ if (!con) {
+ eval_report_error(state);
+ return false;
+ }
+
+ bool ret = eval_fnmatch(expr, con);
+ bfs_freecon(con);
+ return ret;
+}
+
+/**
* Get the given timespec field out of a stat buffer.
*/
static const struct timespec *eval_stat_time(const struct bfs_stat *statbuf, enum bfs_stat_field field, struct bfs_eval *state) {
@@ -238,11 +268,11 @@ bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) {
time_t diff = timespec_diff(&expr->reftime, time);
switch (expr->time_unit) {
case BFS_DAYS:
- diff /= 60*24;
- BFS_FALLTHROUGH;
+ diff /= 60 * 24;
+ fallthru;
case BFS_MINUTES:
diff /= 60;
- BFS_FALLTHROUGH;
+ fallthru;
case BFS_SECONDS:
break;
}
@@ -270,7 +300,7 @@ bool eval_used(const struct bfs_expr *expr, struct bfs_eval *state) {
return false;
}
- long long day_seconds = 60*60*24;
+ long long day_seconds = 60 * 60 * 24;
diff = (diff + day_seconds - 1) / day_seconds;
return bfs_expr_cmp(expr, diff);
}
@@ -308,13 +338,11 @@ bool eval_nogroup(const struct bfs_expr *expr, struct bfs_eval *state) {
return false;
}
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
- if (!groups) {
+ const struct group *grp = bfs_getgrgid(state->ctx->groups, statbuf->gid);
+ if (errno != 0) {
eval_report_error(state);
- return false;
}
-
- return bfs_getgrgid(groups, statbuf->gid) == NULL;
+ return grp == NULL;
}
/**
@@ -326,13 +354,11 @@ bool eval_nouser(const struct bfs_expr *expr, struct bfs_eval *state) {
return false;
}
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- if (!users) {
+ const struct passwd *pwd = bfs_getpwuid(state->ctx->users, statbuf->uid);
+ if (errno != 0) {
eval_report_error(state);
- return false;
}
-
- return bfs_getpwuid(users, statbuf->uid) == NULL;
+ return pwd == NULL;
}
/**
@@ -376,11 +402,10 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c
}
ret = -1;
}
- } else if (bfs_expr_has_children(expr)) {
- if (expr->lhs && eval_exec_finish(expr->lhs, ctx) != 0) {
- ret = -1;
- }
- if (expr->rhs && eval_exec_finish(expr->rhs, ctx) != 0) {
+ }
+
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (eval_exec_finish(child, ctx) != 0) {
ret = -1;
}
}
@@ -420,33 +445,42 @@ bool eval_depth(const struct bfs_expr *expr, struct bfs_eval *state) {
* -empty test.
*/
bool eval_empty(const struct bfs_expr *expr, struct bfs_eval *state) {
- bool ret = false;
const struct BFTW *ftwbuf = state->ftwbuf;
+ const struct bfs_stat *statbuf;
+ struct bfs_dir *dir;
- if (ftwbuf->type == BFS_DIR) {
- struct bfs_dir *dir = bfs_opendir(ftwbuf->at_fd, ftwbuf->at_path);
+ switch (ftwbuf->type) {
+ case BFS_REG:
+ statbuf = eval_stat(state);
+ return statbuf && statbuf->size == 0;
+
+ case BFS_DIR:
+ dir = bfs_allocdir();
if (!dir) {
- eval_report_error(state);
- goto done;
+ goto error;
}
- int did_read = bfs_readdir(dir, NULL);
- if (did_read < 0) {
- eval_report_error(state);
- } else {
- ret = !did_read;
+ if (bfs_opendir(dir, ftwbuf->at_fd, ftwbuf->at_path, 0) != 0) {
+ goto error;
}
+ int did_read = bfs_readdir(dir, NULL);
bfs_closedir(dir);
- } else if (ftwbuf->type == BFS_REG) {
- const struct bfs_stat *statbuf = eval_stat(state);
- if (statbuf) {
- ret = statbuf->size == 0;
+
+ if (did_read < 0) {
+ goto error;
}
- }
-done:
- return ret;
+ free(dir);
+ return did_read == 0;
+ error:
+ eval_report_error(state);
+ free(dir);
+ return false;
+
+ default:
+ return false;
+ }
}
/**
@@ -478,7 +512,7 @@ bool eval_flags(const struct bfs_expr *expr, struct bfs_eval *state) {
return (flags & set) || (flags & clear) != clear;
}
- assert(!"Invalid comparison mode");
+ bfs_bug("Invalid comparison mode");
return false;
}
@@ -498,6 +532,11 @@ bool eval_fstype(const struct bfs_expr *expr, struct bfs_eval *state) {
}
const char *type = bfs_fstype(mtab, statbuf);
+ if (!type) {
+ eval_report_error(state);
+ return false;
+ }
+
return strcmp(type, expr->argv[1]) == 0;
}
@@ -561,7 +600,7 @@ bool eval_lname(const struct bfs_expr *expr, struct bfs_eval *state) {
goto done;
}
- ret = fnmatch(expr->argv[1], name, expr->num) == 0;
+ ret = eval_fnmatch(expr, name);
done:
free(name);
@@ -572,6 +611,7 @@ done:
* -i?name test.
*/
bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) {
+ bool ret = false;
const struct BFTW *ftwbuf = state->ftwbuf;
const char *name = ftwbuf->path + ftwbuf->nameoff;
@@ -579,18 +619,16 @@ bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) {
if (ftwbuf->depth == 0) {
// Any trailing slashes are not part of the name. This can only
// happen for the root path.
- const char *slash = strchr(name, '/');
- if (slash && slash > name) {
- copy = strndup(name, slash - name);
- if (!copy) {
- eval_report_error(state);
- return false;
- }
- name = copy;
+ name = copy = xbasename(name);
+ if (!name) {
+ eval_report_error(state);
+ goto done;
}
}
- bool ret = fnmatch(expr->argv[1], name, expr->num) == 0;
+ ret = eval_fnmatch(expr, name);
+
+done:
free(copy);
return ret;
}
@@ -599,8 +637,7 @@ bool eval_name(const struct bfs_expr *expr, struct bfs_eval *state) {
* -i?path test.
*/
bool eval_path(const struct bfs_expr *expr, struct bfs_eval *state) {
- const struct BFTW *ftwbuf = state->ftwbuf;
- return fnmatch(expr->argv[1], ftwbuf->path, expr->num) == 0;
+ return eval_fnmatch(expr, state->ftwbuf->path);
}
/**
@@ -631,27 +668,49 @@ bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state) {
return !(mode & target) == !target;
}
- assert(!"Invalid comparison mode");
+ bfs_bug("Invalid comparison mode");
return false;
}
+/** Print a user/group name/id, and update the column width. */
+static int print_owner(FILE *file, const char *name, uintmax_t id, int *width) {
+ if (name) {
+ int len = xstrwidth(name);
+ if (*width < len) {
+ *width = len;
+ }
+
+ return fprintf(file, " %s%*s", name, *width - len, "");
+ } else {
+ int ret = fprintf(file, " %-*ju", *width, id);
+ if (ret >= 0 && *width < ret - 1) {
+ *width = ret - 1;
+ }
+ return ret;
+ }
+}
+
/**
* -f?ls action.
*/
bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) {
CFILE *cfile = expr->cfile;
FILE *file = cfile->file;
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
+ const struct bfs_ctx *ctx = state->ctx;
const struct BFTW *ftwbuf = state->ftwbuf;
const struct bfs_stat *statbuf = eval_stat(state);
if (!statbuf) {
goto done;
}
+ // ls -l prints non-path text in the "normal" color, so do the same
+ if (cfprintf(cfile, "${no}") < 0) {
+ goto error;
+ }
+
uintmax_t ino = statbuf->ino;
- uintmax_t block_size = state->ctx->posixly_correct ? 512 : 1024;
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + block_size - 1)/block_size;
+ uintmax_t block_size = ctx->posixly_correct ? 512 : 1024;
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + block_size - 1) / block_size;
char mode[11];
xstrmode(statbuf->mode, mode);
char acl = bfs_check_acl(ftwbuf) > 0 ? '+' : ' ';
@@ -660,33 +719,21 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) {
goto error;
}
- uintmax_t uid = statbuf->uid;
- const struct passwd *pwd = users ? bfs_getpwuid(users, uid) : NULL;
- if (pwd) {
- if (fprintf(file, " %-8s", pwd->pw_name) < 0) {
- goto error;
- }
- } else {
- if (fprintf(file, " %-8ju", uid) < 0) {
- goto error;
- }
+ const struct passwd *pwd = bfs_getpwuid(ctx->users, statbuf->uid);
+ static int uwidth = 8;
+ if (print_owner(file, pwd ? pwd->pw_name : NULL, statbuf->uid, &uwidth) < 0) {
+ goto error;
}
- uintmax_t gid = statbuf->gid;
- const struct group *grp = groups ? bfs_getgrgid(groups, gid) : NULL;
- if (grp) {
- if (fprintf(file, " %-8s", grp->gr_name) < 0) {
- goto error;
- }
- } else {
- if (fprintf(file, " %-8ju", gid) < 0) {
- goto error;
- }
+ const struct group *grp = bfs_getgrgid(ctx->groups, statbuf->gid);
+ static int gwidth = 8;
+ if (print_owner(file, grp ? grp->gr_name : NULL, statbuf->gid, &gwidth) < 0) {
+ goto error;
}
if (ftwbuf->type == BFS_BLK || ftwbuf->type == BFS_CHR) {
- int ma = bfs_major(statbuf->rdev);
- int mi = bfs_minor(statbuf->rdev);
+ int ma = xmajor(statbuf->rdev);
+ int mi = xminor(statbuf->rdev);
if (fprintf(file, " %3d, %3d", ma, mi) < 0) {
goto error;
}
@@ -698,23 +745,25 @@ bool eval_fls(const struct bfs_expr *expr, struct bfs_eval *state) {
}
time_t time = statbuf->mtime.tv_sec;
- time_t now = expr->reftime.tv_sec;
- time_t six_months_ago = now - 6*30*24*60*60;
- time_t tomorrow = now + 24*60*60;
+ time_t now = ctx->now.tv_sec;
+ time_t six_months_ago = now - 6 * 30 * 24 * 60 * 60;
+ time_t tomorrow = now + 24 * 60 * 60;
struct tm tm;
- if (xlocaltime(&time, &tm) != 0) {
+ if (!localtime_r(&time, &tm)) {
goto error;
}
char time_str[256];
- const char *time_format = "%b %e %H:%M";
+ size_t time_ret;
if (time <= six_months_ago || time >= tomorrow) {
- time_format = "%b %e %Y";
+ time_ret = strftime(time_str, sizeof(time_str), "%b %e %Y", &tm);
+ } else {
+ time_ret = strftime(time_str, sizeof(time_str), "%b %e %H:%M", &tm);
}
- if (!strftime(time_str, sizeof(time_str), time_format, &tm)) {
+ if (time_ret == 0) {
errno = EOVERFLOW;
goto error;
}
- if (fprintf(file, " %s", time_str) < 0) {
+ if (cfprintf(cfile, " %s${rs}", time_str) < 0) {
goto error;
}
@@ -736,7 +785,7 @@ done:
return true;
error:
- eval_report_error(state);
+ eval_io_error(expr, state);
return true;
}
@@ -745,7 +794,7 @@ error:
*/
bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state) {
if (cfprintf(expr->cfile, "%pP\n", state->ftwbuf) < 0) {
- eval_report_error(state);
+ eval_io_error(expr, state);
}
return true;
}
@@ -757,7 +806,7 @@ bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) {
const char *path = state->ftwbuf->path;
size_t length = strlen(path) + 1;
if (fwrite(path, 1, length, expr->cfile->file) != length) {
- eval_report_error(state);
+ eval_io_error(expr, state);
}
return true;
}
@@ -767,7 +816,7 @@ bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state) {
*/
bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state) {
if (bfs_printf(expr->cfile, expr->printf, state->ftwbuf) != 0) {
- eval_report_error(state);
+ eval_io_error(expr, state);
}
return true;
@@ -799,7 +848,6 @@ bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) {
++path;
}
-
if (fputc('\n', file) == EOF) {
goto error;
}
@@ -807,7 +855,20 @@ bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state) {
return true;
error:
- eval_report_error(state);
+ eval_io_error(expr, state);
+ return true;
+}
+
+/**
+ * -limit action.
+ */
+bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state) {
+ long long evals = expr->evaluations + 1;
+ if (evals >= expr->num) {
+ state->action = BFTW_STOP;
+ state->quit = true;
+ }
+
return true;
}
@@ -881,7 +942,7 @@ bool eval_size(const struct bfs_expr *expr, struct bfs_eval *state) {
};
off_t scale = scales[expr->size_unit];
- off_t size = (statbuf->size + scale - 1)/scale; // Round up
+ off_t size = (statbuf->size + scale - 1) / scale; // Round up
return bfs_expr_cmp(expr, size);
}
@@ -894,7 +955,7 @@ bool eval_sparse(const struct bfs_expr *expr, struct bfs_eval *state) {
return false;
}
- blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1)/BFS_STAT_BLKSIZE;
+ blkcnt_t expected = (statbuf->size + BFS_STAT_BLKSIZE - 1) / BFS_STAT_BLKSIZE;
return statbuf->blocks < expected;
}
@@ -947,9 +1008,9 @@ bool eval_xtype(const struct bfs_expr *expr, struct bfs_eval *state) {
}
#if _POSIX_MONOTONIC_CLOCK > 0
-# define BFS_CLOCK CLOCK_MONOTONIC
+# define BFS_CLOCK CLOCK_MONOTONIC
#elif _POSIX_TIMERS > 0
-# define BFS_CLOCK CLOCK_REALTIME
+# define BFS_CLOCK CLOCK_REALTIME
#endif
/**
@@ -994,7 +1055,7 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) {
}
}
- assert(!state->quit);
+ bfs_assert(!state->quit);
bool ret = expr->eval_fn(expr, state);
@@ -1010,10 +1071,10 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) {
}
if (bfs_expr_never_returns(expr)) {
- assert(state->quit);
+ bfs_assert(state->quit);
} else if (!state->quit) {
- assert(!expr->always_true || ret);
- assert(!expr->always_false || !ret);
+ bfs_assert(!expr->always_true || ret);
+ bfs_assert(!expr->always_false || !ret);
}
return ret;
@@ -1023,50 +1084,49 @@ static bool eval_expr(struct bfs_expr *expr, struct bfs_eval *state) {
* Evaluate a negation.
*/
bool eval_not(const struct bfs_expr *expr, struct bfs_eval *state) {
- return !eval_expr(expr->rhs, state);
+ return !eval_expr(bfs_expr_children(expr), state);
}
/**
* Evaluate a conjunction.
*/
bool eval_and(const struct bfs_expr *expr, struct bfs_eval *state) {
- if (!eval_expr(expr->lhs, state)) {
- return false;
- }
-
- if (state->quit) {
- return false;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (!eval_expr(child, state) || state->quit) {
+ return false;
+ }
}
- return eval_expr(expr->rhs, state);
+ return true;
}
/**
* Evaluate a disjunction.
*/
bool eval_or(const struct bfs_expr *expr, struct bfs_eval *state) {
- if (eval_expr(expr->lhs, state)) {
- return true;
- }
-
- if (state->quit) {
- return false;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (eval_expr(child, state) || state->quit) {
+ return true;
+ }
}
- return eval_expr(expr->rhs, state);
+ return false;
}
/**
* Evaluate the comma operator.
*/
bool eval_comma(const struct bfs_expr *expr, struct bfs_eval *state) {
- eval_expr(expr->lhs, state);
+ bool ret uninit(false);
- if (state->quit) {
- return false;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ ret = eval_expr(child, state);
+ if (state->quit) {
+ break;
+ }
}
- return eval_expr(expr->rhs, state);
+ return ret;
}
/** Update the status bar. */
@@ -1091,20 +1151,21 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time
const struct BFTW *ftwbuf = state->ftwbuf;
- char *rhs = dstrprintf(" (visited: %zu, depth: %2zu)", count, ftwbuf->depth);
+ dchar *status = NULL;
+ dchar *rhs = dstrprintf(" (visited: %'zu; depth: %2zu)", count, ftwbuf->depth);
if (!rhs) {
return;
}
- size_t rhslen = dstrlen(rhs);
+ size_t rhslen = xstrwidth(rhs);
if (3 + rhslen > width) {
dstresize(&rhs, 0);
rhslen = 0;
}
- char *status = dstralloc(0);
+ status = dstralloc(0);
if (!status) {
- goto out_rhs;
+ goto out;
}
const char *path = ftwbuf->path;
@@ -1113,26 +1174,25 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time
pathlen = strlen(path);
}
+ // Escape weird filename characters
+ if (dstrnescat(&status, path, pathlen, WESC_TTY) != 0) {
+ goto out;
+ }
+ pathlen = dstrlen(status);
+
// Try to make sure even wide characters fit in the status bar
size_t pathmax = width - rhslen - 3;
size_t pathwidth = 0;
- mbstate_t mb;
- memset(&mb, 0, sizeof(mb));
- while (pathlen > 0) {
- wchar_t wc;
- size_t len = mbrtowc(&wc, path, pathlen, &mb);
+ size_t lhslen = 0;
+ mbstate_t mb = {0};
+ for (size_t i = lhslen; lhslen < pathlen; lhslen = i) {
+ wint_t wc = xmbrtowc(status, &i, pathlen, &mb);
int cwidth;
- if (len == (size_t)-1) {
+ if (wc == WEOF) {
// Invalid byte sequence, assume a single-width '?'
- len = 1;
- cwidth = 1;
- memset(&mb, 0, sizeof(mb));
- } else if (len == (size_t)-2) {
- // Incomplete byte sequence, assume a single-width '?'
- len = pathlen;
cwidth = 1;
} else {
- cwidth = wcwidth(wc);
+ cwidth = xwcwidth(wc);
if (cwidth < 0) {
cwidth = 0;
}
@@ -1141,35 +1201,29 @@ static void eval_status(struct bfs_eval *state, struct bfs_bar *bar, struct time
if (pathwidth + cwidth > pathmax) {
break;
}
-
- if (dstrncat(&status, path, len) != 0) {
- goto out_rhs;
- }
-
- path += len;
- pathlen -= len;
pathwidth += cwidth;
}
+ dstresize(&status, lhslen);
if (dstrcat(&status, "...") != 0) {
- goto out_rhs;
+ goto out;
}
while (pathwidth < pathmax) {
if (dstrapp(&status, ' ') != 0) {
- goto out_rhs;
+ goto out;
}
++pathwidth;
}
- if (dstrcat(&status, rhs) != 0) {
- goto out_rhs;
+ if (dstrdcat(&status, rhs) != 0) {
+ goto out;
}
bfs_bar_update(bar, status);
+out:
dstrfree(status);
-out_rhs:
dstrfree(rhs);
}
@@ -1198,21 +1252,21 @@ static bool eval_file_unique(struct bfs_eval *state, struct trie *seen) {
}
}
-#define DEBUG_FLAG(flags, flag) \
- do { \
- if ((flags & flag) || flags == flag) { \
- fputs(#flag, stderr); \
- flags ^= flag; \
- if (flags) { \
- fputs(" | ", stderr); \
- } \
- } \
+#define DEBUG_FLAG(flags, flag) \
+ do { \
+ if ((flags & flag) || flags == flag) { \
+ fputs(#flag, stderr); \
+ flags ^= flag; \
+ if (flags) { \
+ fputs(" | ", stderr); \
+ } \
+ } \
} while (0)
/**
* Log a stat() call.
*/
-static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, const struct bftw_stat *cache, enum bfs_stat_flags flags) {
+static void debug_stat(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf, enum bfs_stat_flags flags, int err) {
bfs_debug_prefix(ctx, DEBUG_STAT);
fprintf(stderr, "bfs_stat(");
@@ -1230,11 +1284,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 : -1);
- if (cache->error) {
- fprintf(stderr, " [%d]", cache->error);
+ if (err) {
+ fprintf(stderr, " [%d]", err);
}
fprintf(stderr, "\n");
@@ -1248,14 +1303,14 @@ static void debug_stats(const struct bfs_ctx *ctx, const struct BFTW *ftwbuf) {
return;
}
- const struct bfs_stat *statbuf = ftwbuf->stat_cache.buf;
- if (statbuf || ftwbuf->stat_cache.error) {
- debug_stat(ctx, ftwbuf, &ftwbuf->stat_cache, BFS_STAT_FOLLOW);
+ const struct bftw_stat *bufs = &ftwbuf->stat_bufs;
+
+ if (bufs->stat_err >= 0) {
+ debug_stat(ctx, ftwbuf, BFS_STAT_FOLLOW, bufs->stat_err);
}
- const struct bfs_stat *lstatbuf = ftwbuf->lstat_cache.buf;
- if ((lstatbuf && lstatbuf != statbuf) || ftwbuf->lstat_cache.error) {
- debug_stat(ctx, ftwbuf, &ftwbuf->lstat_cache, BFS_STAT_NOFOLLOW);
+ if (bufs->lstat_err >= 0) {
+ debug_stat(ctx, ftwbuf, BFS_STAT_NOFOLLOW, bufs->lstat_err);
}
}
@@ -1352,7 +1407,7 @@ static enum bftw_action eval_callback(const struct BFTW *ftwbuf, void *ptr) {
if (ftwbuf->type == BFS_ERROR) {
if (!eval_should_ignore(&state, ftwbuf->error)) {
- eval_error(&state, "%s.\n", strerror(ftwbuf->error));
+ eval_error(&state, "%s.\n", xstrerror(ftwbuf->error));
}
state.action = BFTW_PRUNE;
goto done;
@@ -1409,59 +1464,48 @@ done:
return state.action;
}
-/** Check if an rlimit value is infinite. */
-static bool rlim_isinf(rlim_t r) {
- // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite
- if (r == RLIM_INFINITY) {
- return true;
- }
+/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
+static int raise_fdlimit(struct bfs_ctx *ctx) {
+ rlim_t cur = ctx->orig_nofile.rlim_cur;
+ rlim_t max = ctx->orig_nofile.rlim_max;
-#ifdef RLIM_SAVED_CUR
- if (r == RLIM_SAVED_CUR) {
- return true;
+ rlim_t target = 64 << 10;
+ if (rlim_cmp(target, max) > 0) {
+ target = max;
}
-#endif
-#ifdef RLIM_SAVED_MAX
- if (r == RLIM_SAVED_MAX) {
- return true;
+ if (rlim_cmp(target, cur) <= 0) {
+ return target;
}
-#endif
- return false;
-}
+ const struct rlimit rl = {
+ .rlim_cur = target,
+ .rlim_max = max,
+ };
-/** Compare two rlimit values, accounting for RLIM_INFINITY etc. */
-static int rlim_cmp(rlim_t a, rlim_t b) {
- bool a_inf = rlim_isinf(a);
- bool b_inf = rlim_isinf(b);
- if (a_inf || b_inf) {
- return a_inf - b_inf;
+ if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
+ return cur;
}
- return (a > b) - (a < b);
+ ctx->cur_nofile = rl;
+ return target;
}
-/** Raise RLIMIT_NOFILE if possible, and return the new limit. */
-static int raise_fdlimit(const struct bfs_ctx *ctx) {
- rlim_t target = 64 << 10;
- if (rlim_cmp(target, ctx->nofile_hard) > 0) {
- target = ctx->nofile_hard;
- }
-
- int ret = target;
+/** Preallocate the fd table in the kernel. */
+static void reserve_fds(int limit) {
+ // Kernels typically implement the fd table as a dynamic array.
+ // Growing the array can be expensive, especially if files are being
+ // opened in parallel. We can work around this by allocating the
+ // highest possible fd, forcing the kernel to grow the table upfront.
- if (rlim_cmp(target, ctx->nofile_soft) > 0) {
- const struct rlimit rl = {
- .rlim_cur = target,
- .rlim_max = ctx->nofile_hard,
- };
- if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
- ret = ctx->nofile_soft;
- }
+#ifdef F_DUPFD_CLOEXEC
+ int fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, limit - 1);
+#else
+ int fd = fcntl(STDIN_FILENO, F_DUPFD, limit - 1);
+#endif
+ if (fd >= 0) {
+ xclose(fd);
}
-
- return ret;
}
/** Infer the number of file descriptors available to bftw(). */
@@ -1471,20 +1515,25 @@ static int infer_fdlimit(const struct bfs_ctx *ctx, int limit) {
// Check /proc/self/fd for the current number of open fds, if possible
// (we may have inherited more than just the standard ones)
- struct bfs_dir *dir = bfs_opendir(AT_FDCWD, "/proc/self/fd");
+ struct bfs_dir *dir = bfs_allocdir();
if (!dir) {
- dir = bfs_opendir(AT_FDCWD, "/dev/fd");
+ goto done;
}
- if (dir) {
- // Account for 'dir' itself
- nopen = -1;
- while (bfs_readdir(dir, NULL) > 0) {
- ++nopen;
- }
+ if (bfs_opendir(dir, AT_FDCWD, "/proc/self/fd", 0) != 0
+ && bfs_opendir(dir, AT_FDCWD, "/dev/fd", 0) != 0) {
+ goto done;
+ }
- bfs_closedir(dir);
+ // Account for 'dir' itself
+ nopen = -1;
+
+ while (bfs_readdir(dir, NULL) > 0) {
+ ++nopen;
}
+ bfs_closedir(dir);
+done:
+ free(dir);
int ret = limit - nopen;
ret -= ctx->expr->persistent_fds;
@@ -1513,8 +1562,9 @@ static void dump_bftw_flags(enum bftw_flags flags) {
DEBUG_FLAG(flags, BFTW_PRUNE_MOUNTS);
DEBUG_FLAG(flags, BFTW_SORT);
DEBUG_FLAG(flags, BFTW_BUFFER);
+ DEBUG_FLAG(flags, BFTW_WHITEOUTS);
- assert(!flags);
+ bfs_assert(flags == 0, "Missing bftw flag 0x%X", flags);
}
/**
@@ -1546,12 +1596,8 @@ static bool eval_must_buffer(const struct bfs_expr *expr) {
return true;
}
- if (bfs_expr_has_children(expr)) {
- if (expr->lhs && eval_must_buffer(expr->lhs)) {
- return true;
- }
-
- if (expr->rhs && eval_must_buffer(expr->rhs)) {
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (eval_must_buffer(child)) {
return true;
}
}
@@ -1560,7 +1606,7 @@ static bool eval_must_buffer(const struct bfs_expr *expr) {
return false;
}
-int bfs_eval(const struct bfs_ctx *ctx) {
+int bfs_eval(struct bfs_ctx *ctx) {
if (!ctx->expr) {
return EXIT_SUCCESS;
}
@@ -1584,14 +1630,19 @@ int bfs_eval(const struct bfs_ctx *ctx) {
}
int fdlimit = raise_fdlimit(ctx);
+ reserve_fds(fdlimit);
fdlimit = infer_fdlimit(ctx, fdlimit);
+ // -1 for the main thread
+ int nthreads = ctx->threads - 1;
+
struct bftw_args bftw_args = {
.paths = ctx->paths,
- .npaths = darray_length(ctx->paths),
+ .npaths = ctx->npaths,
.callback = eval_callback,
.ptr = &args,
.nopenfd = fdlimit,
+ .nthreads = nthreads,
.flags = ctx->flags,
.strategy = ctx->strategy,
.mtab = bfs_ctx_mtab(ctx),
@@ -1611,6 +1662,7 @@ int bfs_eval(const struct bfs_ctx *ctx) {
fprintf(stderr, "\t.callback = eval_callback,\n");
fprintf(stderr, "\t.ptr = &args,\n");
fprintf(stderr, "\t.nopenfd = %d,\n", bftw_args.nopenfd);
+ fprintf(stderr, "\t.nthreads = %d,\n", bftw_args.nthreads);
fprintf(stderr, "\t.flags = ");
dump_bftw_flags(bftw_args.flags);
fprintf(stderr, ",\n\t.strategy = %s,\n", dump_bftw_strategy(bftw_args.strategy));
diff --git a/src/eval.h b/src/eval.h
index a50dc4e..4dd7996 100644
--- a/src/eval.h
+++ b/src/eval.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* The evaluation functions that implement primary expressions like -name,
@@ -22,7 +9,7 @@
#ifndef BFS_EVAL_H
#define BFS_EVAL_H
-#include <stdbool.h>
+#include "prelude.h"
struct bfs_ctx;
struct bfs_expr;
@@ -52,7 +39,7 @@ typedef bool bfs_eval_fn(const struct bfs_expr *expr, struct bfs_eval *state);
* @return
* EXIT_SUCCESS on success, otherwise on failure.
*/
-int bfs_eval(const struct bfs_ctx *ctx);
+int bfs_eval(struct bfs_ctx *ctx);
// Predicate evaluation functions
@@ -62,6 +49,7 @@ bool eval_false(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_access(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_acl(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_capable(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_context(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_perm(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_xattr(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_xattrname(const struct bfs_expr *expr, struct bfs_eval *state);
@@ -101,6 +89,7 @@ bool eval_fprint(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_fprint0(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_fprintf(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_fprintx(const struct bfs_expr *expr, struct bfs_eval *state);
+bool eval_limit(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_prune(const struct bfs_expr *expr, struct bfs_eval *state);
bool eval_quit(const struct bfs_expr *expr, struct bfs_eval *state);
diff --git a/src/exec.c b/src/exec.c
index 0130317..e782d49 100644
--- a/src/exec.c
+++ b/src/exec.c
@@ -1,32 +1,19 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "exec.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "bftw.h"
-#include "ctx.h"
#include "color.h"
+#include "ctx.h"
#include "diag.h"
#include "dstring.h"
-#include "util.h"
#include "xspawn.h"
-#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
-#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -35,7 +22,7 @@
#include <unistd.h>
/** Print some debugging info. */
-BFS_FORMATTER(2, 3)
+attr(printf(2, 3))
static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) {
const struct bfs_ctx *ctx = execbuf->ctx;
@@ -138,26 +125,16 @@ static void bfs_exec_parse_error(const struct bfs_ctx *ctx, const struct bfs_exe
}
struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs_exec_flags flags) {
- struct bfs_exec *execbuf = malloc(sizeof(*execbuf));
+ struct bfs_exec *execbuf = ZALLOC(struct bfs_exec);
if (!execbuf) {
- bfs_perror(ctx, "malloc()");
+ bfs_perror(ctx, "zalloc()");
goto fail;
}
execbuf->flags = flags;
execbuf->ctx = ctx;
execbuf->tmpl_argv = argv + 1;
- execbuf->tmpl_argc = 0;
- execbuf->argv = NULL;
- execbuf->argc = 0;
- execbuf->argv_cap = 0;
- execbuf->arg_size = 0;
- execbuf->arg_max = 0;
- execbuf->arg_min = 0;
execbuf->wd_fd = -1;
- execbuf->wd_path = NULL;
- execbuf->wd_len = 0;
- execbuf->ret = 0;
while (true) {
const char *arg = execbuf->tmpl_argv[execbuf->tmpl_argc];
@@ -172,7 +149,7 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs
goto fail;
} else if (strcmp(arg, ";") == 0) {
break;
- } else if (strcmp(arg, "+") == 0) {
+ } else if (execbuf->tmpl_argc > 0 && strcmp(arg, "+") == 0) {
const char *prev = execbuf->tmpl_argv[execbuf->tmpl_argc - 1];
if (!(execbuf->flags & BFS_EXEC_CONFIRM) && strcmp(prev, "{}") == 0) {
execbuf->flags |= BFS_EXEC_MULTI;
@@ -190,9 +167,9 @@ struct bfs_exec *bfs_exec_parse(const struct bfs_ctx *ctx, char **argv, enum bfs
}
execbuf->argv_cap = execbuf->tmpl_argc + 1;
- execbuf->argv = malloc(execbuf->argv_cap*sizeof(*execbuf->argv));
+ execbuf->argv = ALLOC_ARRAY(char *, execbuf->argv_cap);
if (!execbuf->argv) {
- bfs_perror(ctx, "malloc()");
+ bfs_perror(ctx, "alloc()");
goto fail;
}
@@ -238,9 +215,8 @@ static char *bfs_exec_format_path(const struct bfs_exec *execbuf, const struct B
return NULL;
}
- strcpy(path, "./");
- strcpy(path + 2, name);
-
+ char *cur = stpcpy(path, "./");
+ cur = stpcpy(cur, name);
return path;
}
@@ -251,7 +227,7 @@ static char *bfs_exec_format_arg(char *arg, const char *path) {
return arg;
}
- char *ret = dstralloc(0);
+ dchar *ret = dstralloc(0);
if (!ret) {
return NULL;
}
@@ -283,18 +259,18 @@ err:
/** Free a formatted argument. */
static void bfs_exec_free_arg(char *arg, const char *tmpl) {
if (arg != tmpl) {
- dstrfree(arg);
+ dstrfree((dchar *)arg);
}
}
/** Open a file to use as the working directory. */
static int bfs_exec_openwd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf) {
- assert(execbuf->wd_fd < 0);
- assert(!execbuf->wd_path);
+ bfs_assert(execbuf->wd_fd < 0);
+ bfs_assert(!execbuf->wd_path);
if (ftwbuf->at_fd != AT_FDCWD) {
// Rely on at_fd being the immediate parent
- assert(ftwbuf->at_path == xbasename(ftwbuf->at_path));
+ bfs_assert(xbaseoff(ftwbuf->at_path) == 0);
execbuf->wd_fd = ftwbuf->at_fd;
if (!(execbuf->flags & BFS_EXEC_MULTI)) {
@@ -351,6 +327,11 @@ static void bfs_exec_closewd(struct bfs_exec *execbuf, const struct BFTW *ftwbuf
/** Actually spawn the process. */
static int bfs_exec_spawn(const struct bfs_exec *execbuf) {
+ const struct bfs_ctx *ctx = execbuf->ctx;
+
+ // Flush the context state for consistency with the external process
+ bfs_ctx_flush(ctx);
+
if (execbuf->flags & BFS_EXEC_CONFIRM) {
for (size_t i = 0; i < execbuf->argc; ++i) {
if (fprintf(stderr, "%s ", execbuf->argv[i]) < 0) {
@@ -367,54 +348,48 @@ static int bfs_exec_spawn(const struct bfs_exec *execbuf) {
}
}
- // Flush cached state for consistency with the external process
- bfs_ctx_flush(execbuf->ctx);
-
if (execbuf->flags & BFS_EXEC_MULTI) {
bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments] (size %zu)\n",
- execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size);
+ execbuf->argv[0], execbuf->argc - 1, execbuf->arg_size);
} else {
bfs_exec_debug(execbuf, "Executing '%s' ... [%zu arguments]\n", execbuf->argv[0], execbuf->argc - 1);
}
pid_t pid = -1;
- int error;
- struct bfs_spawn ctx;
- if (bfs_spawn_init(&ctx) != 0) {
+ struct bfs_spawn spawn;
+ if (bfs_spawn_init(&spawn) != 0) {
return -1;
}
- if (bfs_spawn_setflags(&ctx, BFS_SPAWN_USEPATH) != 0) {
- goto fail;
- }
+ spawn.flags |= BFS_SPAWN_USE_PATH;
- // Reset RLIMIT_NOFILE, to avoid breaking applications that use select()
- struct rlimit rl = {
- .rlim_cur = execbuf->ctx->nofile_soft,
- .rlim_max = execbuf->ctx->nofile_hard,
- };
- if (bfs_spawn_addsetrlimit(&ctx, RLIMIT_NOFILE, &rl) != 0) {
- goto fail;
+ if (execbuf->wd_fd >= 0) {
+ if (bfs_spawn_addfchdir(&spawn, execbuf->wd_fd) != 0) {
+ goto fail;
+ }
}
- if (execbuf->wd_fd >= 0) {
- if (bfs_spawn_addfchdir(&ctx, execbuf->wd_fd) != 0) {
+ // Reset RLIMIT_NOFILE if necessary, to avoid breaking applications that use select()
+ if (rlim_cmp(ctx->orig_nofile.rlim_cur, ctx->cur_nofile.rlim_cur) < 0) {
+ if (bfs_spawn_setrlimit(&spawn, RLIMIT_NOFILE, &ctx->orig_nofile) != 0) {
goto fail;
}
}
- pid = bfs_spawn(execbuf->argv[0], &ctx, execbuf->argv, NULL);
-fail:
- error = errno;
- bfs_spawn_destroy(&ctx);
+ pid = bfs_spawn(execbuf->argv[0], &spawn, execbuf->argv, NULL);
+
+fail:;
+ int error = errno;
+
+ bfs_spawn_destroy(&spawn);
if (pid < 0) {
errno = error;
return -1;
}
int wstatus;
- if (waitpid(pid, &wstatus, 0) < 0) {
+ if (xwaitpid(pid, &wstatus, 0) < 0) {
return -1;
}
@@ -433,9 +408,9 @@ fail:
if (!str) {
str = "unknown";
}
- bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str);
+ bfs_warning(ctx, "Command '${ex}%s${rs}' terminated by signal %d (%s)\n", execbuf->argv[0], sig, str);
} else {
- bfs_warning(execbuf->ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]);
+ bfs_warning(ctx, "Command '${ex}%s${rs}' terminated abnormally\n", execbuf->argv[0]);
}
errno = 0;
@@ -495,7 +470,7 @@ static bool bfs_exec_args_remain(const struct bfs_exec *execbuf) {
static size_t bfs_exec_estimate_max(const struct bfs_exec *execbuf) {
size_t min = execbuf->arg_min;
size_t max = execbuf->arg_max;
- return min + (max - min)/2;
+ return min + (max - min) / 2;
}
/** Update the ARG_MAX lower bound from a successful execution. */
@@ -510,7 +485,7 @@ static void bfs_exec_update_min(struct bfs_exec *execbuf) {
size_t estimate = bfs_exec_estimate_max(execbuf);
bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n",
- execbuf->arg_min, execbuf->arg_max, estimate);
+ execbuf->arg_min, execbuf->arg_max, estimate);
}
}
@@ -526,7 +501,7 @@ static size_t bfs_exec_update_max(struct bfs_exec *execbuf) {
// Trim a fraction off the max size to avoid repeated failures near the
// top end of the working range
- size -= size/16;
+ size -= size / 16;
if (size < execbuf->arg_max) {
execbuf->arg_max = size;
@@ -539,7 +514,7 @@ static size_t bfs_exec_update_max(struct bfs_exec *execbuf) {
// Binary search for a more precise bound
size_t estimate = bfs_exec_estimate_max(execbuf);
bfs_exec_debug(execbuf, "ARG_MAX between [%zu, %zu], trying %zu\n",
- execbuf->arg_min, execbuf->arg_max, estimate);
+ execbuf->arg_min, execbuf->arg_max, estimate);
return estimate;
}
@@ -613,7 +588,7 @@ static bool bfs_exec_would_overflow(const struct bfs_exec *execbuf, const char *
size_t next_size = execbuf->arg_size + bfs_exec_arg_size(arg);
if (next_size > arg_max) {
bfs_exec_debug(execbuf, "Command size (%zu) would exceed maximum (%zu), executing buffered command\n",
- next_size, arg_max);
+ next_size, arg_max);
return true;
}
@@ -625,8 +600,8 @@ static int bfs_exec_push(struct bfs_exec *execbuf, char *arg) {
execbuf->argv[execbuf->argc] = arg;
if (execbuf->argc + 1 >= execbuf->argv_cap) {
- size_t cap = 2*execbuf->argv_cap;
- char **argv = realloc(execbuf->argv, cap*sizeof(*argv));
+ size_t cap = 2 * execbuf->argv_cap;
+ char **argv = REALLOC_ARRAY(char *, execbuf->argv, execbuf->argv_cap, cap);
if (!argv) {
return -1;
}
diff --git a/src/exec.h b/src/exec.h
index a3e3c71..9d4192d 100644
--- a/src/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.
diff --git a/src/expr.c b/src/expr.c
new file mode 100644
index 0000000..5784220
--- /dev/null
+++ b/src/expr.c
@@ -0,0 +1,85 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "expr.h"
+#include "alloc.h"
+#include "ctx.h"
+#include "diag.h"
+#include "eval.h"
+#include "exec.h"
+#include "list.h"
+#include "printf.h"
+#include "xregex.h"
+#include <string.h>
+
+struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval_fn, size_t argc, char **argv) {
+ struct bfs_expr *expr = arena_alloc(&ctx->expr_arena);
+ if (!expr) {
+ return NULL;
+ }
+
+ memset(expr, 0, sizeof(*expr));
+ expr->eval_fn = eval_fn;
+ expr->argc = argc;
+ expr->argv = argv;
+ expr->probability = 0.5;
+ SLIST_PREPEND(&ctx->expr_list, expr, freelist);
+
+ if (bfs_expr_is_parent(expr)) {
+ SLIST_INIT(&expr->children);
+ }
+
+ return expr;
+}
+
+bool bfs_expr_is_parent(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_and
+ || expr->eval_fn == eval_or
+ || expr->eval_fn == eval_not
+ || expr->eval_fn == eval_comma;
+}
+
+struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr) {
+ if (bfs_expr_is_parent(expr)) {
+ return expr->children.head;
+ } else {
+ return NULL;
+ }
+}
+
+void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child) {
+ bfs_assert(bfs_expr_is_parent(expr));
+
+ SLIST_APPEND(&expr->children, child);
+
+ if (!child->pure) {
+ expr->pure = false;
+ }
+
+ expr->persistent_fds += child->persistent_fds;
+ if (expr->ephemeral_fds < child->ephemeral_fds) {
+ expr->ephemeral_fds = child->ephemeral_fds;
+ }
+}
+
+void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children) {
+ while (!SLIST_EMPTY(children)) {
+ struct bfs_expr *child = SLIST_POP(children);
+ bfs_expr_append(expr, child);
+ }
+}
+
+bool bfs_expr_never_returns(const struct bfs_expr *expr) {
+ // Expressions that never return are vacuously both always true and always false
+ return expr->always_true && expr->always_false;
+}
+
+void bfs_expr_clear(struct bfs_expr *expr) {
+ if (expr->eval_fn == eval_exec) {
+ bfs_exec_free(expr->exec);
+ } else if (expr->eval_fn == eval_fprintf) {
+ bfs_printf_free(expr->printf);
+ } else if (expr->eval_fn == eval_regex) {
+ bfs_regfree(expr->regex);
+ }
+}
diff --git a/src/expr.h b/src/expr.h
index 1f1ece6..7bcace7 100644
--- a/src/expr.h
+++ b/src/expr.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* The expression tree representation.
@@ -21,11 +8,10 @@
#ifndef BFS_EXPR_H
#define BFS_EXPR_H
+#include "prelude.h"
#include "color.h"
#include "eval.h"
#include "stat.h"
-#include <stdbool.h>
-#include <stddef.h>
#include <sys/types.h>
#include <time.h>
@@ -88,9 +74,22 @@ enum bfs_size_unit {
};
/**
+ * A linked list of expressions.
+ */
+struct bfs_exprs {
+ struct bfs_expr *head;
+ struct bfs_expr **tail;
+};
+
+/**
* A command line expression.
*/
struct bfs_expr {
+ /** This expression's next sibling, if any. */
+ struct bfs_expr *next;
+ /** The next allocated expression. */
+ struct { struct bfs_expr *next; } freelist;
+
/** The function that evaluates this expression. */
bfs_eval_fn *eval_fn;
@@ -110,8 +109,8 @@ struct bfs_expr {
bool always_true;
/** Whether this expression always evaluates to false. */
bool always_false;
- /** Whether this expression doesn't appear on the command line. */
- bool synthetic;
+ /** Whether this expression uses stat(). */
+ bool calls_stat;
/** Estimated cost. */
float cost;
@@ -127,12 +126,7 @@ struct bfs_expr {
/** Auxilliary data for the evaluation function. */
union {
/** Child expressions. */
- struct {
- /** The left hand side of the expression. */
- struct bfs_expr *lhs;
- /** The right hand side of the expression. */
- struct bfs_expr *rhs;
- };
+ struct bfs_exprs children;
/** Integer comparisons. */
struct {
@@ -141,27 +135,33 @@ struct bfs_expr {
/** The comparison mode. */
enum bfs_int_cmp int_cmp;
- /** Optional extra data. */
- union {
- /** -size data. */
- enum bfs_size_unit size_unit;
-
- /** Timestamp comparison data. */
- struct {
- /** The stat field to look at. */
- enum bfs_stat_field stat_field;
- /** The reference time. */
- struct timespec reftime;
- /** The time unit. */
- enum bfs_time_unit time_unit;
- };
- };
+ /** -size data. */
+ enum bfs_size_unit size_unit;
+
+ /** The stat field to look at. */
+ enum bfs_stat_field stat_field;
+ /** The time unit. */
+ enum bfs_time_unit time_unit;
+ /** The reference time. */
+ struct timespec reftime;
+ };
+
+ /** String comparisons. */
+ struct {
+ /** String pattern. */
+ const char *pattern;
+ /** fnmatch() flags. */
+ int fnm_flags;
+ /** Whether strcmp() can be used instead of fnmatch(). */
+ bool literal;
};
/** Printing actions. */
struct {
/** The output stream. */
CFILE *cfile;
+ /** Optional file path. */
+ const char *path;
/** Optional -printf format. */
struct bfs_printf *printf;
};
@@ -202,20 +202,32 @@ struct bfs_expr {
};
};
-/** Singleton true expression instance. */
-extern struct bfs_expr bfs_true;
-/** Singleton false expression instance. */
-extern struct bfs_expr bfs_false;
+struct bfs_ctx;
/**
* Create a new expression.
*/
-struct bfs_expr *bfs_expr_new(bfs_eval_fn *eval, size_t argc, char **argv);
+struct bfs_expr *bfs_expr_new(struct bfs_ctx *ctx, bfs_eval_fn *eval, size_t argc, char **argv);
+
+/**
+ * @return Whether this type of expression has children.
+ */
+bool bfs_expr_is_parent(const struct bfs_expr *expr);
+
+/**
+ * @return The first child of this expression, or NULL if it has none.
+ */
+struct bfs_expr *bfs_expr_children(const struct bfs_expr *expr);
+
+/**
+ * Add a child to an expression.
+ */
+void bfs_expr_append(struct bfs_expr *expr, struct bfs_expr *child);
/**
- * @return Whether the expression has child expressions.
+ * Add a list of children to an expression.
*/
-bool bfs_expr_has_children(const struct bfs_expr *expr);
+void bfs_expr_extend(struct bfs_expr *expr, struct bfs_exprs *children);
/**
* @return Whether expr is known to always quit.
@@ -228,8 +240,8 @@ bool bfs_expr_never_returns(const struct bfs_expr *expr);
bool bfs_expr_cmp(const struct bfs_expr *expr, long long n);
/**
- * Free an expression tree.
+ * Free any resources owned by an expression.
*/
-void bfs_expr_free(struct bfs_expr *expr);
+void bfs_expr_clear(struct bfs_expr *expr);
#endif // BFS_EXPR_H
diff --git a/src/fsade.c b/src/fsade.c
index 1444cf4..d56fb07 100644
--- a/src/fsade.c
+++ b/src/fsade.c
@@ -1,55 +1,47 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2021 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "fsade.h"
+#include "atomic.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_HAS_SYS_EXTATTR
-# include <sys/extattr.h>
-#elif BFS_HAS_SYS_XATTR
-# include <sys/xattr.h>
+#if BFS_CAN_CHECK_CONTEXT
+# include <selinux/selinux.h>
#endif
-#if BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS
+#if BFS_USE_SYS_EXTATTR_H
+# include <sys/extattr.h>
+#elif BFS_USE_SYS_XATTR_H
+# include <sys/xattr.h>
+#endif
/**
* Many of the APIs used here don't have *at() variants, but we can try to
* emulate something similar if /proc/self/fd is available.
*/
+attr(maybe_unused)
static const char *fake_at(const struct BFTW *ftwbuf) {
- static bool proc_works = true;
- static bool proc_checked = false;
+ static atomic int proc_works = -1;
- char *path = NULL;
- if (!proc_works || ftwbuf->at_fd == AT_FDCWD) {
+ dchar *path = NULL;
+ if (ftwbuf->at_fd == AT_FDCWD || load(&proc_works, relaxed) == 0) {
goto fail;
}
@@ -58,11 +50,12 @@ static const char *fake_at(const struct BFTW *ftwbuf) {
goto fail;
}
- if (!proc_checked) {
- proc_checked = true;
+ if (load(&proc_works, relaxed) < 0) {
if (xfaccessat(AT_FDCWD, path, F_OK) != 0) {
- proc_works = false;
+ store(&proc_works, 0, relaxed);
goto fail;
+ } else {
+ store(&proc_works, 1, relaxed);
}
}
@@ -77,15 +70,17 @@ fail:
return ftwbuf->path;
}
+attr(maybe_unused)
static void free_fake_at(const struct BFTW *ftwbuf, const char *path) {
if (path != ftwbuf->path) {
- dstrfree((char *)path);
+ dstrfree((dchar *)path);
}
}
/**
* Check if an error was caused by the absence of support or data for a feature.
*/
+attr(maybe_unused)
static bool is_absence_error(int error) {
// If the OS doesn't support the feature, it's obviously not enabled for
// any files
@@ -124,28 +119,73 @@ static bool is_absence_error(int error) {
return false;
}
-#endif // BFS_CAN_CHECK_ACL || BFS_CAN_CHECK_CAPABILITIES || BFS_CAN_CHECK_XATTRS
-
#if BFS_CAN_CHECK_ACL
+#if BFS_HAS_ACL_GET_FILE
+
+/** Unified interface for incompatible acl_get_entry() implementations. */
+static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
+#if BFS_HAS_ACL_GET_ENTRY
+ int ret = acl_get_entry(acl, which, entry);
+# if __APPLE__
+ // POSIX.1e specifies a return value of 1 for success, but macOS returns 0 instead
+ return !ret;
+# else
+ return ret;
+# endif
+#elif __DragonFly__
+# if !defined(ACL_FIRST_ENTRY) && !defined(ACL_NEXT_ENTRY)
+# define ACL_FIRST_ENTRY 0
+# define ACL_NEXT_ENTRY 1
+# endif
+
+ switch (which) {
+ case ACL_FIRST_ENTRY:
+ *entry = &acl->acl_entry[0];
+ break;
+ case ACL_NEXT_ENTRY:
+ ++*entry;
+ break;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+
+ acl_entry_t last = &acl->acl_entry[acl->acl_cnt];
+ return *entry == last;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
+/** Unified interface for acl_get_tag_type(). */
+attr(maybe_unused)
+static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) {
+#if BFS_HAS_ACL_GET_TAG_TYPE
+ return acl_get_tag_type(entry, tag);
+#elif __DragonFly__
+ *tag = entry->ae_tag;
+ return 0;
+#else
+ errno = ENOTSUP;
+ return -1;
+#endif
+}
+
/** Check if a POSIX.1e ACL is non-trivial. */
static int bfs_check_posix1e_acl(acl_t acl, bool ignore_required) {
int ret = 0;
acl_entry_t entry;
- for (int status = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
-#if __APPLE__
- // POSIX.1e specifies a return value of 1 for success, but macOS
- // returns 0 instead
- status == 0;
-#else
+ for (int status = bfs_acl_entry(acl, ACL_FIRST_ENTRY, &entry);
status > 0;
-#endif
- status = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
+ status = bfs_acl_entry(acl, ACL_NEXT_ENTRY, &entry))
+ {
#if defined(ACL_USER_OBJ) && defined(ACL_GROUP_OBJ) && defined(ACL_OTHER)
if (ignore_required) {
acl_tag_t tag;
- if (acl_get_tag_type(entry, &tag) != 0) {
+ if (bfs_acl_tag_type(entry, &tag) != 0) {
ret = -1;
continue;
}
@@ -169,52 +209,56 @@ static int bfs_check_acl_type(acl_t acl, acl_type_t type) {
return bfs_check_posix1e_acl(acl, false);
}
-#if __FreeBSD__
+#if BFS_HAS_ACL_IS_TRIVIAL_NP
int trivial;
+ int ret = acl_is_trivial_np(acl, &trivial);
-#if 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 +280,7 @@ int bfs_check_acl(const struct BFTW *ftwbuf) {
error = errno;
acl_free(acl);
}
+#endif
free_fake_at(ftwbuf, path);
errno = error;
@@ -299,17 +344,62 @@ int bfs_check_capabilities(const struct BFTW *ftwbuf) {
#if BFS_CAN_CHECK_XATTRS
+#if BFS_USE_SYS_EXTATTR_H
+
+/** Wrapper for extattr_list_{file,link}. */
+static ssize_t bfs_extattr_list(const char *path, enum bfs_type type, int namespace) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_LIST_LINK
+ return extattr_list_link(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_LIST_FILE
+ return extattr_list_file(path, namespace, NULL, 0);
+#elif BFS_HAS_EXTATTR_GET_FILE
+ // From man extattr(2):
+ //
+ // In earlier versions of this API, passing an empty string for the
+ // attribute name to extattr_get_file() would return the list of attributes
+ // defined for the target object. This interface has been deprecated in
+ // preference to using the explicit list API, and should not be used.
+ return extattr_get_file(path, namespace, "", NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+/** Wrapper for extattr_get_{file,link}. */
+static ssize_t bfs_extattr_get(const char *path, enum bfs_type type, int namespace, const char *name) {
+ if (type == BFS_LNK) {
+#if BFS_HAS_EXTATTR_GET_LINK
+ return extattr_get_link(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+ }
+
+#if BFS_HAS_EXTATTR_GET_FILE
+ return extattr_get_file(path, namespace, name, NULL, 0);
+#else
+ return 0;
+#endif
+}
+
+#endif // BFS_USE_SYS_EXTATTR_H
+
int bfs_check_xattrs(const struct BFTW *ftwbuf) {
const char *path = fake_at(ftwbuf);
ssize_t len;
-#if BFS_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_SYS_EXTATTR_H
+ 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 +432,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_SYS_EXTATTR_H
+ 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 +477,32 @@ int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name) {
}
#endif
+
+char *bfs_getfilecon(const struct BFTW *ftwbuf) {
+#if BFS_CAN_CHECK_CONTEXT
+ const char *path = fake_at(ftwbuf);
+
+ char *con;
+ int ret;
+ if (ftwbuf->type == BFS_LNK) {
+ ret = lgetfilecon(path, &con);
+ } else {
+ ret = getfilecon(path, &con);
+ }
+
+ if (ret >= 0) {
+ return con;
+ } else {
+ return NULL;
+ }
+#else
+ errno = ENOTSUP;
+ return NULL;
+#endif
+}
+
+void bfs_freecon(char *con) {
+#if BFS_CAN_CHECK_CONTEXT
+ freecon(con);
+#endif
+}
diff --git a/src/fsade.h b/src/fsade.h
index e964112..eefef9f 100644
--- a/src/fsade.h
+++ b/src/fsade.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A facade over (file)system features that are (un)implemented differently
@@ -22,19 +9,15 @@
#ifndef BFS_FSADE_H
#define BFS_FSADE_H
-#include "util.h"
-#include <stdbool.h>
+#include "prelude.h"
-#define BFS_CAN_CHECK_ACL BFS_HAS_SYS_ACL
+#define BFS_CAN_CHECK_ACL (BFS_HAS_ACL_GET_FILE || BFS_HAS_ACL_TRIVIAL)
-#if !defined(BFS_CAN_CHECK_CAPABILITIES) && BFS_HAS_SYS_CAPABILITY && !__FreeBSD__
-# include <sys/capability.h>
-# ifdef CAP_CHOWN
-# define BFS_CAN_CHECK_CAPABILITIES true
-# endif
-#endif
+#define BFS_CAN_CHECK_CAPABILITIES BFS_USE_LIBCAP
-#define BFS_CAN_CHECK_XATTRS (BFS_HAS_SYS_EXTATTR || BFS_HAS_SYS_XATTR)
+#define BFS_CAN_CHECK_CONTEXT BFS_USE_LIBSELINUX
+
+#define BFS_CAN_CHECK_XATTRS (BFS_USE_SYS_EXTATTR_H || BFS_USE_SYS_XATTR_H)
struct BFTW;
@@ -80,4 +63,19 @@ int bfs_check_xattrs(const struct BFTW *ftwbuf);
*/
int bfs_check_xattr_named(const struct BFTW *ftwbuf, const char *name);
+/**
+ * Get a file's SELinux context
+ *
+ * @param ftwbuf
+ * The file to check.
+ * @return
+ * The file's SELinux context, or NULL on failure.
+ */
+char *bfs_getfilecon(const struct BFTW *ftwbuf);
+
+/**
+ * Free a bfs_getfilecon() result.
+ */
+void bfs_freecon(char *con);
+
#endif // BFS_FSADE_H
diff --git a/src/ioq.c b/src/ioq.c
new file mode 100644
index 0000000..43a1b35
--- /dev/null
+++ b/src/ioq.c
@@ -0,0 +1,1100 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * An asynchronous I/O queue implementation.
+ *
+ * struct ioq is composed of two separate queues:
+ *
+ * struct ioqq *pending; // Pending I/O requests
+ * struct ioqq *ready; // Ready I/O responses
+ *
+ * Worker threads pop requests from `pending`, execute them, and push them back
+ * to the `ready` queue. The main thread pushes requests to `pending` and pops
+ * them from `ready`.
+ *
+ * struct ioqq is a blocking MPMC queue (though it could be SPMC/MPSC for
+ * pending/ready respectively). It is implemented as a circular buffer:
+ *
+ * size_t mask; // (1 << N) - 1
+ * [padding]
+ * size_t head; // Writer index
+ * [padding]
+ * size_t tail; // Reader index
+ * [padding]
+ * ioq_slot slots[1 << N]; // Queue contents
+ *
+ * Pushes are implemented with an unconditional
+ *
+ * fetch_add(&ioqq->head, 1)
+ *
+ * which scales better on many architectures than compare-and-swap (see [1] for
+ * details). Pops are implemented similarly. Since the fetch-and-adds are
+ * unconditional, non-blocking readers can get ahead of writers:
+ *
+ * Reader Writer
+ * ──────────────── ──────────────────────
+ * head: 0 → 1
+ * slots[0]: empty
+ * tail: 0 → 1
+ * slots[0]: empty → full
+ * head: 1 → 2
+ * slots[1]: empty!
+ *
+ * To avoid this, non-blocking reads (ioqq_pop(ioqq, false)) must mark the slots
+ * somehow so that writers can skip them:
+ *
+ * Reader Writer
+ * ─────────────────────── ───────────────────────
+ * head: 0 → 1
+ * slots[0]: empty → skip
+ * tail: 0 → 1
+ * slots[0]: skip → empty
+ * tail: 1 → 2
+ * slots[1]: empty → full
+ * head: 1 → 2
+ * slots[1]: full → empty
+ *
+ * As well, a reader might "lap" a writer (or another reader), so slots need to
+ * count how many times they should be skipped:
+ *
+ * Reader Writer
+ * ────────────────────────── ─────────────────────────
+ * head: 0 → 1
+ * slots[0]: empty → skip(1)
+ * head: 1 → 2
+ * slots[1]: empty → skip(1)
+ * ...
+ * head: M → 0
+ * slots[M]: empty → skip(1)
+ * head: 0 → 1
+ * slots[0]: skip(1 → 2)
+ * tail: 0 → 1
+ * slots[0]: skip(2 → 1)
+ * tail: 1 → 2
+ * slots[1]: skip(1) → empty
+ * ...
+ * tail: M → 0
+ * slots[M]: skip(1) → empty
+ * tail: 0 → 1
+ * slots[0]: skip(1) → empty
+ * tail: 1 → 2
+ * slots[1]: empty → full
+ * head: 1 → 2
+ * slots[1]: full → empty
+ *
+ * As described in [1], this approach is susceptible to livelock if readers stay
+ * ahead of writers. This is okay for us because we don't retry failed non-
+ * blocking reads.
+ *
+ * The slot representation uses tag bits to hold either a pointer or skip(N):
+ *
+ * IOQ_SKIP (highest bit) IOQ_BLOCKED (lowest bit)
+ * ↓ ↓
+ * 0 0 0 ... 0 0 0
+ * └──────────┬──────────┘
+ * │
+ * value bits
+ *
+ * If IOQ_SKIP is unset, the value bits hold a pointer (or zero/NULL for empty).
+ * If IOQ_SKIP is set, the value bits hold a negative skip count. Writers can
+ * reduce the skip count by adding 1 to the value bits, and when the count hits
+ * zero, the carry will automatically clear IOQ_SKIP:
+ *
+ * IOQ_SKIP IOQ_BLOCKED
+ * ↓ ↓
+ * 1 1 1 ... 1 0 0 skip(2)
+ * 1 1 1 ... 1 1 0 skip(1)
+ * 0 0 0 ... 0 0 0 empty
+ *
+ * The IOQ_BLOCKED flag is used to track sleeping waiters, futex-style. To wait
+ * for a slot to change, waiters call ioq_slot_wait() which sets IOQ_BLOCKED and
+ * goes to sleep. Whenever a slot is updated, if the old value had IOQ_BLOCKED
+ * set, ioq_slot_wake() must be called to wake up that waiter.
+ *
+ * Blocking/waking uses a pool of monitors (mutex, condition variable pairs).
+ * Slots are assigned round-robin to a monitor from the pool.
+ *
+ * [1]: https://arxiv.org/abs/2201.02179
+ */
+
+#include "prelude.h"
+#include "ioq.h"
+#include "alloc.h"
+#include "atomic.h"
+#include "bfstd.h"
+#include "bit.h"
+#include "diag.h"
+#include "dir.h"
+#include "stat.h"
+#include "thread.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#if BFS_USE_LIBURING
+# include <liburing.h>
+#endif
+
+/**
+ * A monitor for an I/O queue slot.
+ */
+struct ioq_monitor {
+ cache_align pthread_mutex_t mutex;
+ pthread_cond_t cond;
+};
+
+/** Initialize an ioq_monitor. */
+static int ioq_monitor_init(struct ioq_monitor *monitor) {
+ if (mutex_init(&monitor->mutex, NULL) != 0) {
+ return -1;
+ }
+
+ if (cond_init(&monitor->cond, NULL) != 0) {
+ mutex_destroy(&monitor->mutex);
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Destroy an ioq_monitor. */
+static void ioq_monitor_destroy(struct ioq_monitor *monitor) {
+ cond_destroy(&monitor->cond);
+ mutex_destroy(&monitor->mutex);
+}
+
+/** A single entry in a command queue. */
+typedef atomic uintptr_t ioq_slot;
+
+/** Someone might be waiting on this slot. */
+#define IOQ_BLOCKED ((uintptr_t)1)
+
+/** Bit for IOQ_SKIP. */
+#define IOQ_SKIP_BIT (UINTPTR_WIDTH - 1)
+/** The next push(es) should skip this slot. */
+#define IOQ_SKIP ((uintptr_t)1 << IOQ_SKIP_BIT)
+/** Amount to add for an additional skip. */
+#define IOQ_SKIP_ONE (~IOQ_BLOCKED)
+
+// Need room for two flag bits
+bfs_static_assert(alignof(struct ioq_ent) >= (1 << 2));
+
+/**
+ * An MPMC queue of I/O commands.
+ */
+struct ioqq {
+ /** Circular buffer index mask. */
+ size_t slot_mask;
+
+ /** Monitor index mask. */
+ size_t monitor_mask;
+ /** Array of monitors used by the slots. */
+ struct ioq_monitor *monitors;
+
+ /** Index of next writer. */
+ cache_align atomic size_t head;
+ /** Index of next reader. */
+ cache_align atomic size_t tail;
+
+ /** The circular buffer itself. */
+ cache_align ioq_slot slots[];
+};
+
+/** Destroy an I/O command queue. */
+static void ioqq_destroy(struct ioqq *ioqq) {
+ if (!ioqq) {
+ return;
+ }
+
+ for (size_t i = 0; i < ioqq->monitor_mask + 1; ++i) {
+ ioq_monitor_destroy(&ioqq->monitors[i]);
+ }
+ free(ioqq->monitors);
+ free(ioqq);
+}
+
+/** Create an I/O command queue. */
+static struct ioqq *ioqq_create(size_t size) {
+ // Circular buffer size must be a power of two
+ size = bit_ceil(size);
+
+ struct ioqq *ioqq = ALLOC_FLEX(struct ioqq, slots, size);
+ if (!ioqq) {
+ return NULL;
+ }
+
+ ioqq->slot_mask = size - 1;
+ ioqq->monitor_mask = -1;
+
+ // Use a pool of monitors
+ size_t nmonitors = size < 64 ? size : 64;
+ ioqq->monitors = ALLOC_ARRAY(struct ioq_monitor, nmonitors);
+ if (!ioqq->monitors) {
+ ioqq_destroy(ioqq);
+ return NULL;
+ }
+
+ for (size_t i = 0; i < nmonitors; ++i) {
+ if (ioq_monitor_init(&ioqq->monitors[i]) != 0) {
+ ioqq_destroy(ioqq);
+ return NULL;
+ }
+ ++ioqq->monitor_mask;
+ }
+
+ atomic_init(&ioqq->head, 0);
+ atomic_init(&ioqq->tail, 0);
+
+ for (size_t i = 0; i < size; ++i) {
+ atomic_init(&ioqq->slots[i], 0);
+ }
+
+ return ioqq;
+}
+
+/** Get the monitor associated with a slot. */
+static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) {
+ size_t i = slot - ioqq->slots;
+ return &ioqq->monitors[i & ioqq->monitor_mask];
+}
+
+/** Atomically wait for a slot to change. */
+attr(noinline)
+static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) {
+ struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot);
+ mutex_lock(&monitor->mutex);
+
+ uintptr_t ret = load(slot, relaxed);
+ if (ret != value) {
+ goto done;
+ }
+
+ if (!(value & IOQ_BLOCKED)) {
+ value |= IOQ_BLOCKED;
+ if (!compare_exchange_strong(slot, &ret, value, relaxed, relaxed)) {
+ goto done;
+ }
+ }
+
+ do {
+ // To avoid missed wakeups, it is important that
+ // cond_broadcast() is not called right here
+ cond_wait(&monitor->cond, &monitor->mutex);
+ ret = load(slot, relaxed);
+ } while (ret == value);
+
+done:
+ mutex_unlock(&monitor->mutex);
+ return ret;
+}
+
+/** Wake up any threads waiting on a slot. */
+attr(noinline)
+static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) {
+ struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot);
+
+ // The following implementation would clearly avoid the missed wakeup
+ // issue mentioned above in ioq_slot_wait():
+ //
+ // mutex_lock(&monitor->mutex);
+ // cond_broadcast(&monitor->cond);
+ // mutex_unlock(&monitor->mutex);
+ //
+ // As a minor optimization, we move the broadcast outside of the lock.
+ // This optimization is correct, even though it leads to a seemingly-
+ // useless empty critical section.
+
+ mutex_lock(&monitor->mutex);
+ mutex_unlock(&monitor->mutex);
+ cond_broadcast(&monitor->cond);
+}
+
+/** Branch-free (slot & IOQ_SKIP) ? ~IOQ_BLOCKED : 0 */
+static uintptr_t ioq_skip_mask(uintptr_t slot) {
+ return -(slot >> IOQ_SKIP_BIT) << 1;
+}
+
+/** Push an entry into a slot. */
+static bool ioq_slot_push(struct ioqq *ioqq, ioq_slot *slot, struct ioq_ent *ent) {
+ uintptr_t prev = load(slot, relaxed);
+
+ while (true) {
+ size_t skip_mask = ioq_skip_mask(prev);
+ size_t full_mask = ~skip_mask & ~IOQ_BLOCKED;
+ if (prev & full_mask) {
+ // full(ptr) → wait
+ prev = ioq_slot_wait(ioqq, slot, prev);
+ continue;
+ }
+
+ // empty → full(ptr)
+ uintptr_t next = ((uintptr_t)ent >> 1) & full_mask;
+ // skip(1) → empty
+ // skip(n) → skip(n - 1)
+ next |= (prev - IOQ_SKIP_ONE) & skip_mask;
+
+ if (compare_exchange_weak(slot, &prev, next, release, relaxed)) {
+ break;
+ }
+ }
+
+ if (prev & IOQ_BLOCKED) {
+ ioq_slot_wake(ioqq, slot);
+ }
+
+ return !(prev & IOQ_SKIP);
+}
+
+/** (Try to) pop an entry from a slot. */
+static struct ioq_ent *ioq_slot_pop(struct ioqq *ioqq, ioq_slot *slot, bool block) {
+ uintptr_t prev = load(slot, relaxed);
+ while (true) {
+ // empty → skip(1)
+ // skip(n) → skip(n + 1)
+ // full(ptr) → full(ptr - 1)
+ uintptr_t next = prev + IOQ_SKIP_ONE;
+ // skip(n) → ~IOQ_BLOCKED
+ // full(ptr) → 0
+ next &= ioq_skip_mask(next);
+
+ if (block && next) {
+ prev = ioq_slot_wait(ioqq, slot, prev);
+ continue;
+ }
+
+ if (compare_exchange_weak(slot, &prev, next, acquire, relaxed)) {
+ break;
+ }
+ }
+
+ if (prev & IOQ_BLOCKED) {
+ ioq_slot_wake(ioqq, slot);
+ }
+
+ // empty → 0
+ // skip(n) → 0
+ // full(ptr) → ptr
+ prev &= ioq_skip_mask(~prev);
+ return (struct ioq_ent *)(prev << 1);
+}
+
+/** Push an entry onto the queue. */
+static void ioqq_push(struct ioqq *ioqq, struct ioq_ent *ent) {
+ while (true) {
+ size_t i = fetch_add(&ioqq->head, 1, relaxed);
+ ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask];
+ if (ioq_slot_push(ioqq, slot, ent)) {
+ break;
+ }
+ }
+}
+
+/** Push a batch of entries to the queue. */
+static void ioqq_push_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size) {
+ size_t mask = ioqq->slot_mask;
+ do {
+ size_t i = fetch_add(&ioqq->head, size, relaxed);
+ for (size_t j = i + size; i != j; ++i) {
+ ioq_slot *slot = &ioqq->slots[i & mask];
+ if (ioq_slot_push(ioqq, slot, *batch)) {
+ ++batch;
+ --size;
+ }
+ }
+ } while (size > 0);
+}
+
+/** Pop an entry from the queue. */
+static struct ioq_ent *ioqq_pop(struct ioqq *ioqq, bool block) {
+ size_t i = fetch_add(&ioqq->tail, 1, relaxed);
+ ioq_slot *slot = &ioqq->slots[i & ioqq->slot_mask];
+ return ioq_slot_pop(ioqq, slot, block);
+}
+
+/** Pop a batch of entries from the queue. */
+static void ioqq_pop_batch(struct ioqq *ioqq, struct ioq_ent *batch[], size_t size, bool block) {
+ size_t mask = ioqq->slot_mask;
+ size_t i = fetch_add(&ioqq->tail, size, relaxed);
+ for (size_t j = i + size; i != j; ++i) {
+ ioq_slot *slot = &ioqq->slots[i & mask];
+ *batch++ = ioq_slot_pop(ioqq, slot, block);
+ block = false;
+ }
+}
+
+/** Use cache-line-sized batches. */
+#define IOQ_BATCH (FALSE_SHARING_SIZE / sizeof(ioq_slot))
+
+/**
+ * A batch of entries to send all at once.
+ */
+struct ioq_batch {
+ /** The current batch size. */
+ size_t size;
+ /** The array of entries. */
+ struct ioq_ent *entries[IOQ_BATCH];
+};
+
+/** Send the batch to a queue. */
+static void ioq_batch_flush(struct ioqq *ioqq, struct ioq_batch *batch) {
+ if (batch->size > 0) {
+ ioqq_push_batch(ioqq, batch->entries, batch->size);
+ batch->size = 0;
+ }
+}
+
+/** An an entry to a batch, flushing if necessary. */
+static void ioq_batch_push(struct ioqq *ioqq, struct ioq_batch *batch, struct ioq_ent *ent) {
+ if (batch->size >= IOQ_BATCH) {
+ ioq_batch_flush(ioqq, batch);
+ }
+
+ batch->entries[batch->size++] = ent;
+}
+
+/** Sentinel stop command. */
+static struct ioq_ent IOQ_STOP;
+
+#if BFS_USE_LIBURING
+/**
+ * Supported io_uring operations.
+ */
+enum ioq_ring_ops {
+ IOQ_RING_OPENAT = 1 << 0,
+ IOQ_RING_CLOSE = 1 << 1,
+ IOQ_RING_STATX = 1 << 2,
+};
+#endif
+
+/** I/O queue thread-specific data. */
+struct ioq_thread {
+ /** The thread handle. */
+ pthread_t id;
+ /** Pointer back to the I/O queue. */
+ struct ioq *parent;
+
+#if BFS_USE_LIBURING
+ /** io_uring instance. */
+ struct io_uring ring;
+ /** Any error that occurred initializing the ring. */
+ int ring_err;
+ /** Bitmask of supported io_uring operations. */
+ enum ioq_ring_ops ring_ops;
+#endif
+};
+
+struct ioq {
+ /** The depth of the queue. */
+ size_t depth;
+ /** The current size of the queue. */
+ size_t size;
+ /** Cancellation flag. */
+ atomic bool cancel;
+
+ /** ioq_ent arena. */
+ struct arena ents;
+#if BFS_USE_LIBURING && BFS_USE_STATX
+ /** struct statx arena. */
+ struct arena xbufs;
+#endif
+
+ /** Pending I/O requests. */
+ struct ioqq *pending;
+ /** Ready I/O responses. */
+ struct ioqq *ready;
+
+ /** The number of background threads. */
+ size_t nthreads;
+ /** The background threads themselves. */
+ struct ioq_thread threads[];
+};
+
+/** Cancel a request if we need to. */
+static bool ioq_check_cancel(struct ioq *ioq, struct ioq_ent *ent) {
+ if (!load(&ioq->cancel, relaxed)) {
+ return false;
+ }
+
+ // Always close(), even if we're cancelled, just like a real EINTR
+ if (ent->op == IOQ_CLOSE || ent->op == IOQ_CLOSEDIR) {
+ return false;
+ }
+
+ ent->result = -EINTR;
+ return true;
+}
+
+/** Dispatch a single request synchronously. */
+static void ioq_dispatch_sync(struct ioq *ioq, struct ioq_ent *ent) {
+ switch (ent->op) {
+ case IOQ_CLOSE:
+ ent->result = try(xclose(ent->close.fd));
+ return;
+
+ case IOQ_OPENDIR: {
+ struct ioq_opendir *args = &ent->opendir;
+ ent->result = try(bfs_opendir(args->dir, args->dfd, args->path, args->flags));
+ if (ent->result >= 0) {
+ bfs_polldir(args->dir);
+ }
+ return;
+ }
+
+ case IOQ_CLOSEDIR:
+ ent->result = try(bfs_closedir(ent->closedir.dir));
+ return;
+
+ case IOQ_STAT: {
+ struct ioq_stat *args = &ent->stat;
+ ent->result = try(bfs_stat(args->dfd, args->path, args->flags, args->buf));
+ return;
+ }
+ }
+
+ bfs_bug("Unknown ioq_op %d", (int)ent->op);
+ ent->result = -ENOSYS;
+}
+
+#if BFS_USE_LIBURING
+
+/** io_uring worker state. */
+struct ioq_ring_state {
+ /** The I/O queue. */
+ struct ioq *ioq;
+ /** The io_uring. */
+ struct io_uring *ring;
+ /** Supported io_uring operations. */
+ enum ioq_ring_ops ops;
+ /** Number of prepped, unsubmitted SQEs. */
+ size_t prepped;
+ /** Number of submitted, unreaped SQEs. */
+ size_t submitted;
+ /** Whether to stop the loop. */
+ bool stop;
+ /** A batch of ready entries. */
+ struct ioq_batch ready;
+};
+
+/** Dispatch a single request asynchronously. */
+static struct io_uring_sqe *ioq_dispatch_async(struct ioq_ring_state *state, struct ioq_ent *ent) {
+ struct io_uring *ring = state->ring;
+ enum ioq_ring_ops ops = state->ops;
+ struct io_uring_sqe *sqe = NULL;
+
+ switch (ent->op) {
+ case IOQ_CLOSE:
+ if (ops & IOQ_RING_CLOSE) {
+ sqe = io_uring_get_sqe(ring);
+ io_uring_prep_close(sqe, ent->close.fd);
+ }
+ return sqe;
+
+ case IOQ_OPENDIR:
+ if (ops & IOQ_RING_OPENAT) {
+ sqe = io_uring_get_sqe(ring);
+ struct ioq_opendir *args = &ent->opendir;
+ int flags = O_RDONLY | O_CLOEXEC | O_DIRECTORY;
+ io_uring_prep_openat(sqe, args->dfd, args->path, flags, 0);
+ }
+ return sqe;
+
+ case IOQ_CLOSEDIR:
+#if BFS_USE_UNWRAPDIR
+ if (ops & IOQ_RING_CLOSE) {
+ sqe = io_uring_get_sqe(ring);
+ io_uring_prep_close(sqe, bfs_unwrapdir(ent->closedir.dir));
+ }
+#endif
+ return sqe;
+
+ case IOQ_STAT:
+#if BFS_USE_STATX
+ if (ops & IOQ_RING_STATX) {
+ sqe = io_uring_get_sqe(ring);
+ struct ioq_stat *args = &ent->stat;
+ int flags = bfs_statx_flags(args->flags);
+ unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
+ io_uring_prep_statx(sqe, args->dfd, args->path, flags, mask, args->xbuf);
+ }
+#endif
+ return sqe;
+ }
+
+ bfs_bug("Unknown ioq_op %d", (int)ent->op);
+ return NULL;
+}
+
+/** Check if ioq_ring_reap() has work to do. */
+static bool ioq_ring_empty(struct ioq_ring_state *state) {
+ return !state->prepped && !state->submitted && !state->ready.size;
+}
+
+/** Prep a single SQE. */
+static void ioq_prep_sqe(struct ioq_ring_state *state, struct ioq_ent *ent) {
+ struct ioq *ioq = state->ioq;
+ if (ioq_check_cancel(ioq, ent)) {
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+ return;
+ }
+
+ struct io_uring_sqe *sqe = ioq_dispatch_async(state, ent);
+ if (sqe) {
+ io_uring_sqe_set_data(sqe, ent);
+ ++state->prepped;
+ } else {
+ ioq_dispatch_sync(ioq, ent);
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+ }
+}
+
+/** Prep a batch of SQEs. */
+static bool ioq_ring_prep(struct ioq_ring_state *state) {
+ if (state->stop) {
+ return false;
+ }
+
+ struct ioq *ioq = state->ioq;
+ struct io_uring *ring = state->ring;
+ struct ioq_ent *pending[IOQ_BATCH];
+
+ while (io_uring_sq_space_left(ring) >= IOQ_BATCH) {
+ bool block = ioq_ring_empty(state);
+ ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, block);
+
+ bool any = false;
+ for (size_t i = 0; i < IOQ_BATCH; ++i) {
+ struct ioq_ent *ent = pending[i];
+ if (ent == &IOQ_STOP) {
+ ioqq_push(ioq->pending, &IOQ_STOP);
+ state->stop = true;
+ goto done;
+ } else if (ent) {
+ ioq_prep_sqe(state, ent);
+ any = true;
+ }
+ }
+
+ if (!any) {
+ break;
+ }
+ }
+
+done:
+ return !ioq_ring_empty(state);
+}
+
+/** Reap a single CQE. */
+static void ioq_reap_cqe(struct ioq_ring_state *state, struct io_uring_cqe *cqe) {
+ struct ioq *ioq = state->ioq;
+ struct io_uring *ring = state->ring;
+
+ struct ioq_ent *ent = io_uring_cqe_get_data(cqe);
+ ent->result = cqe->res;
+ io_uring_cqe_seen(ring, cqe);
+ --state->submitted;
+
+ if (ent->result < 0) {
+ goto push;
+ }
+
+ switch (ent->op) {
+ case IOQ_OPENDIR: {
+ int fd = ent->result;
+ if (ioq_check_cancel(ioq, ent)) {
+ xclose(fd);
+ goto push;
+ }
+
+ struct ioq_opendir *args = &ent->opendir;
+ ent->result = try(bfs_opendir(args->dir, fd, NULL, args->flags));
+ if (ent->result >= 0) {
+ // TODO: io_uring_prep_getdents()
+ bfs_polldir(args->dir);
+ } else {
+ xclose(fd);
+ }
+
+ break;
+ }
+
+#if BFS_USE_STATX
+ case IOQ_STAT: {
+ struct ioq_stat *args = &ent->stat;
+ ent->result = try(bfs_statx_convert(args->buf, args->xbuf));
+ break;
+ }
+#endif
+
+ default:
+ break;
+ }
+
+push:
+ ioq_batch_push(ioq->ready, &state->ready, ent);
+}
+
+/** Reap a batch of CQEs. */
+static void ioq_ring_reap(struct ioq_ring_state *state) {
+ struct ioq *ioq = state->ioq;
+ struct io_uring *ring = state->ring;
+
+ while (state->prepped) {
+ int ret = io_uring_submit_and_wait(ring, 1);
+ if (ret > 0) {
+ state->prepped -= ret;
+ state->submitted += ret;
+ }
+ }
+
+ while (state->submitted) {
+ struct io_uring_cqe *cqe;
+ if (io_uring_wait_cqe(ring, &cqe) < 0) {
+ continue;
+ }
+
+ ioq_reap_cqe(state, cqe);
+ }
+
+ ioq_batch_flush(ioq->ready, &state->ready);
+}
+
+/** io_uring worker loop. */
+static void ioq_ring_work(struct ioq_thread *thread) {
+ struct ioq_ring_state state = {
+ .ioq = thread->parent,
+ .ring = &thread->ring,
+ .ops = thread->ring_ops,
+ };
+
+ while (ioq_ring_prep(&state)) {
+ ioq_ring_reap(&state);
+ }
+}
+
+#endif // BFS_USE_LIBURING
+
+/** Synchronous syscall loop. */
+static void ioq_sync_work(struct ioq_thread *thread) {
+ struct ioq *ioq = thread->parent;
+
+ bool stop = false;
+ while (!stop) {
+ struct ioq_ent *pending[IOQ_BATCH];
+ ioqq_pop_batch(ioq->pending, pending, IOQ_BATCH, true);
+
+ struct ioq_batch ready;
+ ready.size = 0;
+
+ for (size_t i = 0; i < IOQ_BATCH; ++i) {
+ struct ioq_ent *ent = pending[i];
+ if (ent == &IOQ_STOP) {
+ ioqq_push(ioq->pending, &IOQ_STOP);
+ stop = true;
+ break;
+ } else if (ent) {
+ if (!ioq_check_cancel(ioq, ent)) {
+ ioq_dispatch_sync(ioq, ent);
+ }
+ ioq_batch_push(ioq->ready, &ready, ent);
+ }
+ }
+
+ ioq_batch_flush(ioq->ready, &ready);
+ }
+}
+
+/** Background thread entry point. */
+static void *ioq_work(void *ptr) {
+ struct ioq_thread *thread = ptr;
+
+#if BFS_USE_LIBURING
+ if (thread->ring_err == 0) {
+ ioq_ring_work(thread);
+ return NULL;
+ }
+#endif
+
+ ioq_sync_work(thread);
+ return NULL;
+}
+
+/** Initialize io_uring thread state. */
+static int ioq_ring_init(struct ioq *ioq, struct ioq_thread *thread) {
+#if BFS_USE_LIBURING
+ struct ioq_thread *prev = NULL;
+ if (thread > ioq->threads) {
+ prev = thread - 1;
+ }
+
+ if (prev && prev->ring_err) {
+ thread->ring_err = prev->ring_err;
+ return -1;
+ }
+
+ // Share io-wq workers between rings
+ struct io_uring_params params = {0};
+ if (prev) {
+ params.flags |= IORING_SETUP_ATTACH_WQ;
+ params.wq_fd = prev->ring.ring_fd;
+ }
+
+ // Use a page for each SQE ring
+ size_t entries = 4096 / sizeof(struct io_uring_sqe);
+ thread->ring_err = -io_uring_queue_init_params(entries, &thread->ring, &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;
+ }
+
+ // Limit the number of io_uring workers
+ unsigned int values[] = {
+ ioq->nthreads, // [IO_WQ_BOUND]
+ 0, // [IO_WQ_UNBOUND]
+ };
+ io_uring_register_iowq_max_workers(&thread->ring, values);
+#endif
+
+ return 0;
+}
+
+/** Destroy an io_uring. */
+static void ioq_ring_exit(struct ioq_thread *thread) {
+#if BFS_USE_LIBURING
+ if (thread->ring_err == 0) {
+ io_uring_queue_exit(&thread->ring);
+ }
+#endif
+}
+
+/** Create an I/O queue thread. */
+static int ioq_thread_create(struct ioq *ioq, struct ioq_thread *thread) {
+ thread->parent = ioq;
+
+ ioq_ring_init(ioq, thread);
+
+ if (thread_create(&thread->id, NULL, ioq_work, thread) != 0) {
+ ioq_ring_exit(thread);
+ return -1;
+ }
+
+ return 0;
+}
+
+/** Join an I/O queue thread. */
+static void ioq_thread_join(struct ioq_thread *thread) {
+ thread_join(thread->id, NULL);
+ ioq_ring_exit(thread);
+}
+
+struct ioq *ioq_create(size_t depth, size_t nthreads) {
+ struct ioq *ioq = ZALLOC_FLEX(struct ioq, threads, nthreads);
+ if (!ioq) {
+ goto fail;
+ }
+
+ ioq->depth = depth;
+
+ ARENA_INIT(&ioq->ents, struct ioq_ent);
+#if BFS_USE_LIBURING && BFS_USE_STATX
+ ARENA_INIT(&ioq->xbufs, struct statx);
+#endif
+
+ ioq->pending = ioqq_create(depth);
+ if (!ioq->pending) {
+ goto fail;
+ }
+
+ ioq->ready = ioqq_create(depth);
+ if (!ioq->ready) {
+ goto fail;
+ }
+
+ ioq->nthreads = nthreads;
+ for (size_t i = 0; i < nthreads; ++i) {
+ if (ioq_thread_create(ioq, &ioq->threads[i]) != 0) {
+ ioq->nthreads = i;
+ goto fail;
+ }
+ }
+
+ return ioq;
+
+ int err;
+fail:
+ err = errno;
+ ioq_destroy(ioq);
+ errno = err;
+ return NULL;
+}
+
+size_t ioq_capacity(const struct ioq *ioq) {
+ return ioq->depth - ioq->size;
+}
+
+static struct ioq_ent *ioq_request(struct ioq *ioq, enum ioq_op op, void *ptr) {
+ if (load(&ioq->cancel, relaxed)) {
+ errno = EINTR;
+ return NULL;
+ }
+
+ if (ioq->size >= ioq->depth) {
+ errno = EAGAIN;
+ return NULL;
+ }
+
+ struct ioq_ent *ent = arena_alloc(&ioq->ents);
+ if (!ent) {
+ return NULL;
+ }
+
+ ent->op = op;
+ ent->ptr = ptr;
+ ++ioq->size;
+ return ent;
+}
+
+int ioq_close(struct ioq *ioq, int fd, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSE, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ ent->close.fd = fd;
+
+ ioqq_push(ioq->pending, ent);
+ return 0;
+}
+
+int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_OPENDIR, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ struct ioq_opendir *args = &ent->opendir;
+ args->dir = dir;
+ args->dfd = dfd;
+ args->path = path;
+ args->flags = flags;
+
+ ioqq_push(ioq->pending, ent);
+ return 0;
+}
+
+int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_CLOSEDIR, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ ent->closedir.dir = dir;
+
+ ioqq_push(ioq->pending, ent);
+ return 0;
+}
+
+int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr) {
+ struct ioq_ent *ent = ioq_request(ioq, IOQ_STAT, ptr);
+ if (!ent) {
+ return -1;
+ }
+
+ struct ioq_stat *args = &ent->stat;
+ args->dfd = dfd;
+ args->path = path;
+ args->flags = flags;
+ args->buf = buf;
+
+#if BFS_USE_LIBURING && BFS_USE_STATX
+ args->xbuf = arena_alloc(&ioq->xbufs);
+ if (!args->xbuf) {
+ ioq_free(ioq, ent);
+ return -1;
+ }
+#endif
+
+ ioqq_push(ioq->pending, ent);
+ return 0;
+}
+
+struct ioq_ent *ioq_pop(struct ioq *ioq, bool block) {
+ if (ioq->size == 0) {
+ return NULL;
+ }
+
+ return ioqq_pop(ioq->ready, block);
+}
+
+void ioq_free(struct ioq *ioq, struct ioq_ent *ent) {
+ bfs_assert(ioq->size > 0);
+ --ioq->size;
+
+#if BFS_USE_LIBURING && BFS_USE_STATX
+ if (ent->op == IOQ_STAT && ent->stat.xbuf) {
+ arena_free(&ioq->xbufs, ent->stat.xbuf);
+ }
+#endif
+
+ arena_free(&ioq->ents, ent);
+}
+
+void ioq_cancel(struct ioq *ioq) {
+ if (!exchange(&ioq->cancel, true, relaxed)) {
+ ioqq_push(ioq->pending, &IOQ_STOP);
+ }
+}
+
+void ioq_destroy(struct ioq *ioq) {
+ if (!ioq) {
+ return;
+ }
+
+ if (ioq->nthreads > 0) {
+ ioq_cancel(ioq);
+ }
+
+ for (size_t i = 0; i < ioq->nthreads; ++i) {
+ ioq_thread_join(&ioq->threads[i]);
+ }
+
+ ioqq_destroy(ioq->ready);
+ ioqq_destroy(ioq->pending);
+
+#if BFS_USE_LIBURING && BFS_USE_STATX
+ arena_destroy(&ioq->xbufs);
+#endif
+ arena_destroy(&ioq->ents);
+
+ free(ioq);
+}
diff --git a/src/ioq.h b/src/ioq.h
new file mode 100644
index 0000000..d8e1573
--- /dev/null
+++ b/src/ioq.h
@@ -0,0 +1,198 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Asynchronous I/O queues.
+ */
+
+#ifndef BFS_IOQ_H
+#define BFS_IOQ_H
+
+#include "prelude.h"
+#include "dir.h"
+#include "stat.h"
+#include <stddef.h>
+
+/**
+ * An queue of asynchronous I/O operations.
+ */
+struct ioq;
+
+/**
+ * I/O queue operations.
+ */
+enum ioq_op {
+ /** ioq_close(). */
+ IOQ_CLOSE,
+ /** ioq_opendir(). */
+ IOQ_OPENDIR,
+ /** ioq_closedir(). */
+ IOQ_CLOSEDIR,
+ /** ioq_stat(). */
+ IOQ_STAT,
+};
+
+/**
+ * The I/O queue implementation needs two tag bits in each pointer to a struct
+ * ioq_ent, so we need to ensure at least 4-byte alignment. The natural
+ * alignment is enough on most architectures, but not m68k, so over-align it.
+ */
+#define IOQ_ENT_ALIGN alignas(4)
+
+/**
+ * An I/O queue entry.
+ */
+struct ioq_ent {
+ /** The I/O operation. */
+ IOQ_ENT_ALIGN enum ioq_op op;
+
+ /** The return value (on success) or negative error code (on failure). */
+ int result;
+
+ /** Arbitrary user data. */
+ void *ptr;
+
+ /** Operation-specific arguments. */
+ union {
+ /** ioq_close() args. */
+ struct ioq_close {
+ int fd;
+ } close;
+ /** ioq_opendir() args. */
+ struct ioq_opendir {
+ struct bfs_dir *dir;
+ const char *path;
+ int dfd;
+ enum bfs_dir_flags flags;
+ } opendir;
+ /** ioq_closedir() args. */
+ struct ioq_closedir {
+ struct bfs_dir *dir;
+ } closedir;
+ /** ioq_stat() args. */
+ struct ioq_stat {
+ const char *path;
+ struct bfs_stat *buf;
+ void *xbuf;
+ int dfd;
+ enum bfs_stat_flags flags;
+ } stat;
+ };
+};
+
+/**
+ * Create an I/O queue.
+ *
+ * @param depth
+ * The maximum depth of the queue.
+ * @param nthreads
+ * The maximum number of background threads.
+ * @return
+ * The new I/O queue, or NULL on failure.
+ */
+struct ioq *ioq_create(size_t depth, size_t nthreads);
+
+/**
+ * Check the remaining capacity of a queue.
+ */
+size_t ioq_capacity(const struct ioq *ioq);
+
+/**
+ * Asynchronous close().
+ *
+ * @param ioq
+ * The I/O queue.
+ * @param fd
+ * The fd to close.
+ * @param ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_close(struct ioq *ioq, int fd, void *ptr);
+
+/**
+ * Asynchronous bfs_opendir().
+ *
+ * @param ioq
+ * The I/O queue.
+ * @param dir
+ * The allocated directory.
+ * @param dfd
+ * The base file descriptor.
+ * @param path
+ * The path to open, relative to dfd.
+ * @param flags
+ * Flags that control which directory entries are listed.
+ * @param ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_opendir(struct ioq *ioq, struct bfs_dir *dir, int dfd, const char *path, enum bfs_dir_flags flags, void *ptr);
+
+/**
+ * Asynchronous bfs_closedir().
+ *
+ * @param ioq
+ * The I/O queue.
+ * @param dir
+ * The directory to close.
+ * @param ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_closedir(struct ioq *ioq, struct bfs_dir *dir, void *ptr);
+
+/**
+ * Asynchronous bfs_stat().
+ *
+ * @param ioq
+ * The I/O queue.
+ * @param dfd
+ * The base file descriptor.
+ * @param path
+ * The path to stat, relative to dfd.
+ * @param flags
+ * Flags that affect the lookup.
+ * @param buf
+ * A place to store the stat buffer, if successful.
+ * @param ptr
+ * An arbitrary pointer to associate with the request.
+ * @return
+ * 0 on success, or -1 on failure.
+ */
+int ioq_stat(struct ioq *ioq, int dfd, const char *path, enum bfs_stat_flags flags, struct bfs_stat *buf, void *ptr);
+
+/**
+ * Pop a response from the queue.
+ *
+ * @param ioq
+ * The I/O queue.
+ * @return
+ * The next response, or NULL.
+ */
+struct ioq_ent *ioq_pop(struct ioq *ioq, bool block);
+
+/**
+ * Free a queue entry.
+ *
+ * @param ioq
+ * The I/O queue.
+ * @param ent
+ * The entry to free.
+ */
+void ioq_free(struct ioq *ioq, struct ioq_ent *ent);
+
+/**
+ * Cancel any pending I/O operations.
+ */
+void ioq_cancel(struct ioq *ioq);
+
+/**
+ * Stop and destroy an I/O queue.
+ */
+void ioq_destroy(struct ioq *ioq);
+
+#endif // BFS_IOQ_H
diff --git a/src/list.h b/src/list.h
new file mode 100644
index 0000000..61d0e5b
--- /dev/null
+++ b/src/list.h
@@ -0,0 +1,581 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Intrusive linked lists.
+ *
+ * Singly-linked lists are declared like this:
+ *
+ * struct item {
+ * struct item *next;
+ * };
+ *
+ * struct list {
+ * struct item *head;
+ * struct item **tail;
+ * };
+ *
+ * The SLIST_*() macros manipulate singly-linked lists.
+ *
+ * struct list list;
+ * SLIST_INIT(&list);
+ *
+ * struct item item;
+ * SLIST_ITEM_INIT(&item);
+ * SLIST_APPEND(&list, &item);
+ *
+ * Doubly linked lists are similar:
+ *
+ * struct item {
+ * struct item *next;
+ * struct item *prev;
+ * };
+ *
+ * struct list {
+ * struct item *head;
+ * struct item *tail;
+ * };
+ *
+ * struct list list;
+ * LIST_INIT(&list);
+ *
+ * struct item item;
+ * LIST_ITEM_INIT(&item);
+ * LIST_APPEND(&list, &item);
+ *
+ * Items can be on multiple lists at once:
+ *
+ * struct item {
+ * struct {
+ * struct item *next;
+ * } chain;
+ *
+ * struct {
+ * struct item *next;
+ * struct item *prev;
+ * } lru;
+ * };
+ *
+ * struct items {
+ * struct {
+ * struct item *head;
+ * struct item **tail;
+ * } queue;
+ *
+ * struct {
+ * struct item *head;
+ * struct item *tail;
+ * } cache;
+ * };
+ *
+ * struct items items;
+ * SLIST_INIT(&items.queue);
+ * LIST_INIT(&items.cache);
+ *
+ * struct item item;
+ * SLIST_ITEM_INIT(&item, chain);
+ * SLIST_APPEND(&items.queue, &item, chain);
+ * LIST_ITEM_INIT(&item, lru);
+ * LIST_APPEND(&items.cache, &item, lru);
+ */
+
+#ifndef BFS_LIST_H
+#define BFS_LIST_H
+
+#include "diag.h"
+#include <stddef.h>
+#include <string.h>
+
+/**
+ * Initialize a singly-linked list.
+ *
+ * @param list
+ * The list to initialize.
+ *
+ * ---
+ *
+ * Like many macros in this file, this macro delegates the bulk of its work to
+ * some helper macros. We explicitly parenthesize (list) here so the helpers
+ * don't have to.
+ */
+#define SLIST_INIT(list) \
+ SLIST_INIT_((list))
+
+/**
+ * Helper for SLIST_INIT().
+ */
+#define SLIST_INIT_(list) LIST_VOID_( \
+ list->head = NULL, \
+ list->tail = &list->head)
+
+/**
+ * Cast a list of expressions to void.
+ */
+#define LIST_VOID_(...) ((void)(__VA_ARGS__))
+
+/**
+ * Initialize a singly-linked list item.
+ *
+ * @param item
+ * The item to initialize.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ *
+ * ---
+ *
+ * We play some tricks with variadic macros to handle the optional parameter:
+ *
+ * SLIST_ITEM_INIT(item) => item->next = NULL
+ * SLIST_ITEM_INIT(item, node) => item->node.next = NULL
+ *
+ * The first trick is that
+ *
+ * #define SLIST_ITEM_INIT(item, ...)
+ *
+ * won't work because both commas are required (until C23; see N3033). As a
+ * workaround, we dispatch to another macro and add a trailing comma.
+ *
+ * SLIST_ITEM_INIT(item) => SLIST_ITEM_INIT_(item, )
+ * SLIST_ITEM_INIT(item, node) => SLIST_ITEM_INIT_(item, node, )
+ */
+#define SLIST_ITEM_INIT(...) \
+ SLIST_ITEM_INIT_(__VA_ARGS__, )
+
+/**
+ * Now we need a way to generate either ->next or ->node.next depending on
+ * whether the node parameter was passed. The approach is based on
+ *
+ * #define FOO(...) BAR(__VA_ARGS__, 1, 2, )
+ * #define BAR(x, y, z, ...) z
+ *
+ * FOO(a) => 2
+ * FOO(a, b) => 1
+ *
+ * The LIST_NEXT_() macro uses this technique:
+ *
+ * LIST_NEXT_() => LIST_NODE_(next, )
+ * LIST_NEXT_(node, ) => LIST_NODE_(next, node, )
+ */
+#define LIST_NEXT_(...) \
+ LIST_NODE_(next, __VA_ARGS__)
+
+/**
+ * LIST_NODE_() dispatches to yet another macro:
+ *
+ * LIST_NODE_(next, ) => LIST_NODE__(next, , . , , )
+ * LIST_NODE_(next, node, ) => LIST_NODE__(next, node, , . , , )
+ */
+#define LIST_NODE_(dir, ...) \
+ LIST_NODE__(dir, __VA_ARGS__, . , , )
+
+/**
+ * And finally, LIST_NODE__() adds the node and the dot if necessary.
+ *
+ * dir node ignored dot
+ * v v v v
+ * LIST_NODE__(next, , . , , ) => next
+ * LIST_NODE__(next, node, , . , , ) => node . next
+ * ^ ^ ^ ^
+ * dir node ignored dot
+ */
+#define LIST_NODE__(dir, node, ignored, dot, ...) \
+ node dot dir
+
+/**
+ * SLIST_ITEM_INIT_() uses LIST_NEXT_() to generate the right name for the list
+ * node, and finally delegates to the actual implementation.
+ */
+#define SLIST_ITEM_INIT_(item, ...) \
+ SLIST_ITEM_INIT__((item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_ITEM_INIT__(item, next) \
+ LIST_VOID_(item->next = NULL)
+
+/**
+ * Type-checking macro for singly-linked lists.
+ */
+#define SLIST_CHECK_(list) \
+ (void)sizeof(list->tail - &list->head)
+
+/**
+ * Get the head of a singly-linked list.
+ *
+ * @param list
+ * The list in question.
+ * @return
+ * The first item in the list.
+ */
+#define SLIST_HEAD(list) \
+ SLIST_HEAD_((list))
+
+#define SLIST_HEAD_(list) \
+ (SLIST_CHECK_(list), list->head)
+
+/**
+ * Check if a singly-linked list is empty.
+ */
+#define SLIST_EMPTY(list) \
+ (!SLIST_HEAD(list))
+
+/**
+ * Like container_of(), but using the head pointer instead of offsetof() since
+ * we don't have the type around.
+ */
+#define SLIST_CONTAINER_(tail, head, next) \
+ (void *)((char *)tail - ((char *)&head->next - (char *)head))
+
+/**
+ * Get the tail of a singly-linked list.
+ *
+ * @param list
+ * The list in question.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * The last item in the list.
+ */
+#define SLIST_TAIL(...) \
+ SLIST_TAIL_(__VA_ARGS__, )
+
+#define SLIST_TAIL_(list, ...) \
+ SLIST_TAIL__((list), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_TAIL__(list, next) \
+ (list->head ? SLIST_CONTAINER_(list->tail, list->head, next) : NULL)
+
+/**
+ * Check if an item is attached to a singly-linked list.
+ *
+ * @param list
+ * The list to check.
+ * @param item
+ * The item to check.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * Whether the item is attached to the list.
+ */
+#define SLIST_ATTACHED(list, ...) \
+ SLIST_ATTACHED_(list, __VA_ARGS__, )
+
+#define SLIST_ATTACHED_(list, item, ...) \
+ SLIST_ATTACHED__((list), (item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_ATTACHED__(list, item, next) \
+ (item->next || list->tail == &item->next)
+
+/**
+ * Insert an item into a singly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param cursor
+ * A pointer to the item to insert after, e.g. &list->head or list->tail.
+ * @param item
+ * The item to insert.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * A cursor for the next item.
+ */
+#define SLIST_INSERT(list, cursor, ...) \
+ SLIST_INSERT_(list, cursor, __VA_ARGS__, )
+
+#define SLIST_INSERT_(list, cursor, item, ...) \
+ SLIST_INSERT__((list), (cursor), (item), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_INSERT__(list, cursor, item, next) \
+ (bfs_assert(!SLIST_ATTACHED__(list, item, next)), \
+ item->next = *cursor, \
+ *cursor = item, \
+ list->tail = item->next ? list->tail : &item->next, \
+ &item->next)
+
+/**
+ * Add an item to the tail of a singly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param item
+ * The item to append.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define SLIST_APPEND(list, ...) \
+ SLIST_APPEND_(list, __VA_ARGS__, )
+
+#define SLIST_APPEND_(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT_(list, (list)->tail, item, __VA_ARGS__))
+
+/**
+ * Add an item to the head of a singly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param item
+ * The item to prepend.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define SLIST_PREPEND(list, ...) \
+ SLIST_PREPEND_(list, __VA_ARGS__, )
+
+#define SLIST_PREPEND_(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__))
+
+/**
+ * Add an entire singly-linked list to the tail of another.
+ *
+ * @param dest
+ * The destination list.
+ * @param src
+ * The source list.
+ */
+#define SLIST_EXTEND(dest, src) \
+ SLIST_EXTEND_((dest), (src))
+
+#define SLIST_EXTEND_(dest, src) \
+ (src->head ? (*dest->tail = src->head, dest->tail = src->tail, SLIST_INIT(src)) : (void)0)
+
+/**
+ * Remove an item from a singly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param cursor
+ * A pointer to the item to remove, either &list->head or &prev->next.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ * @return
+ * The removed item.
+ */
+#define SLIST_REMOVE(list, ...) \
+ SLIST_REMOVE_(list, __VA_ARGS__, )
+
+#define SLIST_REMOVE_(list, cursor, ...) \
+ SLIST_REMOVE__((list), (cursor), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_REMOVE__(list, cursor, next) \
+ (list->tail = (*cursor)->next ? list->tail : cursor, \
+ slist_remove_impl(*cursor, cursor, &(*cursor)->next, sizeof(*cursor)))
+
+// Helper for SLIST_REMOVE()
+static inline void *slist_remove_impl(void *ret, void *cursor, void *next, size_t size) {
+ // ret = *cursor;
+ // *cursor = ret->next;
+ memcpy(cursor, next, size);
+ // ret->next = NULL;
+ memset(next, 0, size);
+ return ret;
+}
+
+/**
+ * Pop the head off a singly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param node (optional)
+ * If specified, use head->node.next rather than head->next.
+ * @return
+ * The popped item, or NULL if the list was empty.
+ */
+#define SLIST_POP(...) \
+ SLIST_POP_(__VA_ARGS__, )
+
+#define SLIST_POP_(list, ...) \
+ SLIST_POP__((list), __VA_ARGS__)
+
+#define SLIST_POP__(list, ...) \
+ (list->head ? SLIST_REMOVE_(list, &list->head, __VA_ARGS__) : NULL)
+
+/**
+ * Loop over the items in a singly-linked list.
+ *
+ * @param type
+ * The list item type.
+ * @param item
+ * The induction variable name.
+ * @param list
+ * The list to iterate.
+ * @param node (optional)
+ * If specified, use head->node.next rather than head->next.
+ */
+#define for_slist(type, item, ...) \
+ for_slist_(type, item, __VA_ARGS__, )
+
+#define for_slist_(type, item, list, ...) \
+ for_slist__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+
+#define for_slist__(type, item, list, next) \
+ for (type *item = list->head, *_next; \
+ item && (SLIST_CHECK_(list), _next = item->next, true); \
+ item = _next)
+
+/**
+ * Initialize a doubly-linked list.
+ *
+ * @param list
+ * The list to initialize.
+ */
+#define LIST_INIT(list) \
+ LIST_INIT_((list))
+
+#define LIST_INIT_(list) \
+ LIST_VOID_(list->head = list->tail = NULL)
+
+/**
+ * LIST_PREV_() => prev
+ * LIST_PREV_(node, ) => node.prev
+ */
+#define LIST_PREV_(...) \
+ LIST_NODE_(prev, __VA_ARGS__)
+
+/**
+ * Initialize a doubly-linked list item.
+ *
+ * @param item
+ * The item to initialize.
+ * @param node (optional)
+ * If specified, use item->node.next rather than item->next.
+ */
+#define LIST_ITEM_INIT(...) \
+ LIST_ITEM_INIT_(__VA_ARGS__, )
+
+#define LIST_ITEM_INIT_(item, ...) \
+ LIST_ITEM_INIT__((item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_ITEM_INIT__(item, prev, next) \
+ LIST_VOID_(item->prev = item->next = NULL)
+
+/**
+ * Type-checking macro for doubly-linked lists.
+ */
+#define LIST_CHECK_(list) \
+ (void)sizeof(list->tail - list->head)
+
+/**
+ * Check if a doubly-linked list is empty.
+ */
+#define LIST_EMPTY(list) \
+ LIST_EMPTY_((list))
+
+#define LIST_EMPTY_(list) \
+ (LIST_CHECK_(list), !list->head)
+
+/**
+ * Add an item to the tail of a doubly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param item
+ * The item to append.
+ * @param node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_APPEND(list, ...) \
+ LIST_INSERT(list, (list)->tail, __VA_ARGS__)
+
+/**
+ * Add an item to the head of a doubly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param item
+ * The item to prepend.
+ * @param node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_PREPEND(list, ...) \
+ LIST_INSERT(list, NULL, __VA_ARGS__)
+
+/**
+ * Check if an item is attached to a doubly-linked list.
+ *
+ * @param list
+ * The list to check.
+ * @param item
+ * The item to check.
+ * @param node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ * @return
+ * Whether the item is attached to the list.
+ */
+#define LIST_ATTACHED(list, ...) \
+ LIST_ATTACHED_(list, __VA_ARGS__, )
+
+#define LIST_ATTACHED_(list, item, ...) \
+ LIST_ATTACHED__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_ATTACHED__(list, item, prev, next) \
+ (item->prev || item->next || list->head == item || list->tail == item)
+
+/**
+ * Insert into a doubly-linked list after the given cursor.
+ *
+ * @param list
+ * The list to modify.
+ * @param cursor
+ * Insert after this element.
+ * @param item
+ * The item to insert.
+ * @param node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_INSERT(list, cursor, ...) \
+ LIST_INSERT_(list, cursor, __VA_ARGS__, )
+
+#define LIST_INSERT_(list, cursor, item, ...) \
+ LIST_INSERT__((list), (cursor), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_INSERT__(list, cursor, item, prev, next) LIST_VOID_( \
+ bfs_assert(!LIST_ATTACHED__(list, item, prev, next)), \
+ item->prev = cursor, \
+ item->next = cursor ? cursor->next : list->head, \
+ *(item->prev ? &item->prev->next : &list->head) = item, \
+ *(item->next ? &item->next->prev : &list->tail) = item)
+
+/**
+ * Remove an item from a doubly-linked list.
+ *
+ * @param list
+ * The list to modify.
+ * @param item
+ * The item to remove.
+ * @param node (optional)
+ * If specified, use item->node.{prev,next} rather than item->{prev,next}.
+ */
+#define LIST_REMOVE(list, ...) \
+ LIST_REMOVE_(list, __VA_ARGS__, )
+
+#define LIST_REMOVE_(list, item, ...) \
+ LIST_REMOVE__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+
+#define LIST_REMOVE__(list, item, prev, next) LIST_VOID_( \
+ *(item->prev ? &item->prev->next : &list->head) = item->next, \
+ *(item->next ? &item->next->prev : &list->tail) = item->prev, \
+ item->prev = item->next = NULL)
+
+/**
+ * Loop over the items in a doubly-linked list.
+ *
+ * @param type
+ * The list item type.
+ * @param item
+ * The induction variable name.
+ * @param list
+ * The list to iterate.
+ * @param node (optional)
+ * If specified, use head->node.next rather than head->next.
+ */
+#define for_list(type, item, ...) \
+ for_list_(type, item, __VA_ARGS__, )
+
+#define for_list_(type, item, list, ...) \
+ for_list__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+
+#define for_list__(type, item, list, next) \
+ for (type *item = list->head, *_next; \
+ item && (LIST_CHECK_(list), _next = item->next, true); \
+ item = _next)
+
+#endif // BFS_LIST_H
diff --git a/src/main.c b/src/main.c
index b8ee6d0..9d8b206 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* - main(): the entry point for bfs(1), a breadth-first version of find(1)
@@ -33,35 +20,43 @@
* - bftw.[ch] (an extended version of nftw(3))
*
* - Utilities:
- * - bfs.h (constants about bfs itself)
+ * - alloc.[ch] (memory allocation)
+ * - atomic.h (atomic operations)
* - bar.[ch] (a terminal status bar)
+ * - bit.h (bit manipulation)
+ * - bfstd.[ch] (standard library wrappers/polyfills)
* - color.[ch] (for pretty terminal colors)
- * - darray.[ch] (a dynamic array library)
+ * - prelude.h (configuration and feature/platform detection)
* - diag.[ch] (formats diagnostic messages)
* - dir.[ch] (a directory API facade)
* - dstring.[ch] (a dynamic string library)
* - fsade.[ch] (a facade over non-standard filesystem features)
+ * - ioq.[ch] (an async I/O queue)
+ * - list.h (linked list macros)
* - mtab.[ch] (parses the system's mount table)
* - pwcache.[ch] (a cache for the user/group tables)
+ * - sanity.h (sanitizer interfaces)
* - stat.[ch] (wraps stat(), or statx() on Linux)
+ * - thread.h (multi-threading)
* - trie.[ch] (a trie set/map implementation)
* - typo.[ch] (fuzzy matching for typos)
- * - util.[ch] (everything else)
* - xregex.[ch] (regular expression support)
* - xspawn.[ch] (spawns processes)
* - xtime.[ch] (date/time handling utilities)
*/
+#include "prelude.h"
+#include "bfstd.h"
#include "ctx.h"
+#include "diag.h"
#include "eval.h"
#include "parse.h"
-#include "util.h"
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
-#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
+#include <time.h>
#include <unistd.h>
/**
@@ -123,15 +118,29 @@ int main(int argc, char *argv[]) {
}
// Use the system locale instead of "C"
- setlocale(LC_ALL, "");
+ int locale_err = 0;
+ if (!setlocale(LC_ALL, "")) {
+ locale_err = errno;
+ }
+
+ // Apply the environment's timezone
+ tzset();
+ // Parse the command line
struct bfs_ctx *ctx = bfs_parse_cmdline(argc, argv);
if (!ctx) {
return EXIT_FAILURE;
}
+ // Warn if setlocale() failed, unless there's no expression to evaluate
+ if (locale_err && ctx->warn && ctx->expr) {
+ bfs_warning(ctx, "Failed to set locale: %s\n\n", xstrerror(locale_err));
+ }
+
+ // Walk the file system tree, evaluating the expression on each file
int ret = bfs_eval(ctx);
+ // Free the parsed command line, and detect any last-minute errors
if (bfs_ctx_free(ctx) != 0 && ret == EXIT_SUCCESS) {
ret = EXIT_FAILURE;
}
diff --git a/src/mtab.c b/src/mtab.c
index adc3f58..7905d14 100644
--- a/src/mtab.c
+++ b/src/mtab.c
@@ -1,68 +1,59 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "mtab.h"
-#include "darray.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "stat.h"
#include "trie.h"
-#include "util.h"
#include <errno.h>
#include <fcntl.h>
-#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
+#if !defined(BFS_USE_MNTENT) && BFS_USE_MNTENT_H
+# define BFS_USE_MNTENT true
+#elif !defined(BFS_USE_MNTINFO) && BSD
+# define BFS_USE_MNTINFO true
+#elif !defined(BFS_USE_MNTTAB) && __SVR4
+# define BFS_USE_MNTTAB true
#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>
+#if BFS_USE_MNTENT
+# include <mntent.h>
+# include <paths.h>
+# include <stdio.h>
+#elif BFS_USE_MNTINFO
+# include <sys/mount.h>
+# include <sys/ucred.h>
+#elif BFS_USE_MNTTAB
+# include <stdio.h>
+# include <sys/mnttab.h>
#endif
/**
* A mount point in the table.
*/
-struct bfs_mtab_entry {
+struct bfs_mount {
/** The path to the mount point. */
char *path;
/** The filesystem type. */
char *type;
+ /** Buffer for the strings. */
+ char buf[];
};
struct bfs_mtab {
- /** The list of mount points. */
- struct bfs_mtab_entry *entries;
+ /** Mount point arena. */
+ struct varena varena;
+
+ /** The array of mount points. */
+ struct bfs_mount **mounts;
+ /** The number of mount points. */
+ size_t nmounts;
+
/** The basenames of every mount point. */
struct trie names;
@@ -75,47 +66,56 @@ struct bfs_mtab {
/**
* Add an entry to the mount table.
*/
+attr(maybe_unused)
static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) {
- struct bfs_mtab_entry entry = {
- .path = strdup(path),
- .type = strdup(type),
- };
-
- if (!entry.path || !entry.type) {
- goto fail_entry;
+ size_t path_size = strlen(path) + 1;
+ size_t type_size = strlen(type) + 1;
+ size_t size = path_size + type_size;
+ struct bfs_mount *mount = varena_alloc(&mtab->varena, size);
+ if (!mount) {
+ return -1;
}
- if (DARRAY_PUSH(&mtab->entries, &entry) != 0) {
- goto fail_entry;
+ struct bfs_mount **ptr = RESERVE(struct bfs_mount *, &mtab->mounts, &mtab->nmounts);
+ if (!ptr) {
+ goto free;
}
+ *ptr = mount;
- if (!trie_insert_str(&mtab->names, xbasename(path))) {
- goto fail;
+ mount->path = mount->buf;
+ memcpy(mount->path, path, path_size);
+
+ mount->type = mount->buf + path_size;
+ memcpy(mount->type, type, type_size);
+
+ const char *name = path + xbaseoff(path);
+ if (!trie_insert_str(&mtab->names, name)) {
+ goto shrink;
}
return 0;
-fail_entry:
- free(entry.type);
- free(entry.path);
-fail:
+shrink:
+ --mtab->nmounts;
+free:
+ varena_free(&mtab->varena, mount, size);
return -1;
}
struct bfs_mtab *bfs_mtab_parse(void) {
- struct bfs_mtab *mtab = malloc(sizeof(*mtab));
+ struct bfs_mtab *mtab = ZALLOC(struct bfs_mtab);
if (!mtab) {
return NULL;
}
- mtab->entries = NULL;
+ VARENA_INIT(&mtab->varena, struct bfs_mount, buf);
+
trie_init(&mtab->names);
trie_init(&mtab->types);
- mtab->types_filled = false;
int error = 0;
-#if BFS_MNTENT
+#if BFS_USE_MNTENT
FILE *file = setmntent(_PATH_MOUNTED, "r");
if (!file) {
@@ -138,7 +138,7 @@ struct bfs_mtab *bfs_mtab_parse(void) {
endmntent(file);
-#elif BFS_MNTINFO
+#elif BFS_USE_MNTINFO
#if __NetBSD__
typedef struct statvfs bfs_statfs;
@@ -160,7 +160,7 @@ struct bfs_mtab *bfs_mtab_parse(void) {
}
}
-#elif BFS_MNTTAB
+#elif BFS_USE_MNTTAB
FILE *file = xfopen(MNTTAB, O_RDONLY | O_CLOEXEC);
if (!file) {
@@ -194,27 +194,89 @@ fail:
return NULL;
}
-static void bfs_mtab_fill_types(struct bfs_mtab *mtab) {
- for (size_t i = 0; i < darray_length(mtab->entries); ++i) {
- struct bfs_mtab_entry *entry = &mtab->entries[i];
+static int bfs_mtab_fill_types(struct bfs_mtab *mtab) {
+ const enum bfs_stat_flags flags = BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC;
+ int ret = -1;
+
+ // It's possible that /path/to/mount was unmounted between bfs_mtab_parse() and bfs_mtab_fill_types().
+ // In that case, the dev_t of /path/to/mount will be the same as /path/to, which should not get its
+ // fstype from the old mount record of /path/to/mount.
+ //
+ // Detect this by comparing the st_dev of the parent (/path/to) and child (/path/to/mount). Only when
+ // they differ can the filesystem type actually change between them. As a minor optimization, we keep
+ // the parent directory open in case multiple mounts have the same parent (e.g. /mnt).
+ char *parent_dir = NULL;
+ int parent_fd = -1;
+ int parent_ret = -1;
+ struct bfs_stat parent_stat;
+
+ for (size_t i = 0; i < mtab->nmounts; ++i) {
+ struct bfs_mount *mount = mtab->mounts[i];
+ const char *path = mount->path;
+ int fd = AT_FDCWD;
+
+ char *dir = xdirname(path);
+ if (!dir) {
+ goto fail;
+ }
+
+ if (parent_dir && strcmp(parent_dir, dir) == 0) {
+ // Same parent
+ free(dir);
+ } else {
+ free(parent_dir);
+ parent_dir = dir;
+
+ if (parent_fd >= 0) {
+ xclose(parent_fd);
+ }
+ parent_fd = open(parent_dir, O_SEARCH | O_CLOEXEC | O_DIRECTORY);
+
+ parent_ret = -1;
+ if (parent_fd >= 0) {
+ parent_ret = bfs_stat(parent_fd, NULL, flags, &parent_stat);
+ }
+ }
+
+ if (parent_fd >= 0) {
+ fd = parent_fd;
+ path += xbaseoff(path);
+ }
struct bfs_stat sb;
- if (bfs_stat(AT_FDCWD, entry->path, BFS_STAT_NOFOLLOW | BFS_STAT_NOSYNC, &sb) != 0) {
+ if (bfs_stat(fd, path, flags, &sb) != 0) {
+ continue;
+ }
+
+ if (parent_ret == 0 && parent_stat.dev == sb.dev && parent_stat.ino != sb.ino) {
+ // Not a mount point any more (or a bind mount, but with the same fstype)
continue;
}
struct trie_leaf *leaf = trie_insert_mem(&mtab->types, &sb.dev, sizeof(sb.dev));
if (leaf) {
- leaf->value = entry->type;
+ leaf->value = mount->type;
+ } else {
+ goto fail;
}
}
mtab->types_filled = true;
+ ret = 0;
+
+fail:
+ if (parent_fd >= 0) {
+ xclose(parent_fd);
+ }
+ free(parent_dir);
+ return ret;
}
const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statbuf) {
if (!mtab->types_filled) {
- bfs_mtab_fill_types((struct bfs_mtab *)mtab);
+ if (bfs_mtab_fill_types((struct bfs_mtab *)mtab) != 0) {
+ return NULL;
+ }
}
const struct trie_leaf *leaf = trie_find_mem(&mtab->types, &statbuf->dev, sizeof(statbuf->dev));
@@ -225,8 +287,7 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb
}
}
-bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path) {
- const char *name = xbasename(path);
+bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name) {
return trie_find_str(&mtab->names, name);
}
@@ -235,11 +296,8 @@ void bfs_mtab_free(struct bfs_mtab *mtab) {
trie_destroy(&mtab->types);
trie_destroy(&mtab->names);
- for (size_t i = 0; i < darray_length(mtab->entries); ++i) {
- free(mtab->entries[i].type);
- free(mtab->entries[i].path);
- }
- darray_free(mtab->entries);
+ free(mtab->mounts);
+ varena_destroy(&mtab->varena);
free(mtab);
}
diff --git a/src/mtab.h b/src/mtab.h
index 807539d..67290c2 100644
--- a/src/mtab.h
+++ b/src/mtab.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
/**
* A facade over platform-specific APIs for enumerating mounted filesystems.
@@ -21,7 +8,7 @@
#ifndef BFS_MTAB_H
#define BFS_MTAB_H
-#include <stdbool.h>
+#include "prelude.h"
struct bfs_stat;
@@ -56,12 +43,12 @@ const char *bfs_fstype(const struct bfs_mtab *mtab, const struct bfs_stat *statb
*
* @param mtab
* The current mount table.
- * @param path
- * The path to check.
+ * @param name
+ * The name of the file to check.
* @return
* Whether the named file could be a mount point.
*/
-bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *path);
+bool bfs_might_be_mount(const struct bfs_mtab *mtab, const char *name);
/**
* Free a mount table.
diff --git a/src/opt.c b/src/opt.c
index f8c0ba3..883d598 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -1,32 +1,18 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* The expression optimizer. Different optimization levels are supported:
*
* -O1: basic logical simplifications, like folding (-true -and -foo) to -foo.
*
- * -O2: dead code elimination and data flow analysis. struct opt_facts is used
+ * -O2: dead code elimination and data flow analysis. struct df_domain is used
* to record data flow facts that are true at various points of evaluation.
- * Specifically, struct opt_facts records the facts that must be true before an
- * expression is evaluated (state->facts), and those that must be true after the
- * expression is evaluated, given that it returns true (state->facts_when_true)
- * or false (state->facts_when_true). Additionally, state->facts_when_impure
- * records the possible data flow facts before any expressions with side effects
- * are evaluated.
+ * Specifically, struct df_domain records the state before an expression is
+ * evaluated (opt->before), and after an expression returns true
+ * (opt->after_true) or false (opt->after_false). Additionally, opt->impure
+ * records the possible state before any expression with side effects is
+ * evaluated.
*
* -O3: expression re-ordering to reduce expected cost. In an expression like
* (-foo -and -bar), if both -foo and -bar are pure (no side effects), they can
@@ -35,41 +21,154 @@
* -bar is likely to return false.
*
* -O4/-Ofast: aggressive optimizations that may affect correctness in corner
- * cases. The main effect is to use facts_when_impure to determine if any side-
- * effects are reachable at all, and skipping the traversal if not.
+ * cases. The main effect is to use opt->impure to determine if any side-
+ * effects are reachable at all, skipping the traversal if not.
*/
+#include "prelude.h"
#include "opt.h"
+#include "bftw.h"
+#include "bit.h"
#include "color.h"
#include "ctx.h"
#include "diag.h"
+#include "dir.h"
#include "eval.h"
+#include "exec.h"
#include "expr.h"
+#include "list.h"
#include "pwcache.h"
-#include "util.h"
-#include <assert.h>
+#include <errno.h>
#include <limits.h>
#include <stdarg.h>
-#include <stdbool.h>
-#include <stdint.h>
#include <stdio.h>
-#include <string.h>
#include <unistd.h>
-static char *fake_and_arg = "-a";
-static char *fake_or_arg = "-o";
-static char *fake_not_arg = "!";
+static char *fake_and_arg = "-and";
+static char *fake_or_arg = "-or";
+static char *fake_not_arg = "-not";
+
+/**
+ * The data flow domain for predicates.
+ */
+enum df_pred {
+ /** The bottom state (unreachable). */
+ PRED_BOTTOM = 0,
+ /** The predicate is known to be false. */
+ PRED_FALSE = 1 << false,
+ /** The predicate is known to be true. */
+ PRED_TRUE = 1 << true,
+ /** The top state (unknown). */
+ PRED_TOP = PRED_FALSE | PRED_TRUE,
+};
+
+/** Make a predicate known. */
+static void constrain_pred(enum df_pred *pred, bool value) {
+ *pred &= 1 << value;
+}
+
+/** Compute the join (union) of two predicates. */
+static void pred_join(enum df_pred *dest, enum df_pred src) {
+ *dest |= src;
+}
+
+/**
+ * Types of predicates we track.
+ */
+enum pred_type {
+ /** -readable */
+ READABLE_PRED,
+ /** -writable */
+ WRITABLE_PRED,
+ /** -executable */
+ EXECUTABLE_PRED,
+ /** -acl */
+ ACL_PRED,
+ /** -capable */
+ CAPABLE_PRED,
+ /** -empty */
+ EMPTY_PRED,
+ /** -hidden */
+ HIDDEN_PRED,
+ /** -nogroup */
+ NOGROUP_PRED,
+ /** -nouser */
+ NOUSER_PRED,
+ /** -sparse */
+ SPARSE_PRED,
+ /** -xattr */
+ XATTR_PRED,
+ /** The number of pred_types. */
+ PRED_TYPES,
+};
+
+/** Get the name of a predicate type. */
+static const char *pred_type_name(enum pred_type type) {
+ switch (type) {
+ case READABLE_PRED:
+ return "-readable";
+ case WRITABLE_PRED:
+ return "-writable";
+ case EXECUTABLE_PRED:
+ return "-executable";
+ case ACL_PRED:
+ return "-acl";
+ case CAPABLE_PRED:
+ return "-capable";
+ case EMPTY_PRED:
+ return "-empty";
+ case HIDDEN_PRED:
+ return "-hidden";
+ case NOGROUP_PRED:
+ return "-nogroup";
+ case NOUSER_PRED:
+ return "-nouser";
+ case SPARSE_PRED:
+ return "-sparse";
+ case XATTR_PRED:
+ return "-xattr";
+
+ case PRED_TYPES:
+ break;
+ }
+
+ bfs_bug("Unknown predicate %d", (int)type);
+ return "???";
+}
/**
* A contrained integer range.
*/
-struct range {
+struct df_range {
/** The (inclusive) minimum value. */
long long min;
/** The (inclusive) maximum value. */
long long max;
};
+/** Initialize an empty range. */
+static void range_init_bottom(struct df_range *range) {
+ range->min = LLONG_MAX;
+ range->max = LLONG_MIN;
+}
+
+/** Check if a range is empty. */
+static bool range_is_bottom(const struct df_range *range) {
+ return range->min > range->max;
+}
+
+/** Initialize a full range. */
+static void range_init_top(struct df_range *range) {
+ // All ranges we currently track are non-negative
+ range->min = 0;
+ range->max = LLONG_MAX;
+}
+
+/** Check for an infinite range. */
+static bool range_is_top(const struct df_range *range) {
+ return range->min == 0 && range->max == LLONG_MAX;
+}
+
/** Compute the minimum of two values. */
static long long min_value(long long a, long long b) {
if (a < b) {
@@ -89,17 +188,17 @@ static long long max_value(long long a, long long b) {
}
/** Constrain the minimum of a range. */
-static void constrain_min(struct range *range, long long value) {
+static void constrain_min(struct df_range *range, long long value) {
range->min = max_value(range->min, value);
}
/** Contrain the maximum of a range. */
-static void constrain_max(struct range *range, long long value) {
+static void constrain_max(struct df_range *range, long long value) {
range->max = min_value(range->max, value);
}
/** Remove a single value from a range. */
-static void range_remove(struct range *range, long long value) {
+static void range_remove(struct df_range *range, long long value) {
if (range->min == value) {
if (range->min == LLONG_MAX) {
range->max = LLONG_MIN;
@@ -118,20 +217,9 @@ static void range_remove(struct range *range, long long value) {
}
/** Compute the union of two ranges. */
-static void range_union(struct range *result, const struct range *lhs, const struct range *rhs) {
- result->min = min_value(lhs->min, rhs->min);
- result->max = max_value(lhs->max, rhs->max);
-}
-
-/** Check if a range contains no values. */
-static bool range_is_impossible(const struct range *range) {
- return range->min > range->max;
-}
-
-/** Set a range to contain no values. */
-static void set_range_impossible(struct range *range) {
- range->min = LLONG_MAX;
- range->max = LLONG_MIN;
+static void range_join(struct df_range *dest, const struct df_range *src) {
+ dest->min = min_value(dest->min, src->min);
+ dest->max = max_value(dest->max, src->max);
}
/**
@@ -154,179 +242,171 @@ enum range_type {
RANGE_TYPES,
};
-/**
- * A possibly-known value of a predicate.
- */
-enum known_pred {
- /** The state is impossible to reach. */
- PRED_IMPOSSIBLE = -2,
- /** The value of the predicate is not known. */
- PRED_UNKNOWN = -1,
- /** The predicate is known to be false. */
- PRED_FALSE = false,
- /** The predicate is known to be true. */
- PRED_TRUE = true,
-};
-
-/** Make a predicate known. */
-static void constrain_pred(enum known_pred *pred, bool value) {
- if (*pred == PRED_UNKNOWN) {
- *pred = value;
- } else if (*pred == !value) {
- *pred = PRED_IMPOSSIBLE;
- }
-}
-
-/** Compute the union of two known predicates. */
-static enum known_pred pred_union(enum known_pred lhs, enum known_pred rhs) {
- if (lhs == PRED_IMPOSSIBLE) {
- return rhs;
- } else if (rhs == PRED_IMPOSSIBLE) {
- return lhs;
- } else if (lhs == rhs) {
- return lhs;
- } else {
- return PRED_UNKNOWN;
+/** Get the name of a range type. */
+static const char *range_type_name(enum range_type type) {
+ switch (type) {
+ case DEPTH_RANGE:
+ return "-depth";
+ case GID_RANGE:
+ return "-gid";
+ case INUM_RANGE:
+ return "-inum";
+ case LINKS_RANGE:
+ return "-links";
+ case SIZE_RANGE:
+ return "-size";
+ case UID_RANGE:
+ return "-uid";
+
+ case RANGE_TYPES:
+ break;
}
+
+ bfs_bug("Unknown range %d", (int)type);
+ return "???";
}
/**
- * Types of predicates we track.
+ * The data flow analysis domain.
*/
-enum pred_type {
- /** -readable */
- READABLE_PRED,
- /** -writable */
- WRITABLE_PRED,
- /** -executable */
- EXECUTABLE_PRED,
- /** -acl */
- ACL_PRED,
- /** -capable */
- CAPABLE_PRED,
- /** -empty */
- EMPTY_PRED,
- /** -hidden */
- HIDDEN_PRED,
- /** -nogroup */
- NOGROUP_PRED,
- /** -nouser */
- NOUSER_PRED,
- /** -sparse */
- SPARSE_PRED,
- /** -xattr */
- XATTR_PRED,
- /** The number of pred_types. */
- PRED_TYPES,
-};
+struct df_domain {
+ /** The predicates we track. */
+ enum df_pred preds[PRED_TYPES];
-/**
- * Data flow facts about an evaluation point.
- */
-struct opt_facts {
/** The value ranges we track. */
- struct range ranges[RANGE_TYPES];
+ struct df_range ranges[RANGE_TYPES];
- /** The predicates we track. */
- enum known_pred preds[PRED_TYPES];
-
- /** Bitmask of possible file types. */
+ /** Bitmask of possible -types. */
unsigned int types;
- /** Bitmask of possible link target types. */
+ /** Bitmask of possible -xtypes. */
unsigned int xtypes;
};
-/** Initialize some data flow facts. */
-static void facts_init(struct opt_facts *facts) {
- for (int i = 0; i < RANGE_TYPES; ++i) {
- struct range *range = &facts->ranges[i];
- range->min = 0; // All ranges we currently track are non-negative
- range->max = LLONG_MAX;
- }
-
+/** Set a data flow value to bottom. */
+static void df_init_bottom(struct df_domain *value) {
for (int i = 0; i < PRED_TYPES; ++i) {
- facts->preds[i] = PRED_UNKNOWN;
+ value->preds[i] = PRED_BOTTOM;
}
- facts->types = ~0;
- facts->xtypes = ~0;
-}
-
-/** Compute the union of two fact sets. */
-static void facts_union(struct opt_facts *result, const struct opt_facts *lhs, const struct opt_facts *rhs) {
for (int i = 0; i < RANGE_TYPES; ++i) {
- range_union(&result->ranges[i], &lhs->ranges[i], &rhs->ranges[i]);
+ range_init_bottom(&value->ranges[i]);
}
- for (int i = 0; i < PRED_TYPES; ++i) {
- result->preds[i] = pred_union(lhs->preds[i], rhs->preds[i]);
- }
-
- result->types = lhs->types | rhs->types;
- result->xtypes = lhs->xtypes | rhs->xtypes;
+ value->types = 0;
+ value->xtypes = 0;
}
/** Determine whether a fact set is impossible. */
-static bool facts_are_impossible(const struct opt_facts *facts) {
+static bool df_is_bottom(const struct df_domain *value) {
for (int i = 0; i < RANGE_TYPES; ++i) {
- if (range_is_impossible(&facts->ranges[i])) {
+ if (range_is_bottom(&value->ranges[i])) {
return true;
}
}
for (int i = 0; i < PRED_TYPES; ++i) {
- if (facts->preds[i] == PRED_IMPOSSIBLE) {
+ if (value->preds[i] == PRED_BOTTOM) {
return true;
}
}
- if (!facts->types || !facts->xtypes) {
+ if (!value->types || !value->xtypes) {
return true;
}
return false;
}
-/** Set some facts to be impossible. */
-static void set_facts_impossible(struct opt_facts *facts) {
+/** Initialize some data flow value. */
+static void df_init_top(struct df_domain *value) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ value->preds[i] = PRED_TOP;
+ }
+
for (int i = 0; i < RANGE_TYPES; ++i) {
- set_range_impossible(&facts->ranges[i]);
+ range_init_top(&value->ranges[i]);
}
+ value->types = ~0;
+ value->xtypes = ~0;
+}
+
+/** Check for the top element. */
+static bool df_is_top(const struct df_domain *value) {
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] != PRED_TOP) {
+ return false;
+ }
+ }
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (!range_is_top(&value->ranges[i])) {
+ return false;
+ }
+ }
+
+ if (value->types != ~0U) {
+ return false;
+ }
+
+ if (value->xtypes != ~0U) {
+ return false;
+ }
+
+ return true;
+}
+
+/** Compute the union of two fact sets. */
+static void df_join(struct df_domain *dest, const struct df_domain *src) {
for (int i = 0; i < PRED_TYPES; ++i) {
- facts->preds[i] = PRED_IMPOSSIBLE;
+ pred_join(&dest->preds[i], src->preds[i]);
}
- facts->types = 0;
- facts->xtypes = 0;
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ range_join(&dest->ranges[i], &src->ranges[i]);
+ }
+
+ dest->types |= src->types;
+ dest->xtypes |= src->xtypes;
}
/**
* Optimizer state.
*/
-struct opt_state {
+struct bfs_opt {
/** The context we're optimizing. */
- const struct bfs_ctx *ctx;
-
- /** Data flow facts before this expression is evaluated. */
- struct opt_facts facts;
- /** Data flow facts after this expression returns true. */
- struct opt_facts facts_when_true;
- /** Data flow facts after this expression returns false. */
- struct opt_facts facts_when_false;
- /** Data flow facts before any side-effecting expressions are evaluated. */
- struct opt_facts *facts_when_impure;
+ struct bfs_ctx *ctx;
+ /** Optimization level (ctx->optlevel). */
+ int level;
+ /** Recursion depth. */
+ int depth;
+
+ /** Whether to produce warnings. */
+ bool warn;
+ /** Whether the result of this expression is ignored. */
+ bool ignore_result;
+
+ /** Data flow state before this expression is evaluated. */
+ struct df_domain before;
+ /** Data flow state after this expression returns true. */
+ struct df_domain after_true;
+ /** Data flow state after this expression returns false. */
+ struct df_domain after_false;
+ /** Data flow state before any side-effecting expressions are evaluated. */
+ struct df_domain *impure;
};
/** Log an optimization. */
-BFS_FORMATTER(3, 4)
-static bool opt_debug(const struct opt_state *state, int level, const char *format, ...) {
- assert(state->ctx->optlevel >= level);
+attr(printf(2, 3))
+static bool opt_debug(struct bfs_opt *opt, const char *format, ...) {
+ if (bfs_debug_prefix(opt->ctx, DEBUG_OPT)) {
+ for (int i = 0; i < opt->depth; ++i) {
+ cfprintf(opt->ctx->cerr, "│ ");
+ }
- if (bfs_debug(state->ctx, DEBUG_OPT, "${cyn}-O%d${rs}: ", level)) {
va_list args;
va_start(args, format);
- cvfprintf(state->ctx->cerr, format, args);
+ cvfprintf(opt->ctx->cerr, format, args);
va_end(args);
return true;
} else {
@@ -334,755 +414,1886 @@ static bool opt_debug(const struct opt_state *state, int level, const char *form
}
}
-/** Warn about an expression. */
-BFS_FORMATTER(3, 4)
-static void opt_warning(const struct opt_state *state, const struct bfs_expr *expr, const char *format, ...) {
- if (bfs_expr_warning(state->ctx, expr)) {
+/** Log a recursive call. */
+attr(printf(2, 3))
+static bool opt_enter(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+ if (depth > 0) {
+ --opt->depth;
+ }
+
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─╮ " : "");
+ if (debug) {
va_list args;
va_start(args, format);
- bfs_warning(state->ctx, format, args);
+ cvfprintf(opt->ctx->cerr, format, args);
va_end(args);
}
+
+ opt->depth = depth + 1;
+ return debug;
}
-/** Extract a child expression, freeing the outer expression. */
-static struct bfs_expr *extract_child_expr(struct bfs_expr *expr, struct bfs_expr **child) {
- struct bfs_expr *ret = *child;
- *child = NULL;
- bfs_expr_free(expr);
- return ret;
+/** Log a recursive return. */
+attr(printf(2, 3))
+static bool opt_leave(struct bfs_opt *opt, const char *format, ...) {
+ bool debug = false;
+ int depth = opt->depth;
+
+ if (format) {
+ if (depth > 1) {
+ opt->depth -= 2;
+ }
+
+ debug = opt_debug(opt, "%s", depth > 1 ? "├─╯ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+ }
+
+ opt->depth = depth - 1;
+ return debug;
}
-/**
- * Negate an expression.
- */
-static struct bfs_expr *negate_expr(struct bfs_expr *rhs, char **argv) {
- if (rhs->eval_fn == eval_not) {
- return extract_child_expr(rhs, &rhs->rhs);
+/** Log a shallow visit. */
+attr(printf(2, 3))
+static bool opt_visit(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+ if (depth > 0) {
+ --opt->depth;
}
- struct bfs_expr *expr = bfs_expr_new(eval_not, 1, argv);
- if (!expr) {
- bfs_expr_free(rhs);
- return NULL;
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─◯ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
}
- if (argv == &fake_not_arg) {
- expr->synthetic = true;
+ opt->depth = depth;
+ return debug;
+}
+
+/** Log the deletion of an expression. */
+attr(printf(2, 3))
+static bool opt_delete(struct bfs_opt *opt, const char *format, ...) {
+ int depth = opt->depth;
+
+ if (depth > 0) {
+ --opt->depth;
}
- expr->lhs = NULL;
- expr->rhs = rhs;
- return expr;
+ bool debug = opt_debug(opt, "%s", depth > 0 ? "├─✘ " : "");
+ if (debug) {
+ va_list args;
+ va_start(args, format);
+ cvfprintf(opt->ctx->cerr, format, args);
+ va_end(args);
+ }
+
+ opt->depth = depth;
+ return debug;
}
-static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr);
-static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr);
-static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr);
+typedef bool dump_fn(struct bfs_opt *opt, const char *format, ...);
-/**
- * Apply De Morgan's laws.
- */
-static struct bfs_expr *de_morgan(const struct opt_state *state, struct bfs_expr *expr, char **argv) {
- bool debug = opt_debug(state, 1, "De Morgan's laws: %pe ", expr);
+/** Print a df_pred. */
+static void pred_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum pred_type type) {
+ dump(opt, "${blu}%s${rs}: ", pred_type_name(type));
- struct bfs_expr *parent = negate_expr(expr, argv);
- if (!parent) {
- return NULL;
+ FILE *file = opt->ctx->cerr->file;
+ switch (value->preds[type]) {
+ case PRED_BOTTOM:
+ fprintf(file, "⊥\n");
+ break;
+ case PRED_TOP:
+ fprintf(file, "⊤\n");
+ break;
+ case PRED_TRUE:
+ fprintf(file, "true\n");
+ break;
+ case PRED_FALSE:
+ fprintf(file, "false\n");
+ break;
}
+}
- bool has_parent = true;
- if (parent->eval_fn != eval_not) {
- expr = parent;
- has_parent = false;
+/** Print a df_range. */
+static void range_dump(dump_fn *dump, struct bfs_opt *opt, const struct df_domain *value, enum range_type type) {
+ dump(opt, "${blu}%s${rs}: ", range_type_name(type));
+
+ FILE *file = opt->ctx->cerr->file;
+ const struct df_range *range = &value->ranges[type];
+ if (range_is_bottom(range)) {
+ fprintf(file, "⊥\n");
+ } else if (range_is_top(range)) {
+ fprintf(file, "⊤\n");
+ } else if (range->min == range->max) {
+ fprintf(file, "%lld\n", range->min);
+ } else {
+ if (range->min == LLONG_MIN) {
+ fprintf(file, "(-∞, ");
+ } else {
+ fprintf(file, "[%lld, ", range->min);
+ }
+ if (range->max == LLONG_MAX) {
+ fprintf(file, "∞)\n");
+ } else {
+ fprintf(file, "%lld]\n", range->max);
+ }
}
+}
- assert(expr->eval_fn == eval_and || expr->eval_fn == eval_or);
- if (expr->eval_fn == eval_and) {
- expr->eval_fn = eval_or;
- expr->argv = &fake_or_arg;
+/** Print a set of types. */
+static void types_dump(dump_fn *dump, struct bfs_opt *opt, const char *name, unsigned int types) {
+ dump(opt, "${blu}%s${rs}: ", name);
+
+ FILE *file = opt->ctx->cerr->file;
+ if (types == 0) {
+ fprintf(file, " ⊥\n");
+ } else if (types == ~0U) {
+ fprintf(file, " ⊤\n");
+ } else if (count_ones(types) < count_ones(~types)) {
+ fprintf(file, " 0x%X\n", types);
} else {
- expr->eval_fn = eval_and;
- expr->argv = &fake_and_arg;
+ fprintf(file, "~0x%X\n", ~types);
}
- expr->synthetic = true;
+}
- expr->lhs = negate_expr(expr->lhs, argv);
- expr->rhs = negate_expr(expr->rhs, argv);
- if (!expr->lhs || !expr->rhs) {
- bfs_expr_free(parent);
- return NULL;
+/** Calculate the number of lines of df_dump() output. */
+static int df_dump_lines(const struct df_domain *value) {
+ int lines = 0;
+
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ lines += value->preds[i] != PRED_TOP;
}
- if (debug) {
- cfprintf(state->ctx->cerr, "<==> %pe\n", parent);
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ lines += !range_is_top(&value->ranges[i]);
}
- if (expr->lhs->eval_fn == eval_not) {
- expr->lhs = optimize_not_expr(state, expr->lhs);
+ lines += value->types != ~0U;
+ lines += value->xtypes != ~0U;
+
+ return lines;
+}
+
+/** Get the right debugging function for a df_dump() line. */
+static dump_fn *df_dump_line(int lines, int *line) {
+ ++*line;
+
+ if (lines == 1) {
+ return opt_visit;
+ } else if (*line == 1) {
+ return opt_enter;
+ } else if (*line == lines) {
+ return opt_leave;
+ } else {
+ return opt_debug;
}
- if (expr->rhs->eval_fn == eval_not) {
- expr->rhs = optimize_not_expr(state, expr->rhs);
+}
+
+/** Print a data flow value. */
+static void df_dump(struct bfs_opt *opt, const char *str, const struct df_domain *value) {
+ if (df_is_bottom(value)) {
+ opt_debug(opt, "%s: ⊥\n", str);
+ return;
+ } else if (df_is_top(value)) {
+ opt_debug(opt, "%s: ⊤\n", str);
+ return;
}
- if (!expr->lhs || !expr->rhs) {
- bfs_expr_free(parent);
- return NULL;
+
+ if (!opt_debug(opt, "%s:\n", str)) {
+ return;
}
- if (expr->eval_fn == eval_and) {
- expr = optimize_and_expr(state, expr);
- } else {
- expr = optimize_or_expr(state, expr);
+ int lines = df_dump_lines(value);
+ int line = 0;
+
+ for (int i = 0; i < PRED_TYPES; ++i) {
+ if (value->preds[i] != PRED_TOP) {
+ pred_dump(df_dump_line(lines, &line), opt, value, i);
+ }
}
- if (has_parent) {
- parent->rhs = expr;
- } else {
- parent = expr;
+
+ for (int i = 0; i < RANGE_TYPES; ++i) {
+ if (!range_is_top(&value->ranges[i])) {
+ range_dump(df_dump_line(lines, &line), opt, value, i);
+ }
}
- if (!expr) {
- bfs_expr_free(parent);
+
+ if (value->types != ~0U) {
+ types_dump(df_dump_line(lines, &line), opt, "-type", value->types);
+ }
+
+ if (value->xtypes != ~0U) {
+ types_dump(df_dump_line(lines, &line), opt, "-xtype", value->xtypes);
+ }
+}
+
+/** Check if an expression is constant. */
+static bool is_const(const struct bfs_expr *expr) {
+ return expr->eval_fn == eval_true || expr->eval_fn == eval_false;
+}
+
+/** Warn about an expression. */
+attr(printf(3, 4))
+static void opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) {
+ if (!opt->warn) {
+ return;
+ }
+
+ if (bfs_expr_is_parent(expr) || is_const(expr)) {
+ return;
+ }
+
+ if (bfs_expr_warning(opt->ctx, expr)) {
+ va_list args;
+ va_start(args, format);
+ bfs_vwarning(opt->ctx, format, args);
+ va_end(args);
+ }
+}
+
+/** Remove and return an expression's children. */
+static void foster_children(struct bfs_expr *expr, struct bfs_exprs *children) {
+ bfs_assert(bfs_expr_is_parent(expr));
+
+ SLIST_INIT(children);
+ SLIST_EXTEND(children, &expr->children);
+
+ expr->persistent_fds = 0;
+ expr->ephemeral_fds = 0;
+ expr->pure = true;
+}
+
+/** Return an expression's only child. */
+static struct bfs_expr *only_child(struct bfs_expr *expr) {
+ bfs_assert(bfs_expr_is_parent(expr));
+ struct bfs_expr *child = bfs_expr_children(expr);
+ bfs_assert(child && !child->next);
+ return child;
+}
+
+/** Foster an expression's only child. */
+static struct bfs_expr *foster_only_child(struct bfs_expr *expr) {
+ struct bfs_expr *child = only_child(expr);
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+ return child;
+}
+
+/** An expression visitor. */
+struct visitor;
+
+/** An expression-visiting function. */
+typedef struct bfs_expr *visit_fn(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor);
+
+/** An entry in a visitor lookup table. */
+struct visitor_table {
+ /** The evaluation function to match on. */
+ bfs_eval_fn *eval_fn;
+ /** The visitor function. */
+ visit_fn *visit;
+};
+
+/** Look up a visitor in a table. */
+static visit_fn *look_up_visitor(const struct bfs_expr *expr, const struct visitor_table table[]) {
+ for (size_t i = 0; table[i].eval_fn; ++i) {
+ if (expr->eval_fn == table[i].eval_fn) {
+ return table[i].visit;
+ }
+ }
+
+ return NULL;
+}
+
+struct visitor {
+ /** The name of this visitor. */
+ const char *name;
+
+ /** A function to call before visiting children. */
+ visit_fn *enter;
+ /** The default visitor. */
+ visit_fn *visit;
+ /** A function to call after visiting children. */
+ visit_fn *leave;
+
+ /** A visitor lookup table. */
+ const struct visitor_table *table;
+};
+
+/** Recursive visitor implementation. */
+static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor);
+
+/** Visit a negation. */
+static struct bfs_expr *visit_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = foster_only_child(expr);
+
+ struct bfs_opt nested = *opt;
+ rhs = visit_deep(&nested, rhs, visitor);
+ if (!rhs) {
return NULL;
}
- if (has_parent) {
- parent = optimize_not_expr(state, parent);
+ opt->after_true = nested.after_false;
+ opt->after_false = nested.after_true;
+
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/** Visit a conjunction. */
+static struct bfs_expr *visit_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ // Base case (-and) == (-true)
+ df_init_bottom(&opt->after_false);
+ struct bfs_opt nested = *opt;
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = false;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
+ }
+
+ df_join(&opt->after_false, &nested.after_false);
+ nested.before = nested.after_true;
+
+ bfs_expr_append(expr, child);
}
- return parent;
+
+ opt->after_true = nested.after_true;
+
+ return expr;
}
-/** Optimize an expression recursively. */
-static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr);
+/** Visit a disjunction. */
+static struct bfs_expr *visit_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
-/**
- * Optimize a negation.
- */
-static struct bfs_expr *optimize_not_expr(const struct opt_state *state, struct bfs_expr *expr) {
- assert(expr->eval_fn == eval_not);
-
- struct bfs_expr *rhs = expr->rhs;
-
- int optlevel = state->ctx->optlevel;
- if (optlevel >= 1) {
- if (rhs == &bfs_true) {
- opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_false);
- bfs_expr_free(expr);
- return &bfs_false;
- } else if (rhs == &bfs_false) {
- opt_debug(state, 1, "constant propagation: %pe <==> %pe\n", expr, &bfs_true);
- bfs_expr_free(expr);
- return &bfs_true;
- } else if (rhs->eval_fn == eval_not) {
- opt_debug(state, 1, "double negation: %pe <==> %pe\n", expr, rhs->rhs);
- return extract_child_expr(expr, &rhs->rhs);
- } else if (bfs_expr_never_returns(rhs)) {
- opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if ((rhs->eval_fn == eval_and || rhs->eval_fn == eval_or)
- && (rhs->lhs->eval_fn == eval_not || rhs->rhs->eval_fn == eval_not)) {
- return de_morgan(state, expr, expr->argv);
+ // Base case (-or) == (-false)
+ df_init_bottom(&opt->after_true);
+ struct bfs_opt nested = *opt;
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = false;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
}
+
+ df_join(&opt->after_true, &nested.after_true);
+ nested.before = nested.after_false;
+
+ bfs_expr_append(expr, child);
}
- expr->pure = rhs->pure;
- expr->always_true = rhs->always_false;
- expr->always_false = rhs->always_true;
- expr->cost = rhs->cost;
- expr->probability = 1.0 - rhs->probability;
+ opt->after_false = nested.after_false;
return expr;
}
-/** Optimize a negation recursively. */
-static struct bfs_expr *optimize_not_expr_recursive(struct opt_state *state, struct bfs_expr *expr) {
- struct opt_state rhs_state = *state;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
+/** Visit a comma expression. */
+static struct bfs_expr *visit_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_opt nested = *opt;
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (SLIST_EMPTY(&children)) {
+ nested.ignore_result = opt->ignore_result;
+ } else {
+ nested.ignore_result = true;
+ }
+
+ child = visit_deep(&nested, child, visitor);
+ if (!child) {
+ return NULL;
+ }
+
+ nested.before = nested.after_true;
+ df_join(&nested.before, &nested.after_false);
+
+ bfs_expr_append(expr, child);
}
- state->facts_when_true = rhs_state.facts_when_false;
- state->facts_when_false = rhs_state.facts_when_true;
+ opt->after_true = nested.after_true;
+ opt->after_false = nested.after_false;
+
+ return expr;
+}
- return optimize_not_expr(state, expr);
+/** Default enter() function. */
+static struct bfs_expr *visit_enter(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_enter(opt, "%pe\n", expr);
+ opt->after_true = opt->before;
+ opt->after_false = opt->before;
+ return expr;
+}
-fail:
- bfs_expr_free(expr);
- return NULL;
+/** Default leave() function. */
+static struct bfs_expr *visit_leave(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_leave(opt, "%pe\n", expr);
+ return expr;
}
-/** Optimize a conjunction. */
-static struct bfs_expr *optimize_and_expr(const struct opt_state *state, struct bfs_expr *expr) {
- assert(expr->eval_fn == eval_and);
-
- struct bfs_expr *lhs = expr->lhs;
- struct bfs_expr *rhs = expr->rhs;
-
- const struct bfs_ctx *ctx = state->ctx;
- int optlevel = ctx->optlevel;
- if (optlevel >= 1) {
- if (lhs == &bfs_true) {
- opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (rhs == &bfs_true) {
- opt_debug(state, 1, "conjunction elimination: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_false) {
- opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs);
- opt_warning(state, expr->rhs, "This expression is unreachable.\n\n");
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_true && rhs == &bfs_false) {
- bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr);
- struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs);
- ret = negate_expr(ret, &fake_not_arg);
- if (debug && ret) {
- cfprintf(ctx->cerr, "%pe\n", ret);
+static struct bfs_expr *visit_deep(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ bool entered = false;
+
+ visit_fn *enter = visitor->enter ? visitor->enter : visit_enter;
+ visit_fn *leave = visitor->leave ? visitor->leave : visit_leave;
+
+ static const struct visitor_table table[] = {
+ {eval_not, visit_not},
+ {eval_and, visit_and},
+ {eval_or, visit_or},
+ {eval_comma, visit_comma},
+ {NULL, NULL},
+ };
+ visit_fn *recursive = look_up_visitor(expr, table);
+ if (recursive) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ entered = true;
+ }
+
+ expr = recursive(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ visit_fn *general = visitor->visit;
+ if (general) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ entered = true;
+ }
+
+ expr = general(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ visit_fn *specific = look_up_visitor(expr, visitor->table);
+ if (specific) {
+ if (!entered) {
+ expr = enter(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
}
- return ret;
- } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_false) {
- opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n");
- return extract_child_expr(expr, &expr->rhs);
- } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) {
- return de_morgan(state, expr, expr->lhs->argv);
+ entered = true;
+ }
+
+ expr = specific(opt, expr, visitor);
+ if (!expr) {
+ return NULL;
}
}
- expr->pure = lhs->pure && rhs->pure;
- expr->always_true = lhs->always_true && rhs->always_true;
- expr->always_false = lhs->always_false || rhs->always_false;
- expr->cost = lhs->cost + lhs->probability*rhs->cost;
- expr->probability = lhs->probability*rhs->probability;
+ if (entered) {
+ expr = leave(opt, expr, visitor);
+ } else {
+ opt_visit(opt, "%pe\n", expr);
+ }
+
+ return expr;
+}
+/** Visit an expression recursively. */
+static struct bfs_expr *visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ opt_enter(opt, "%s()\n", visitor->name);
+ expr = visit_deep(opt, expr, visitor);
+ opt_leave(opt, "\n");
return expr;
}
-/** Optimize a conjunction recursively. */
-static struct bfs_expr *optimize_and_expr_recursive(struct opt_state *state, struct bfs_expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
+/** Visit an expression non-recursively. */
+static struct bfs_expr *visit_shallow(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ visit_fn *general = visitor->visit;
+ if (expr && general) {
+ expr = general(opt, expr, visitor);
}
- struct opt_state rhs_state = *state;
- rhs_state.facts = lhs_state.facts_when_true;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
+ if (!expr) {
+ return NULL;
}
- state->facts_when_true = rhs_state.facts_when_true;
- facts_union(&state->facts_when_false, &lhs_state.facts_when_false, &rhs_state.facts_when_false);
+ visit_fn *specific = look_up_visitor(expr, visitor->table);
+ if (specific) {
+ expr = specific(opt, expr, visitor);
+ }
- return optimize_and_expr(state, expr);
+ return expr;
+}
-fail:
- bfs_expr_free(expr);
- return NULL;
+/** Annotate -{execut,read,writ}able. */
+static struct bfs_expr *annotate_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->probability = 1.0;
+ if (expr->num & R_OK) {
+ expr->probability *= 0.99;
+ }
+ if (expr->num & W_OK) {
+ expr->probability *= 0.8;
+ }
+ if (expr->num & X_OK) {
+ expr->probability *= 0.2;
+ }
+
+ return expr;
}
-/** Optimize a disjunction. */
-static struct bfs_expr *optimize_or_expr(const struct opt_state *state, struct bfs_expr *expr) {
- assert(expr->eval_fn == eval_or);
-
- struct bfs_expr *lhs = expr->lhs;
- struct bfs_expr *rhs = expr->rhs;
-
- const struct bfs_ctx *ctx = state->ctx;
- int optlevel = ctx->optlevel;
- if (optlevel >= 1) {
- if (lhs->always_true) {
- opt_debug(state, 1, "short-circuit: %pe <==> %pe\n", expr, lhs);
- opt_warning(state, expr->rhs, "This expression is unreachable.\n\n");
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs == &bfs_false) {
- opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, rhs);
- return extract_child_expr(expr, &expr->rhs);
- } else if (rhs == &bfs_false) {
- opt_debug(state, 1, "disjunctive syllogism: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (lhs->always_false && rhs == &bfs_true) {
- bool debug = opt_debug(state, 1, "strength reduction: %pe <==> ", expr);
- struct bfs_expr *ret = extract_child_expr(expr, &expr->lhs);
- ret = negate_expr(ret, &fake_not_arg);
- if (debug && ret) {
- cfprintf(ctx->cerr, "%pe\n", ret);
- }
- return ret;
- } else if (optlevel >= 2 && lhs->pure && rhs == &bfs_true) {
- opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n");
- return extract_child_expr(expr, &expr->rhs);
- } else if (lhs->eval_fn == eval_not && rhs->eval_fn == eval_not) {
- return de_morgan(state, expr, expr->lhs->argv);
- }
+/** Annotate -empty. */
+static struct bfs_expr *annotate_empty(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->level >= 4) {
+ // Since -empty attempts to open and read directories, it may
+ // have side effects such as reporting permission errors, and
+ // thus shouldn't be re-ordered without aggressive optimizations
+ expr->pure = true;
}
- 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;
+}
+
+/** 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;
}
-/** Optimize a disjunction recursively. */
-static struct bfs_expr *optimize_or_expr_recursive(struct opt_state *state, struct bfs_expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
+/** Annotate -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;
}
- struct opt_state rhs_state = *state;
- rhs_state.facts = lhs_state.facts_when_false;
- expr->rhs = optimize_expr_recursive(&rhs_state, expr->rhs);
- if (!expr->rhs) {
- goto fail;
+ return expr;
+}
+
+/** Annotate -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;
+ }
+}
- facts_union(&state->facts_when_true, &lhs_state.facts_when_true, &rhs_state.facts_when_true);
- state->facts_when_false = rhs_state.facts_when_false;
+/** Annotate -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;
+}
- return optimize_or_expr(state, 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;
+ }
-fail:
- bfs_expr_free(expr);
- return NULL;
+ estimate_type_probability(expr);
+ return expr;
}
-/** Optimize an expression in an ignored-result context. */
-static struct bfs_expr *ignore_result(const struct opt_state *state, struct bfs_expr *expr) {
- int optlevel = state->ctx->optlevel;
-
- if (optlevel >= 1) {
- while (true) {
- if (expr->eval_fn == eval_not) {
- opt_debug(state, 1, "ignored result: %pe --> %pe\n", expr, expr->rhs);
- opt_warning(state, expr, "The result of this expression is ignored.\n\n");
- expr = extract_child_expr(expr, &expr->rhs);
- } else if (optlevel >= 2
- && (expr->eval_fn == eval_and || expr->eval_fn == eval_or || expr->eval_fn == eval_comma)
- && expr->rhs->pure) {
- opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, expr->lhs);
- opt_warning(state, expr->rhs, "The result of this expression is ignored.\n\n");
- expr = extract_child_expr(expr, &expr->lhs);
- } else {
- break;
- }
+/** Annotate a negation. */
+static struct bfs_expr *annotate_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = only_child(expr);
+ expr->pure = rhs->pure;
+ expr->always_true = rhs->always_false;
+ expr->always_false = rhs->always_true;
+ expr->cost = rhs->cost;
+ expr->probability = 1.0 - rhs->probability;
+ return expr;
+}
+
+/** Annotate a conjunction. */
+static struct bfs_expr *annotate_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->pure = true;
+ expr->always_true = true;
+ expr->always_false = false;
+ expr->cost = 0.0;
+ expr->probability = 1.0;
+
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ expr->pure &= child->pure;
+ expr->always_true &= child->always_true;
+ expr->always_false |= child->always_false;
+ expr->cost += expr->probability * child->cost;
+ expr->probability *= child->probability;
+ }
+
+ return expr;
+}
+
+/** Annotate a disjunction. */
+static struct bfs_expr *annotate_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ expr->pure = true;
+ expr->always_true = false;
+ expr->always_false = true;
+ expr->cost = 0.0;
+
+ float false_prob = 1.0;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ expr->pure &= child->pure;
+ expr->always_true |= child->always_true;
+ expr->always_false &= child->always_false;
+ expr->cost += false_prob * child->cost;
+ false_prob *= (1.0 - child->probability);
+ }
+ expr->probability = 1.0 - false_prob;
+
+ 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 (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ expr->pure &= child->pure;
+ expr->always_true = child->always_true;
+ expr->always_false = child->always_false;
+ expr->cost += child->cost;
+ expr->probability = child->probability;
+ }
+
+ return expr;
+}
+
+/** Annotate an arbitrary expression. */
+static struct bfs_expr *annotate_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ /** Table of pure expressions. */
+ static bfs_eval_fn *const pure[] = {
+ eval_access,
+ eval_acl,
+ eval_capable,
+ eval_depth,
+ eval_false,
+ eval_flags,
+ eval_fstype,
+ eval_gid,
+ eval_hidden,
+ eval_inum,
+ eval_links,
+ eval_lname,
+ eval_name,
+ eval_newer,
+ eval_nogroup,
+ eval_nouser,
+ eval_path,
+ eval_perm,
+ eval_regex,
+ eval_samefile,
+ eval_size,
+ eval_sparse,
+ eval_time,
+ eval_true,
+ eval_type,
+ eval_uid,
+ eval_used,
+ eval_xattr,
+ eval_xattrname,
+ };
+
+ expr->pure = false;
+ for (size_t i = 0; i < countof(pure); ++i) {
+ if (expr->eval_fn == pure[i]) {
+ expr->pure = true;
+ break;
}
+ }
- if (optlevel >= 2 && expr->pure && expr != &bfs_false) {
- opt_debug(state, 2, "ignored result: %pe --> %pe\n", expr, &bfs_false);
- opt_warning(state, expr, "The result of this expression is ignored.\n\n");
- bfs_expr_free(expr);
- expr = &bfs_false;
+ /** Table of always-true expressions. */
+ static bfs_eval_fn *const always_true[] = {
+ eval_fls,
+ eval_fprint,
+ eval_fprint0,
+ eval_fprintf,
+ eval_fprintx,
+ eval_limit,
+ eval_prune,
+ eval_true,
+ // Non-returning
+ eval_exit,
+ eval_quit,
+ };
+
+ expr->always_true = false;
+ for (size_t i = 0; i < countof(always_true); ++i) {
+ if (expr->eval_fn == always_true[i]) {
+ expr->always_true = true;
+ break;
+ }
+ }
+
+ /** Table of always-false expressions. */
+ static bfs_eval_fn *const always_false[] = {
+ eval_false,
+ // Non-returning
+ eval_exit,
+ eval_quit,
+ };
+
+ expr->always_false = false;
+ for (size_t i = 0; i < countof(always_false); ++i) {
+ if (expr->eval_fn == always_false[i]) {
+ expr->always_false = true;
+ break;
+ }
+ }
+
+ /** Table of stat-calling primaries. */
+ static bfs_eval_fn *const calls_stat[] = {
+ eval_empty,
+ eval_flags,
+ eval_fls,
+ eval_fprintf,
+ eval_fstype,
+ eval_gid,
+ eval_inum,
+ eval_links,
+ eval_newer,
+ eval_nogroup,
+ eval_nouser,
+ eval_perm,
+ eval_samefile,
+ eval_size,
+ eval_sparse,
+ eval_time,
+ eval_uid,
+ eval_used,
+ eval_xattr,
+ eval_xattrname,
+ };
+
+ expr->calls_stat = false;
+ for (size_t i = 0; i < countof(calls_stat); ++i) {
+ if (expr->eval_fn == calls_stat[i]) {
+ expr->calls_stat = true;
+ break;
+ }
+ }
+
+#define FAST_COST 40.0
+#define FNMATCH_COST 400.0
+#define STAT_COST 1000.0
+#define PRINT_COST 20000.0
+
+ /** Table of expression costs. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ float cost;
+ } costs[] = {
+ {eval_access, STAT_COST},
+ {eval_acl, STAT_COST},
+ {eval_capable, STAT_COST},
+ {eval_empty, 2 * STAT_COST}, // readdir() is worse than stat()
+ {eval_flags, STAT_COST},
+ {eval_fls, PRINT_COST},
+ {eval_fprint, PRINT_COST},
+ {eval_fprint0, PRINT_COST},
+ {eval_fprintf, PRINT_COST},
+ {eval_fprintx, PRINT_COST},
+ {eval_fstype, STAT_COST},
+ {eval_gid, STAT_COST},
+ {eval_inum, STAT_COST},
+ {eval_links, STAT_COST},
+ {eval_lname, FNMATCH_COST},
+ {eval_name, FNMATCH_COST},
+ {eval_newer, STAT_COST},
+ {eval_nogroup, STAT_COST},
+ {eval_nouser, STAT_COST},
+ {eval_path, FNMATCH_COST},
+ {eval_perm, STAT_COST},
+ {eval_samefile, STAT_COST},
+ {eval_size, STAT_COST},
+ {eval_sparse, STAT_COST},
+ {eval_time, STAT_COST},
+ {eval_uid, STAT_COST},
+ {eval_used, STAT_COST},
+ {eval_xattr, STAT_COST},
+ {eval_xattrname, STAT_COST},
+ };
+
+ expr->cost = FAST_COST;
+ for (size_t i = 0; i < countof(costs); ++i) {
+ if (expr->eval_fn == costs[i].eval_fn) {
+ expr->cost = costs[i].cost;
+ break;
+ }
+ }
+
+ /** Table of expression probabilities. */
+ static const struct {
+ /** The evaluation function with this cost. */
+ bfs_eval_fn *eval_fn;
+ /** The matching probability. */
+ float probability;
+ } probs[] = {
+ {eval_acl, 0.00002},
+ {eval_capable, 0.000002},
+ {eval_empty, 0.01},
+ {eval_false, 0.0},
+ {eval_hidden, 0.01},
+ {eval_nogroup, 0.01},
+ {eval_nouser, 0.01},
+ {eval_samefile, 0.01},
+ {eval_true, 1.0},
+ {eval_xattr, 0.01},
+ {eval_xattrname, 0.01},
+ };
+
+ expr->probability = 0.5;
+ for (size_t i = 0; i < countof(probs); ++i) {
+ if (expr->eval_fn == probs[i].eval_fn) {
+ expr->probability = probs[i].probability;
+ break;
}
}
return expr;
}
-/** Optimize a comma expression. */
-static struct bfs_expr *optimize_comma_expr(const struct opt_state *state, struct bfs_expr *expr) {
- assert(expr->eval_fn == eval_comma);
-
- struct bfs_expr *lhs = expr->lhs;
- struct bfs_expr *rhs = expr->rhs;
-
- int optlevel = state->ctx->optlevel;
- if (optlevel >= 1) {
- lhs = expr->lhs = ignore_result(state, lhs);
-
- if (bfs_expr_never_returns(lhs)) {
- opt_debug(state, 1, "reachability: %pe <==> %pe\n", expr, lhs);
- opt_warning(state, expr->rhs, "This expression is unreachable.\n\n");
- return extract_child_expr(expr, &expr->lhs);
- } else if ((lhs->always_true && rhs == &bfs_true)
- || (lhs->always_false && rhs == &bfs_false)) {
- opt_debug(state, 1, "redundancy elimination: %pe <==> %pe\n", expr, lhs);
- return extract_child_expr(expr, &expr->lhs);
- } else if (optlevel >= 2 && lhs->pure) {
- opt_debug(state, 2, "purity: %pe <==> %pe\n", expr, rhs);
- opt_warning(state, expr->lhs, "The result of this expression is ignored.\n\n");
- return extract_child_expr(expr, &expr->rhs);
+/**
+ * Annotating visitor.
+ */
+static const struct visitor annotate = {
+ .name = "annotate",
+ .visit = annotate_visit,
+ .table = (const struct visitor_table[]) {
+ {eval_access, annotate_access},
+ {eval_empty, annotate_empty},
+ {eval_exec, annotate_exec},
+ {eval_fprint, annotate_fprint},
+ {eval_lname, annotate_fnmatch},
+ {eval_name, annotate_fnmatch},
+ {eval_path, annotate_fnmatch},
+ {eval_type, annotate_type},
+ {eval_xtype, annotate_xtype},
+
+ {eval_not, annotate_not},
+ {eval_and, annotate_and},
+ {eval_or, annotate_or},
+ {eval_comma, annotate_comma},
+
+ {NULL, NULL},
+ },
+};
+
+/** Create a constant expression. */
+static struct bfs_expr *opt_const(struct bfs_opt *opt, bool value) {
+ static bfs_eval_fn *const fns[] = {eval_false, eval_true};
+ static char *fake_args[] = {"-false", "-true"};
+
+ struct bfs_expr *expr = bfs_expr_new(opt->ctx, fns[value], 1, &fake_args[value]);
+ return visit_shallow(opt, expr, &annotate);
+}
+
+/** Negate an expression, keeping it canonical. */
+static struct bfs_expr *negate_expr(struct bfs_opt *opt, struct bfs_expr *expr, char **argv) {
+ if (expr->eval_fn == eval_not) {
+ return only_child(expr);
+ } else if (expr->eval_fn == eval_true) {
+ return opt_const(opt, false);
+ } else if (expr->eval_fn == eval_false) {
+ return opt_const(opt, true);
+ }
+
+ struct bfs_expr *ret = bfs_expr_new(opt->ctx, eval_not, 1, argv);
+ if (!ret) {
+ return NULL;
+ }
+
+ bfs_expr_append(ret, expr);
+ return visit_shallow(opt, ret, &annotate);
+}
+
+/** Sink negations into a conjunction/disjunction using De Morgan's laws. */
+static struct bfs_expr *sink_not_andor(struct bfs_opt *opt, struct bfs_expr *expr) {
+ opt_debug(opt, "De Morgan's laws\n");
+
+ char **argv = expr->argv;
+ expr = only_child(expr);
+ opt_enter(opt, "%pe\n", expr);
+
+ if (expr->eval_fn == eval_and) {
+ expr->eval_fn = eval_or;
+ expr->argv = &fake_or_arg;
+ } else {
+ bfs_assert(expr->eval_fn == eval_or);
+ expr->eval_fn = eval_and;
+ expr->argv = &fake_and_arg;
+ }
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_expr *child;
+ while ((child = SLIST_POP(&children))) {
+ opt_enter(opt, "%pe\n", child);
+
+ child = negate_expr(opt, child, argv);
+ if (!child) {
+ return NULL;
}
+
+ opt_leave(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
}
- expr->pure = lhs->pure && rhs->pure;
- expr->always_true = bfs_expr_never_returns(lhs) || rhs->always_true;
- expr->always_false = bfs_expr_never_returns(lhs) || rhs->always_false;
- expr->cost = lhs->cost + rhs->cost;
- expr->probability = rhs->probability;
+ opt_leave(opt, "%pe\n", expr);
+ return visit_shallow(opt, expr, &annotate);
+}
- return expr;
+/** Sink a negation into a comma expression. */
+static struct bfs_expr *sink_not_comma(struct bfs_opt *opt, struct bfs_expr *expr) {
+ bfs_assert(expr->eval_fn == eval_comma);
+
+ opt_enter(opt, "%pe\n", expr);
+
+ char **argv = expr->argv;
+ expr = only_child(expr);
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_expr *child;
+ while ((child = SLIST_POP(&children))) {
+ if (SLIST_EMPTY(&children)) {
+ opt_enter(opt, "%pe\n", child);
+ opt_debug(opt, "sink\n");
+
+ child = negate_expr(opt, child, argv);
+ if (!child) {
+ return NULL;
+ }
+
+ opt_leave(opt, "%pe\n", child);
+ } else {
+ opt_visit(opt, "%pe\n", child);
+ }
+
+ bfs_expr_append(expr, child);
+ }
+
+ opt_leave(opt, "%pe\n", expr);
+ return visit_shallow(opt, expr, &annotate);
}
-/** Optimize a comma expression recursively. */
-static struct bfs_expr *optimize_comma_expr_recursive(struct opt_state *state, struct bfs_expr *expr) {
- struct opt_state lhs_state = *state;
- expr->lhs = optimize_expr_recursive(&lhs_state, expr->lhs);
- if (!expr->lhs) {
- goto fail;
+/** Canonicalize a negation. */
+static struct bfs_expr *canonicalize_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *rhs = only_child(expr);
+
+ if (rhs->eval_fn == eval_not) {
+ opt_debug(opt, "double negation\n");
+ rhs = only_child(expr);
+ return only_child(rhs);
+ } else if (rhs->eval_fn == eval_and || rhs->eval_fn == eval_or) {
+ return sink_not_andor(opt, expr);
+ } else if (rhs->eval_fn == eval_comma) {
+ return sink_not_comma(opt, expr);
+ } else if (is_const(rhs)) {
+ opt_debug(opt, "constant propagation\n");
+ return opt_const(opt, rhs->eval_fn == eval_false);
+ } else {
+ return expr;
}
+}
+
+/** Canonicalize an associative operator. */
+static struct bfs_expr *canonicalize_assoc(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ struct bfs_exprs flat;
+ SLIST_INIT(&flat);
- struct 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;
+ struct bfs_expr *child;
+ while ((child = SLIST_POP(&children))) {
+ if (child->eval_fn == expr->eval_fn) {
+ struct bfs_expr *head = SLIST_HEAD(&child->children);
+ struct bfs_expr *tail = SLIST_TAIL(&child->children);
+
+ if (!head) {
+ opt_delete(opt, "%pe [empty]\n", child);
+ } else {
+ opt_enter(opt, "%pe\n", child);
+ opt_debug(opt, "associativity\n");
+ if (head == tail) {
+ opt_leave(opt, "%pe\n", head);
+ } else if (head->next == tail) {
+ opt_leave(opt, "%pe %pe\n", head, tail);
+ } else {
+ opt_leave(opt, "%pe ... %pe\n", head, tail);
+ }
+ }
+
+ SLIST_EXTEND(&flat, &child->children);
+ } else {
+ opt_visit(opt, "%pe\n", child);
+ SLIST_APPEND(&flat, child);
+ }
}
- return optimize_comma_expr(state, expr);
+ bfs_expr_extend(expr, &flat);
-fail:
- bfs_expr_free(expr);
- return NULL;
+ return visit_shallow(opt, expr, &annotate);
}
-/** Infer data flow facts about a predicate. */
-static void infer_pred_facts(struct opt_state *state, enum pred_type pred) {
- constrain_pred(&state->facts_when_true.preds[pred], true);
- constrain_pred(&state->facts_when_false.preds[pred], false);
+/**
+ * Canonicalizing visitor.
+ */
+static const struct visitor canonicalize = {
+ .name = "canonicalize",
+ .table = (const struct visitor_table[]) {
+ {eval_not, canonicalize_not},
+ {eval_and, canonicalize_assoc},
+ {eval_or, canonicalize_assoc},
+ {eval_comma, canonicalize_assoc},
+ {NULL, NULL},
+ },
+};
+
+/** Calculate the cost of an ordered pair of expressions. */
+static float expr_cost(const struct bfs_expr *parent, const struct bfs_expr *lhs, const struct bfs_expr *rhs) {
+ // https://cs.stackexchange.com/a/66921/21004
+ float prob = lhs->probability;
+ if (parent->eval_fn == eval_or) {
+ prob = 1.0 - prob;
+ }
+ return lhs->cost + prob * rhs->cost;
}
-/** Infer data flow facts about an -{execut,read,writ}able expression. */
-static void infer_access_facts(struct opt_state *state, const struct bfs_expr *expr) {
- if (expr->num & R_OK) {
- infer_pred_facts(state, READABLE_PRED);
+/** Sort a block of expressions. */
+static void sort_exprs(struct bfs_opt *opt, struct bfs_expr *parent, struct bfs_exprs *exprs) {
+ if (!exprs->head || !exprs->head->next) {
+ return;
}
- if (expr->num & W_OK) {
- infer_pred_facts(state, WRITABLE_PRED);
+
+ struct bfs_exprs left, right;
+ SLIST_INIT(&left);
+ SLIST_INIT(&right);
+
+ // Split
+ for (struct bfs_expr *hare = exprs->head; hare && (hare = hare->next); hare = hare->next) {
+ struct bfs_expr *tortoise = SLIST_POP(exprs);
+ SLIST_APPEND(&left, tortoise);
}
- if (expr->num & X_OK) {
- infer_pred_facts(state, EXECUTABLE_PRED);
+ SLIST_EXTEND(&right, exprs);
+
+ // Recurse
+ sort_exprs(opt, parent, &left);
+ sort_exprs(opt, parent, &right);
+
+ // Merge
+ while (!SLIST_EMPTY(&left) && !SLIST_EMPTY(&right)) {
+ struct bfs_expr *lhs = left.head;
+ struct bfs_expr *rhs = right.head;
+
+ float cost = expr_cost(parent, lhs, rhs);
+ float swapped = expr_cost(parent, rhs, lhs);
+
+ if (cost <= swapped) {
+ SLIST_POP(&left);
+ SLIST_APPEND(exprs, lhs);
+ } else {
+ opt_enter(opt, "%pe %pe [${ylw}%g${rs}]\n", lhs, rhs, cost);
+ SLIST_POP(&right);
+ SLIST_APPEND(exprs, rhs);
+ opt_leave(opt, "%pe %pe [${ylw}%g${rs}]\n", rhs, lhs, swapped);
+ }
+ }
+ SLIST_EXTEND(exprs, &left);
+ SLIST_EXTEND(exprs, &right);
+}
+
+/** Reorder children to reduce cost. */
+static struct bfs_expr *reorder_andor(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ // Split into blocks of consecutive pure/impure expressions, and sort
+ // the pure blocks
+ struct bfs_exprs pure;
+ SLIST_INIT(&pure);
+
+ struct bfs_expr *child;
+ while ((child = SLIST_POP(&children))) {
+ if (child->pure) {
+ SLIST_APPEND(&pure, child);
+ } else {
+ sort_exprs(opt, expr, &pure);
+ bfs_expr_extend(expr, &pure);
+ bfs_expr_append(expr, child);
+ }
}
+ sort_exprs(opt, expr, &pure);
+ bfs_expr_extend(expr, &pure);
+
+ return visit_shallow(opt, expr, &annotate);
}
-/** Infer data flow facts about an icmp-style ([+-]N) expression. */
-static void infer_icmp_facts(struct opt_state *state, const struct bfs_expr *expr, enum range_type type) {
- struct range *range_when_true = &state->facts_when_true.ranges[type];
- struct range *range_when_false = &state->facts_when_false.ranges[type];
+/**
+ * Reordering visitor.
+ */
+static const struct visitor reorder = {
+ .name = "reorder",
+ .table = (const struct visitor_table[]) {
+ {eval_and, reorder_andor},
+ {eval_or, reorder_andor},
+ {NULL, NULL},
+ },
+};
+
+/** Transfer function for simple predicates. */
+static void data_flow_pred(struct bfs_opt *opt, enum pred_type pred, bool value) {
+ constrain_pred(&opt->after_true.preds[pred], value);
+ constrain_pred(&opt->after_false.preds[pred], !value);
+}
+
+/** Transfer function for icmp-style ([+-]N) expressions. */
+static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enum range_type type) {
+ struct df_range *true_range = &opt->after_true.ranges[type];
+ struct df_range *false_range = &opt->after_false.ranges[type];
long long value = expr->num;
switch (expr->int_cmp) {
case BFS_INT_EQUAL:
- constrain_min(range_when_true, value);
- constrain_max(range_when_true, value);
- range_remove(range_when_false, value);
+ constrain_min(true_range, value);
+ constrain_max(true_range, value);
+ range_remove(false_range, value);
break;
case BFS_INT_LESS:
- constrain_min(range_when_false, value);
- constrain_max(range_when_true, value);
- range_remove(range_when_true, value);
+ constrain_min(false_range, value);
+ constrain_max(true_range, value);
+ range_remove(true_range, value);
break;
case BFS_INT_GREATER:
- constrain_max(range_when_false, value);
- constrain_min(range_when_true, value);
- range_remove(range_when_true, value);
+ constrain_max(false_range, value);
+ constrain_min(true_range, value);
+ range_remove(true_range, value);
break;
}
}
-/** Infer data flow facts about a -gid expression. */
-static void infer_gid_facts(struct opt_state *state, const struct bfs_expr *expr) {
- infer_icmp_facts(state, expr, GID_RANGE);
+/** Transfer function for -{execut,read,writ}able. */
+static struct bfs_expr *data_flow_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (expr->num & R_OK) {
+ data_flow_pred(opt, READABLE_PRED, true);
+ }
+ if (expr->num & W_OK) {
+ data_flow_pred(opt, WRITABLE_PRED, true);
+ }
+ if (expr->num & X_OK) {
+ data_flow_pred(opt, EXECUTABLE_PRED, true);
+ }
+
+ return expr;
+}
- 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) {
+/** Transfer function for -gid. */
+static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[GID_RANGE];
+ if (range->min == range->max) {
gid_t gid = range->min;
- bool nogroup = !bfs_getgrgid(groups, gid);
- constrain_pred(&state->facts_when_true.preds[NOGROUP_PRED], nogroup);
+ bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid);
+ if (errno == 0) {
+ data_flow_pred(opt, NOGROUP_PRED, nogroup);
+ }
}
+
+ return expr;
}
-/** Infer data flow facts about a -uid expression. */
-static void infer_uid_facts(struct opt_state *state, const struct bfs_expr *expr) {
- infer_icmp_facts(state, expr, UID_RANGE);
+/** Transfer function for -inum. */
+static struct bfs_expr *data_flow_inum(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[INUM_RANGE];
+ if (range->min == range->max) {
+ expr->probability = 0.01;
+ } else {
+ expr->probability = 0.5;
+ }
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- struct range *range = &state->facts_when_true.ranges[UID_RANGE];
- if (users && range->min == range->max) {
- uid_t uid = range->min;
- bool nouser = !bfs_getpwuid(users, uid);
- constrain_pred(&state->facts_when_true.preds[NOUSER_PRED], nouser);
- }
-}
-
-/** Infer data flow facts about a -samefile expression. */
-static void infer_samefile_facts(struct opt_state *state, const struct bfs_expr *expr) {
- struct range *range_when_true = &state->facts_when_true.ranges[INUM_RANGE];
- constrain_min(range_when_true, expr->ino);
- constrain_max(range_when_true, expr->ino);
-}
-
-/** Infer data flow facts about a -type expression. */
-static void infer_type_facts(struct opt_state *state, const struct bfs_expr *expr) {
- state->facts_when_true.types &= expr->num;
- state->facts_when_false.types &= ~expr->num;
-}
-
-/** Infer data flow facts about an -xtype expression. */
-static void infer_xtype_facts(struct opt_state *state, const struct bfs_expr *expr) {
- state->facts_when_true.xtypes &= expr->num;
- state->facts_when_false.xtypes &= ~expr->num;
-}
-
-static struct bfs_expr *optimize_expr_recursive(struct opt_state *state, struct bfs_expr *expr) {
- int optlevel = state->ctx->optlevel;
-
- state->facts_when_true = state->facts;
- state->facts_when_false = state->facts;
-
- if (optlevel >= 2 && facts_are_impossible(&state->facts)) {
- opt_debug(state, 2, "reachability: %pe --> %pe\n", expr, &bfs_false);
- opt_warning(state, expr, "This expression is unreachable.\n\n");
- bfs_expr_free(expr);
- expr = &bfs_false;
- goto done;
- }
-
- if (!bfs_expr_has_children(expr) && !expr->pure) {
- facts_union(state->facts_when_impure, state->facts_when_impure, &state->facts);
- }
-
- if (expr->eval_fn == eval_access) {
- infer_access_facts(state, expr);
- } else if (expr->eval_fn == eval_acl) {
- infer_pred_facts(state, ACL_PRED);
- } else if (expr->eval_fn == eval_capable) {
- infer_pred_facts(state, CAPABLE_PRED);
- } else if (expr->eval_fn == eval_depth) {
- infer_icmp_facts(state, expr, DEPTH_RANGE);
- } else if (expr->eval_fn == eval_empty) {
- infer_pred_facts(state, EMPTY_PRED);
- } else if (expr->eval_fn == eval_gid) {
- infer_gid_facts(state, expr);
- } else if (expr->eval_fn == eval_hidden) {
- infer_pred_facts(state, HIDDEN_PRED);
- } else if (expr->eval_fn == eval_inum) {
- infer_icmp_facts(state, expr, INUM_RANGE);
- } else if (expr->eval_fn == eval_links) {
- infer_icmp_facts(state, expr, LINKS_RANGE);
- } else if (expr->eval_fn == eval_nogroup) {
- infer_pred_facts(state, NOGROUP_PRED);
- } else if (expr->eval_fn == eval_nouser) {
- infer_pred_facts(state, NOUSER_PRED);
- } else if (expr->eval_fn == eval_samefile) {
- infer_samefile_facts(state, expr);
- } else if (expr->eval_fn == eval_size) {
- infer_icmp_facts(state, expr, SIZE_RANGE);
- } else if (expr->eval_fn == eval_sparse) {
- infer_pred_facts(state, SPARSE_PRED);
- } else if (expr->eval_fn == eval_type) {
- infer_type_facts(state, expr);
- } else if (expr->eval_fn == eval_uid) {
- infer_uid_facts(state, expr);
- } else if (expr->eval_fn == eval_xattr) {
- infer_pred_facts(state, XATTR_PRED);
- } else if (expr->eval_fn == eval_xtype) {
- infer_xtype_facts(state, expr);
- } else if (expr->eval_fn == eval_not) {
- expr = optimize_not_expr_recursive(state, expr);
- } else if (expr->eval_fn == eval_and) {
- expr = optimize_and_expr_recursive(state, expr);
- } else if (expr->eval_fn == eval_or) {
- expr = optimize_or_expr_recursive(state, expr);
- } else if (expr->eval_fn == eval_comma) {
- expr = optimize_comma_expr_recursive(state, expr);
+ 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;
}
- if (!expr) {
- goto done;
+ return expr;
+}
+
+/** Transfer function for -samefile. */
+static struct bfs_expr *data_flow_samefile(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *true_range = &opt->after_true.ranges[INUM_RANGE];
+ constrain_min(true_range, expr->ino);
+ constrain_max(true_range, expr->ino);
+
+ struct df_range *false_range = &opt->after_false.ranges[INUM_RANGE];
+ range_remove(false_range, expr->ino);
+
+ return expr;
+}
+
+/** Transfer function for -size. */
+static struct bfs_expr *data_flow_size(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct df_range *range = &opt->after_true.ranges[SIZE_RANGE];
+ if (range->min == range->max) {
+ expr->probability = 0.01;
+ } else {
+ expr->probability = 0.5;
}
- if (bfs_expr_has_children(expr)) {
- struct bfs_expr *lhs = expr->lhs;
- struct bfs_expr *rhs = expr->rhs;
- if (rhs) {
- expr->persistent_fds = rhs->persistent_fds;
- expr->ephemeral_fds = rhs->ephemeral_fds;
- }
- if (lhs) {
- expr->persistent_fds += lhs->persistent_fds;
- if (lhs->ephemeral_fds > expr->ephemeral_fds) {
- expr->ephemeral_fds = lhs->ephemeral_fds;
- }
+ 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) {
+ data_flow_pred(opt, 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) {
- set_facts_impossible(&state->facts_when_false);
+ expr->probability = 1.0;
+ df_init_bottom(&opt->after_false);
}
+
if (expr->always_false) {
- set_facts_impossible(&state->facts_when_true);
+ expr->probability = 0.0;
+ df_init_bottom(&opt->after_true);
}
- if (optlevel < 2 || expr == &bfs_true || expr == &bfs_false) {
- goto done;
+ df_dump(opt, "after true", &opt->after_true);
+ df_dump(opt, "after false", &opt->after_false);
+
+ if (df_is_bottom(&opt->after_false)) {
+ if (!expr->pure) {
+ expr->always_true = true;
+ expr->probability = 0.0;
+ } else if (expr->eval_fn != eval_true) {
+ opt_warning(opt, expr, "This expression is always true.\n\n");
+ opt_debug(opt, "pure, always true\n");
+ expr = opt_const(opt, true);
+ if (!expr) {
+ return NULL;
+ }
+ }
}
- if (facts_are_impossible(&state->facts_when_true)) {
- if (expr->pure) {
- opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_false);
- opt_warning(state, expr, "This expression is always false.\n\n");
- bfs_expr_free(expr);
- expr = &bfs_false;
- } else {
+ if (df_is_bottom(&opt->after_true)) {
+ if (!expr->pure) {
expr->always_false = true;
expr->probability = 0.0;
- }
- } else if (facts_are_impossible(&state->facts_when_false)) {
- if (expr->pure) {
- opt_debug(state, 2, "data flow: %pe --> %pe\n", expr, &bfs_true);
- opt_warning(state, expr, "This expression is always true.\n\n");
- bfs_expr_free(expr);
- expr = &bfs_true;
- } else {
- expr->always_true = true;
- expr->probability = 1.0;
+ } else if (expr->eval_fn != eval_false) {
+ opt_warning(opt, expr, "This expression is always false.\n\n");
+ opt_debug(opt, "pure, always false\n");
+ expr = opt_const(opt, false);
+ if (!expr) {
+ return NULL;
+ }
}
}
-done:
- return expr;
+ return visit_leave(opt, expr, visitor);
}
-/** Swap the children of a binary expression if it would reduce the cost. */
-static bool reorder_expr(const struct opt_state *state, struct bfs_expr *expr, float swapped_cost) {
- if (swapped_cost < expr->cost) {
- bool debug = opt_debug(state, 3, "cost: %pe <==> ", expr);
- struct bfs_expr *lhs = expr->lhs;
- expr->lhs = expr->rhs;
- expr->rhs = lhs;
- if (debug) {
- cfprintf(state->ctx->cerr, "%pe (~${ylw}%g${rs} --> ~${ylw}%g${rs})\n", expr, expr->cost, swapped_cost);
+/** Data flow visitor function. */
+static struct bfs_expr *data_flow_visit(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->ignore_result && expr->pure) {
+ opt_debug(opt, "ignored result\n");
+ opt_warning(opt, expr, "The result of this expression is ignored.\n\n");
+ expr = opt_const(opt, false);
+ if (!expr) {
+ return NULL;
}
- expr->cost = swapped_cost;
- return true;
- } else {
- return false;
}
+
+ if (df_is_bottom(&opt->before)) {
+ opt_debug(opt, "unreachable\n");
+ opt_warning(opt, expr, "This expression is unreachable.\n\n");
+ expr = opt_const(opt, false);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ /** Table of simple predicates. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ enum pred_type pred;
+ } preds[] = {
+ {eval_acl, ACL_PRED},
+ {eval_capable, CAPABLE_PRED},
+ {eval_empty, EMPTY_PRED},
+ {eval_hidden, HIDDEN_PRED},
+ {eval_nogroup, NOGROUP_PRED},
+ {eval_nouser, NOUSER_PRED},
+ {eval_sparse, SPARSE_PRED},
+ {eval_xattr, XATTR_PRED},
+ };
+
+ for (size_t i = 0; i < countof(preds); ++i) {
+ if (preds[i].eval_fn == expr->eval_fn) {
+ data_flow_pred(opt, preds[i].pred, true);
+ break;
+ }
+ }
+
+ /** Table of simple range comparisons. */
+ static const struct {
+ bfs_eval_fn *eval_fn;
+ enum range_type range;
+ } ranges[] = {
+ {eval_depth, DEPTH_RANGE},
+ {eval_gid, GID_RANGE},
+ {eval_inum, INUM_RANGE},
+ {eval_links, LINKS_RANGE},
+ {eval_size, SIZE_RANGE},
+ {eval_uid, UID_RANGE},
+ };
+
+ for (size_t i = 0; i < countof(ranges); ++i) {
+ if (ranges[i].eval_fn == expr->eval_fn) {
+ data_flow_icmp(opt, expr, ranges[i].range);
+ break;
+ }
+ }
+
+ return expr;
}
/**
- * Recursively reorder sub-expressions to reduce the overall cost.
- *
- * @param expr
- * The expression to optimize.
- * @return
- * Whether any subexpression was reordered.
+ * Data flow visitor.
*/
-static bool reorder_expr_recursive(const struct opt_state *state, struct bfs_expr *expr) {
- if (!bfs_expr_has_children(expr)) {
- return false;
+static const struct visitor data_flow = {
+ .name = "data_flow",
+ .enter = data_flow_enter,
+ .visit = data_flow_visit,
+ .leave = data_flow_leave,
+ .table = (const struct visitor_table[]) {
+ {eval_access, data_flow_access},
+ {eval_gid, data_flow_gid},
+ {eval_inum, data_flow_inum},
+ {eval_links, data_flow_links},
+ {eval_samefile, data_flow_samefile},
+ {eval_size, data_flow_size},
+ {eval_type, data_flow_type},
+ {eval_uid, data_flow_uid},
+ {eval_xtype, data_flow_xtype},
+ {NULL, NULL},
+ },
+};
+
+/** Simplify a negation. */
+static struct bfs_expr *simplify_not(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ if (opt->ignore_result) {
+ opt_debug(opt, "ignored result\n");
+ expr = only_child(expr);
+ }
+
+ return expr;
+}
+
+/** Lift negations out of a conjunction/disjunction using De Morgan's laws. */
+static struct bfs_expr *lift_andor_not(struct bfs_opt *opt, struct bfs_expr *expr) {
+ // Only lift negations if it would reduce the number of (-not) expressions
+ size_t added = 0, removed = 0;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (child->eval_fn == eval_not) {
+ ++removed;
+ } else {
+ ++added;
+ }
+ }
+ if (added >= removed) {
+ return visit_shallow(opt, expr, &annotate);
+ }
+
+ opt_debug(opt, "De Morgan's laws\n");
+
+ if (expr->eval_fn == eval_and) {
+ expr->eval_fn = eval_or;
+ expr->argv = &fake_or_arg;
+ } else {
+ bfs_assert(expr->eval_fn == eval_or);
+ expr->eval_fn = eval_and;
+ expr->argv = &fake_and_arg;
}
- struct bfs_expr *lhs = expr->lhs;
- struct bfs_expr *rhs = expr->rhs;
+ struct bfs_exprs children;
+ foster_children(expr, &children);
- bool ret = false;
- if (lhs) {
- ret |= reorder_expr_recursive(state, lhs);
+ struct bfs_expr *child;
+ while ((child = SLIST_POP(&children))) {
+ opt_enter(opt, "%pe\n", child);
+
+ child = negate_expr(opt, child, &fake_not_arg);
+ if (!child) {
+ return NULL;
+ }
+
+ opt_leave(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
}
- if (rhs) {
- ret |= reorder_expr_recursive(state, rhs);
+
+ expr = visit_shallow(opt, expr, &annotate);
+ return negate_expr(opt, expr, &fake_not_arg);
+}
+
+/** Get the first ignorable expression in a conjunction/disjunction. */
+static struct bfs_expr *first_ignorable(struct bfs_opt *opt, struct bfs_expr *expr) {
+ if (opt->level < 2 || !opt->ignore_result) {
+ return NULL;
}
- if (expr->eval_fn == eval_and || expr->eval_fn == eval_or) {
- if (lhs->pure && rhs->pure) {
- float rhs_prob = expr->eval_fn == eval_and ? rhs->probability : 1.0 - rhs->probability;
- float swapped_cost = rhs->cost + rhs_prob*lhs->cost;
- ret |= reorder_expr(state, expr, swapped_cost);
+ struct bfs_expr *ret = NULL;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ if (!child->pure) {
+ ret = NULL;
+ } else if (!ret) {
+ ret = child;
}
}
return ret;
}
+/** Simplify a conjunction. */
+static struct bfs_expr *simplify_and(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *ignorable = first_ignorable(opt, expr);
+ bool ignore = false;
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (child == ignorable) {
+ ignore = true;
+ }
+
+ if (ignore) {
+ opt_delete(opt, "%pe [ignored result]\n", child);
+ opt_warning(opt, child, "The result of this expression is ignored.\n\n");
+ continue;
+ }
+
+ if (child->eval_fn == eval_true) {
+ opt_delete(opt, "%pe [conjunction elimination]\n", child);
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+
+ if (child->always_false) {
+ while ((child = SLIST_POP(&children))) {
+ opt_delete(opt, "%pe [short-circuit]\n", child);
+ }
+ }
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (!child) {
+ opt_debug(opt, "nullary identity\n");
+ return opt_const(opt, true);
+ } else if (!child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return lift_andor_not(opt, expr);
+}
+
+/** Simplify a disjunction. */
+static struct bfs_expr *simplify_or(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_expr *ignorable = first_ignorable(opt, expr);
+ bool ignore = false;
+
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (child == ignorable) {
+ ignore = true;
+ }
+
+ if (ignore) {
+ opt_delete(opt, "%pe [ignored result]\n", child);
+ opt_warning(opt, child, "The result of this expression is ignored.\n\n");
+ continue;
+ }
+
+ if (child->eval_fn == eval_false) {
+ opt_delete(opt, "%pe [disjunctive syllogism]\n", child);
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+
+ if (child->always_true) {
+ while ((child = SLIST_POP(&children))) {
+ opt_delete(opt, "%pe [short-circuit]\n", child);
+ }
+ }
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (!child) {
+ opt_debug(opt, "nullary identity\n");
+ return opt_const(opt, false);
+ } else if (!child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return lift_andor_not(opt, expr);
+}
+
+/** Simplify a comma expression. */
+static struct bfs_expr *simplify_comma(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
+ struct bfs_exprs children;
+ foster_children(expr, &children);
+
+ while (!SLIST_EMPTY(&children)) {
+ struct bfs_expr *child = SLIST_POP(&children);
+
+ if (opt->level >= 2 && child->pure && !SLIST_EMPTY(&children)) {
+ opt_delete(opt, "%pe [ignored result]\n", child);
+ opt_warning(opt, child, "The result of this expression is ignored.\n\n");
+ continue;
+ }
+
+ opt_visit(opt, "%pe\n", child);
+ bfs_expr_append(expr, child);
+ }
+
+ struct bfs_expr *child = bfs_expr_children(expr);
+ if (child && !child->next) {
+ opt_debug(opt, "unary identity\n");
+ return only_child(expr);
+ }
+
+ return expr;
+}
+
/**
- * Optimize a top-level expression.
+ * Logical simplification visitor.
*/
-static struct bfs_expr *optimize_expr(struct opt_state *state, struct bfs_expr *expr) {
- struct opt_facts saved_impure = *state->facts_when_impure;
+static const struct visitor simplify = {
+ .name = "simplify",
+ .table = (const struct visitor_table[]) {
+ {eval_not, simplify_not},
+ {eval_and, simplify_and},
+ {eval_or, simplify_or},
+ {eval_comma, simplify_comma},
+ {NULL, NULL},
+ },
+};
- expr = optimize_expr_recursive(state, expr);
- if (!expr) {
- return NULL;
- }
+/** Optimize an expression. */
+static struct bfs_expr *optimize(struct bfs_opt *opt, struct bfs_expr *expr) {
+ opt_enter(opt, "pass 0:\n");
+ expr = visit(opt, expr, &annotate);
+ opt_leave(opt, NULL);
+
+ /** Table of optimization passes. */
+ static const struct {
+ /** Minimum optlevel for this pass. */
+ int level;
+ /** The visitor for this pass. */
+ const struct visitor *visitor;
+ } passes[] = {
+ {1, &canonicalize},
+ {3, &reorder},
+ {2, &data_flow},
+ {1, &simplify},
+ };
- if (state->ctx->optlevel >= 3 && reorder_expr_recursive(state, expr)) {
- // Re-do optimizations to account for the new ordering
- *state->facts_when_impure = saved_impure;
- expr = optimize_expr_recursive(state, expr);
- if (!expr) {
- return NULL;
+ struct df_domain impure;
+
+ for (int i = 0; i < 3; ++i) {
+ struct bfs_opt nested = *opt;
+ nested.impure = &impure;
+ impure = *opt->impure;
+
+ opt_enter(&nested, "pass %d:\n", i + 1);
+
+ for (size_t j = 0; j < countof(passes); ++j) {
+ if (opt->level < passes[j].level) {
+ continue;
+ }
+
+ // Skip reordering the first time through the passes, to
+ // make warnings more understandable
+ if (passes[j].visitor == &reorder) {
+ if (i == 0) {
+ continue;
+ } else {
+ nested.warn = false;
+ }
+ }
+
+ expr = visit(&nested, expr, passes[j].visitor);
+ if (!expr) {
+ return NULL;
+ }
+ }
+
+ opt_leave(&nested, NULL);
+
+ if (!bfs_expr_is_parent(expr)) {
+ break;
}
}
+ *opt->impure = impure;
return expr;
}
+/** Estimate the odds of an expression calling stat(). */
+static float expr_stat_odds(struct bfs_expr *expr) {
+ if (expr->calls_stat) {
+ return 1.0;
+ }
+
+ float nostat_odds = 1.0;
+ float reached_odds = 1.0;
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ float child_odds = expr_stat_odds(child);
+ nostat_odds *= 1.0 - reached_odds * child_odds;
+
+ if (expr->eval_fn == eval_and) {
+ reached_odds *= child->probability;
+ } else if (expr->eval_fn == eval_or) {
+ reached_odds *= 1.0 - child->probability;
+ }
+ }
+
+ return 1.0 - nostat_odds;
+}
+
+/** Estimate the odds of calling stat(). */
+static float estimate_stat_odds(struct bfs_ctx *ctx) {
+ if (ctx->unique) {
+ return 1.0;
+ }
+
+ float nostat_odds = 1.0 - expr_stat_odds(ctx->exclude);
+
+ float reached_odds = 1.0 - ctx->exclude->probability;
+ float expr_odds = expr_stat_odds(ctx->expr);
+ nostat_odds *= 1.0 - reached_odds * expr_odds;
+
+ return 1.0 - nostat_odds;
+}
+
int bfs_optimize(struct bfs_ctx *ctx) {
bfs_ctx_dump(ctx, DEBUG_OPT);
- struct opt_facts facts_when_impure;
- set_facts_impossible(&facts_when_impure);
+ struct df_domain impure;
+ df_init_bottom(&impure);
- struct opt_state state = {
+ struct bfs_opt opt = {
.ctx = ctx,
- .facts_when_impure = &facts_when_impure,
+ .level = ctx->optlevel,
+ .depth = 0,
+ .warn = ctx->warn,
+ .ignore_result = false,
+ .impure = &impure,
};
- facts_init(&state.facts);
+ df_init_top(&opt.before);
- ctx->exclude = optimize_expr(&state, ctx->exclude);
+ ctx->exclude = optimize(&opt, ctx->exclude);
if (!ctx->exclude) {
return -1;
}
// Only non-excluded files are evaluated
- state.facts = state.facts_when_false;
+ opt.before = opt.after_false;
+ opt.ignore_result = true;
- struct range *depth = &state.facts.ranges[DEPTH_RANGE];
- constrain_min(depth, ctx->mindepth);
- constrain_max(depth, ctx->maxdepth);
+ struct df_range *depth = &opt.before.ranges[DEPTH_RANGE];
+ if (ctx->mindepth > 0) {
+ constrain_min(depth, ctx->mindepth);
+ }
+ if (ctx->maxdepth < INT_MAX) {
+ constrain_max(depth, ctx->maxdepth);
+ }
- ctx->expr = optimize_expr(&state, ctx->expr);
+ ctx->expr = optimize(&opt, ctx->expr);
if (!ctx->expr) {
return -1;
}
- ctx->expr = ignore_result(&state, ctx->expr);
-
- if (facts_are_impossible(&facts_when_impure)) {
+ if (opt.level >= 2 && df_is_bottom(&impure)) {
bfs_warning(ctx, "This command won't do anything.\n\n");
}
- const struct range *depth_when_impure = &facts_when_impure.ranges[DEPTH_RANGE];
- long long mindepth = depth_when_impure->min;
- long long maxdepth = depth_when_impure->max;
+ const struct df_range *impure_depth = &impure.ranges[DEPTH_RANGE];
+ long long mindepth = impure_depth->min;
+ long long maxdepth = impure_depth->max;
- int optlevel = ctx->optlevel;
+ opt_enter(&opt, "post-process:\n");
- if (optlevel >= 2 && mindepth > ctx->mindepth) {
+ if (opt.level >= 2 && mindepth > ctx->mindepth) {
if (mindepth > INT_MAX) {
mindepth = INT_MAX;
}
+ opt_enter(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth);
ctx->mindepth = mindepth;
- opt_debug(&state, 2, "data flow: mindepth --> %d\n", ctx->mindepth);
+ opt_leave(&opt, "${blu}-mindepth${rs} ${bld}%d${rs}\n", ctx->mindepth);
}
- if (optlevel >= 4 && maxdepth < ctx->maxdepth) {
+ if (opt.level >= 4 && maxdepth < ctx->maxdepth) {
if (maxdepth < INT_MIN) {
maxdepth = INT_MIN;
}
+ opt_enter(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth);
ctx->maxdepth = maxdepth;
- opt_debug(&state, 4, "data flow: maxdepth --> %d\n", ctx->maxdepth);
+ opt_leave(&opt, "${blu}-maxdepth${rs} ${bld}%d${rs}\n", ctx->maxdepth);
}
+ if (opt.level >= 3) {
+ // bfs_eval() can do lazy stat() calls, but only on one thread.
+ float lazy_cost = estimate_stat_odds(ctx);
+ // bftw() can do eager stat() calls in parallel
+ float eager_cost = 1.0 / ctx->threads;
+
+ if (eager_cost <= lazy_cost) {
+ opt_enter(&opt, "lazy stat cost: ${ylw}%g${rs}\n", lazy_cost);
+ ctx->flags |= BFTW_STAT;
+ opt_leave(&opt, "eager stat cost: ${ylw}%g${rs}\n", eager_cost);
+ }
+
+ }
+
+ opt_leave(&opt, NULL);
+
return 0;
}
diff --git a/src/opt.h b/src/opt.h
index 5f8180d..4aac129 100644
--- a/src/opt.h
+++ b/src/opt.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Optimization.
@@ -34,4 +21,3 @@ struct bfs_ctx;
int bfs_optimize(struct bfs_ctx *ctx);
#endif // BFS_OPT_H
-
diff --git a/src/parse.c b/src/parse.c
index fbb095d..a1155c0 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* The command line parser. Expressions are parsed by recursive descent, with a
@@ -21,28 +8,29 @@
* flags like always-true options, and skipping over paths wherever they appear.
*/
+#include "prelude.h"
#include "parse.h"
-#include "bfs.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
-#include "darray.h"
#include "diag.h"
#include "dir.h"
#include "eval.h"
#include "exec.h"
#include "expr.h"
#include "fsade.h"
+#include "list.h"
#include "opt.h"
#include "printf.h"
#include "pwcache.h"
+#include "sanity.h"
#include "stat.h"
#include "typo.h"
-#include "util.h"
#include "xregex.h"
#include "xspawn.h"
#include "xtime.h"
-#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
@@ -50,170 +38,21 @@
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
-#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/time.h>
#include <sys/stat.h>
-#include <sys/wait.h>
+#include <sys/types.h>
#include <time.h>
#include <unistd.h>
// Strings printed by -D tree for "fake" expressions
-static char *fake_and_arg = "-a";
-static char *fake_false_arg = "-false";
+static char *fake_and_arg = "-and";
static char *fake_hidden_arg = "-hidden";
-static char *fake_or_arg = "-o";
+static char *fake_or_arg = "-or";
static char *fake_print_arg = "-print";
static char *fake_true_arg = "-true";
-// Cost estimation constants
-#define FAST_COST 40.0
-#define STAT_COST 1000.0
-#define PRINT_COST 20000.0
-
-struct bfs_expr bfs_true = {
- .eval_fn = eval_true,
- .argc = 1,
- .argv = &fake_true_arg,
- .pure = true,
- .always_true = true,
- .synthetic = true,
- .cost = FAST_COST,
- .probability = 1.0,
-};
-
-struct bfs_expr bfs_false = {
- .eval_fn = eval_false,
- .argc = 1,
- .argv = &fake_false_arg,
- .pure = true,
- .always_false = true,
- .synthetic = true,
- .cost = FAST_COST,
- .probability = 0.0,
-};
-
-void bfs_expr_free(struct bfs_expr *expr) {
- if (!expr || expr == &bfs_true || expr == &bfs_false) {
- return;
- }
-
- if (bfs_expr_has_children(expr)) {
- bfs_expr_free(expr->rhs);
- bfs_expr_free(expr->lhs);
- } else if (expr->eval_fn == eval_exec) {
- bfs_exec_free(expr->exec);
- } else if (expr->eval_fn == eval_fprintf) {
- bfs_printf_free(expr->printf);
- } else if (expr->eval_fn == eval_regex) {
- bfs_regfree(expr->regex);
- }
-
- free(expr);
-}
-
-struct bfs_expr *bfs_expr_new(bfs_eval_fn *eval_fn, size_t argc, char **argv) {
- struct bfs_expr *expr = malloc(sizeof(*expr));
- if (!expr) {
- perror("malloc()");
- return NULL;
- }
-
- expr->eval_fn = eval_fn;
- expr->argc = argc;
- expr->argv = argv;
- expr->persistent_fds = 0;
- expr->ephemeral_fds = 0;
- expr->pure = false;
- expr->always_true = false;
- expr->always_false = false;
- expr->synthetic = false;
- expr->cost = FAST_COST;
- expr->probability = 0.5;
- expr->evaluations = 0;
- expr->successes = 0;
- expr->elapsed.tv_sec = 0;
- expr->elapsed.tv_nsec = 0;
- return expr;
-}
-
-/**
- * Create a new unary expression.
- */
-static struct bfs_expr *new_unary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) {
- struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv);
- if (!expr) {
- bfs_expr_free(rhs);
- return NULL;
- }
-
- expr->lhs = NULL;
- expr->rhs = rhs;
- assert(bfs_expr_has_children(expr));
-
- expr->persistent_fds = rhs->persistent_fds;
- expr->ephemeral_fds = rhs->ephemeral_fds;
- return expr;
-}
-
-/**
- * Create a new binary expression.
- */
-static struct bfs_expr *new_binary_expr(bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) {
- struct bfs_expr *expr = bfs_expr_new(eval_fn, 1, argv);
- if (!expr) {
- bfs_expr_free(rhs);
- bfs_expr_free(lhs);
- return NULL;
- }
-
- expr->lhs = lhs;
- expr->rhs = rhs;
- assert(bfs_expr_has_children(expr));
-
- if (argv == &fake_and_arg || argv == &fake_or_arg) {
- expr->synthetic = true;
- }
-
- expr->persistent_fds = lhs->persistent_fds + rhs->persistent_fds;
- if (lhs->ephemeral_fds > rhs->ephemeral_fds) {
- expr->ephemeral_fds = lhs->ephemeral_fds;
- } else {
- expr->ephemeral_fds = rhs->ephemeral_fds;
- }
-
- return expr;
-}
-
-bool bfs_expr_has_children(const struct bfs_expr *expr) {
- return expr->eval_fn == eval_and
- || expr->eval_fn == eval_or
- || expr->eval_fn == eval_not
- || expr->eval_fn == eval_comma;
-}
-
-bool bfs_expr_never_returns(const struct bfs_expr *expr) {
- // Expressions that never return are vacuously both always true and always false
- return expr->always_true && expr->always_false;
-}
-
-/**
- * Set an expression to always return true.
- */
-static void expr_set_always_true(struct bfs_expr *expr) {
- expr->always_true = true;
- expr->probability = 1.0;
-}
-
-/**
- * Set an expression to never return.
- */
-static void expr_set_never_returns(struct bfs_expr *expr) {
- expr->always_true = expr->always_false = true;
-}
-
/**
* Color use flags.
*/
@@ -224,9 +63,9 @@ enum use_color {
};
/**
- * Ephemeral state for parsing the command line.
+ * Command line parser state.
*/
-struct parser_state {
+struct bfs_parser {
/** The command line being constructed. */
struct bfs_ctx *ctx;
/** The command line arguments being parsed. */
@@ -258,20 +97,20 @@ struct parser_state {
char **last_arg;
/** A "-depth"-type argument, if any. */
char **depth_arg;
+ /** A "-limit" argument, if any. */
+ char **limit_arg;
/** A "-prune" argument, if any. */
char **prune_arg;
/** A "-mount" argument, if any. */
char **mount_arg;
/** An "-xdev" argument, if any. */
char **xdev_arg;
- /** A "-files0-from" argument, if any. */
- char **files0_arg;
/** A "-files0-from -" argument, if any. */
char **files0_stdin_arg;
/** An "-ok"-type expression, if any. */
const struct bfs_expr *ok_expr;
- /** The current time. */
+ /** The current time (maybe modified by -daystart). */
struct timespec now;
};
@@ -296,8 +135,8 @@ enum token_type {
/**
* Print a low-level error message during parsing.
*/
-static void parse_perror(const struct parser_state *state, const char *str) {
- bfs_perror(state->ctx, str);
+static void parse_perror(const struct bfs_parser *parser, const char *str) {
+ bfs_perror(parser->ctx, str);
}
/** Initialize an empty highlighted range. */
@@ -311,7 +150,7 @@ static void init_highlight(const struct bfs_ctx *ctx, bool *args) {
static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc, bool *args) {
size_t i = argv - ctx->argv;
for (size_t j = 0; j < argc; ++j) {
- assert(i + j < ctx->argc);
+ bfs_assert(i + j < ctx->argc);
args[i + j] = true;
}
}
@@ -319,30 +158,30 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc,
/**
* Print an error message during parsing.
*/
-BFS_FORMATTER(2, 3)
-static void parse_error(const struct parser_state *state, const char *format, ...) {
+attr(printf(2, 3))
+static void parse_error(const struct bfs_parser *parser, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
- highlight_args(ctx, state->argv, 1, highlight);
+ highlight_args(ctx, parser->argv, 1, highlight);
bfs_argv_error(ctx, highlight);
va_list args;
va_start(args, format);
errno = error;
- bfs_verror(state->ctx, format, args);
+ bfs_verror(parser->ctx, format, args);
va_end(args);
}
/**
* Print an error about some command line arguments.
*/
-BFS_FORMATTER(4, 5)
-static void parse_argv_error(const struct parser_state *state, char **argv, size_t argc, const char *format, ...) {
+attr(printf(4, 5))
+static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
@@ -359,10 +198,10 @@ static void parse_argv_error(const struct parser_state *state, char **argv, size
/**
* Print an error about conflicting command line arguments.
*/
-BFS_FORMATTER(6, 7)
-static void parse_conflict_error(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
+attr(printf(6, 7))
+static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
@@ -380,10 +219,10 @@ static void parse_conflict_error(const struct parser_state *state, char **argv1,
/**
* Print an error about an expression.
*/
-BFS_FORMATTER(3, 4)
-static void parse_expr_error(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) {
+attr(printf(3, 4))
+static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bfs_expr_error(ctx, expr);
@@ -397,14 +236,14 @@ static void parse_expr_error(const struct parser_state *state, const struct bfs_
/**
* Print a warning message during parsing.
*/
-BFS_FORMATTER(2, 3)
-static bool parse_warning(const struct parser_state *state, const char *format, ...) {
+attr(printf(2, 3))
+static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
- highlight_args(ctx, state->argv, 1, highlight);
+ highlight_args(ctx, parser->argv, 1, highlight);
if (!bfs_argv_warning(ctx, highlight)) {
return false;
}
@@ -412,7 +251,7 @@ static bool parse_warning(const struct parser_state *state, const char *format,
va_list args;
va_start(args, format);
errno = error;
- bool ret = bfs_vwarning(state->ctx, format, args);
+ bool ret = bfs_vwarning(parser->ctx, format, args);
va_end(args);
return ret;
}
@@ -420,10 +259,10 @@ static bool parse_warning(const struct parser_state *state, const char *format,
/**
* Print a warning about conflicting command line arguments.
*/
-BFS_FORMATTER(6, 7)
-static bool parse_conflict_warning(const struct parser_state *state, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
+attr(printf(6, 7))
+static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
@@ -444,10 +283,10 @@ static bool parse_conflict_warning(const struct parser_state *state, char **argv
/**
* Print a warning about an expression.
*/
-BFS_FORMATTER(3, 4)
-static bool parse_expr_warning(const struct parser_state *state, const struct bfs_expr *expr, const char *format, ...) {
+attr(printf(3, 4))
+static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
int error = errno;
- const struct bfs_ctx *ctx = state->ctx;
+ const struct bfs_ctx *ctx = parser->ctx;
if (!bfs_expr_warning(ctx, expr)) {
return false;
@@ -462,19 +301,58 @@ static bool parse_expr_warning(const struct parser_state *state, const struct bf
}
/**
+ * Allocate a new expression.
+ */
+static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv) {
+ struct bfs_expr *expr = bfs_expr_new(parser->ctx, eval_fn, argc, argv);
+ if (!expr) {
+ parse_perror(parser, "bfs_expr_new()");
+ }
+ return expr;
+}
+
+/**
+ * Create a new unary expression.
+ */
+static struct bfs_expr *new_unary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *rhs, char **argv) {
+ struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv);
+ if (!expr) {
+ return NULL;
+ }
+
+ bfs_assert(bfs_expr_is_parent(expr));
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/**
+ * Create a new binary expression.
+ */
+static struct bfs_expr *new_binary_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, struct bfs_expr *lhs, struct bfs_expr *rhs, char **argv) {
+ struct bfs_expr *expr = parse_new_expr(parser, eval_fn, 1, argv);
+ if (!expr) {
+ return NULL;
+ }
+
+ bfs_assert(bfs_expr_is_parent(expr));
+ bfs_expr_append(expr, lhs);
+ bfs_expr_append(expr, rhs);
+ return expr;
+}
+
+/**
* Fill in a "-print"-type expression.
*/
-static void init_print_expr(struct parser_state *state, struct bfs_expr *expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- expr->cfile = state->ctx->cout;
+static void init_print_expr(struct bfs_parser *parser, struct bfs_expr *expr) {
+ expr->cfile = parser->ctx->cout;
+ expr->path = NULL;
}
/**
* Open a file for an expression.
*/
-static int expr_open(struct parser_state *state, struct bfs_expr *expr, const char *path) {
- struct bfs_ctx *ctx = state->ctx;
+static int expr_open(struct bfs_parser *parser, struct bfs_expr *expr, const char *path) {
+ struct bfs_ctx *ctx = parser->ctx;
FILE *file = NULL;
CFILE *cfile = NULL;
@@ -484,7 +362,7 @@ static int expr_open(struct parser_state *state, struct bfs_expr *expr, const ch
goto fail;
}
- cfile = cfwrap(file, state->use_color ? ctx->colors : NULL, true);
+ cfile = cfwrap(file, parser->use_color ? ctx->colors : NULL, true);
if (!cfile) {
goto fail;
}
@@ -499,10 +377,11 @@ static int expr_open(struct parser_state *state, struct bfs_expr *expr, const ch
}
expr->cfile = dedup;
+ expr->path = path;
return 0;
fail:
- parse_expr_error(state, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%m.\n");
if (cfile) {
cfclose(cfile);
} else if (file) {
@@ -514,15 +393,15 @@ fail:
/**
* Invoke bfs_stat() on an argument.
*/
-static int stat_arg(const struct parser_state *state, char **arg, struct bfs_stat *sb) {
- const struct bfs_ctx *ctx = state->ctx;
+static int stat_arg(const struct bfs_parser *parser, char **arg, struct bfs_stat *sb) {
+ const struct bfs_ctx *ctx = parser->ctx;
bool follow = ctx->flags & (BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
enum bfs_stat_flags flags = follow ? BFS_STAT_TRYFOLLOW : BFS_STAT_NOFOLLOW;
int ret = bfs_stat(AT_FDCWD, *arg, flags, sb);
if (ret != 0) {
- parse_argv_error(state, arg, 1, "%m.\n");
+ parse_argv_error(parser, arg, 1, "%m.\n");
}
return ret;
}
@@ -530,52 +409,53 @@ static int stat_arg(const struct parser_state *state, char **arg, struct bfs_sta
/**
* Parse the expression specified on the command line.
*/
-static struct bfs_expr *parse_expr(struct parser_state *state);
+static struct bfs_expr *parse_expr(struct bfs_parser *parser);
/**
* Advance by a single token.
*/
-static char **parser_advance(struct parser_state *state, enum token_type type, size_t argc) {
+static char **parser_advance(struct bfs_parser *parser, enum token_type type, size_t argc) {
if (type != T_FLAG && type != T_PATH) {
- state->expr_started = true;
+ parser->expr_started = true;
}
if (type != T_PATH) {
- state->last_arg = state->argv;
+ parser->last_arg = parser->argv;
}
- char **argv = state->argv;
- state->argv += argc;
+ char **argv = parser->argv;
+ parser->argv += argc;
return argv;
}
/**
* Parse a root path.
*/
-static int parse_root(struct parser_state *state, const char *path) {
- char *copy = strdup(path);
- if (!copy) {
- parse_perror(state, "strdup()");
+static int parse_root(struct bfs_parser *parser, const char *path) {
+ struct bfs_ctx *ctx = parser->ctx;
+ const char **root = RESERVE(const char *, &ctx->paths, &ctx->npaths);
+ if (!root) {
+ parse_perror(parser, "RESERVE()");
return -1;
}
- struct bfs_ctx *ctx = state->ctx;
- if (DARRAY_PUSH(&ctx->paths, &copy) != 0) {
- parse_perror(state, "DARRAY_PUSH()");
- free(copy);
+ *root = strdup(path);
+ if (!*root) {
+ --ctx->npaths;
+ parse_perror(parser, "strdup()");
return -1;
}
- state->implicit_root = false;
+ parser->implicit_root = false;
return 0;
}
/**
* While parsing an expression, skip any paths and add them to ctx->paths.
*/
-static int skip_paths(struct parser_state *state) {
+static int skip_paths(struct bfs_parser *parser) {
while (true) {
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (!arg) {
return 0;
}
@@ -585,7 +465,7 @@ static int skip_paths(struct parser_state *state) {
// find uses -- to separate flags from the rest
// of the command line. We allow mixing flags
// and paths/predicates, so we just ignore --.
- parser_advance(state, T_FLAG, 1);
+ parser_advance(parser, T_FLAG, 1);
continue;
}
if (strcmp(arg, "-") != 0) {
@@ -600,7 +480,7 @@ static int skip_paths(struct parser_state *state) {
return 0;
}
- if (state->expr_started) {
+ if (parser->expr_started) {
// By POSIX, these can be paths. We only treat them as
// such at the beginning of the command line.
if (strcmp(arg, ")") == 0 || strcmp(arg, ",") == 0) {
@@ -608,16 +488,16 @@ static int skip_paths(struct parser_state *state) {
}
}
- if (state->excluding) {
- parse_warning(state, "This path will not be excluded. Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n");
- bfs_warning(state->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n");
+ if (parser->excluding) {
+ parse_warning(parser, "This path will not be excluded. Use a test like ${blu}-name${rs} or ${blu}-path${rs}\n");
+ bfs_warning(parser->ctx, "within ${red}-exclude${rs} to exclude matching files.\n\n");
}
- if (parse_root(state, arg) != 0) {
+ if (parse_root(parser, arg) != 0) {
return -1;
}
- parser_advance(state, T_PATH, 1);
+ parser_advance(parser, T_PATH, 1);
}
}
@@ -636,14 +516,18 @@ enum int_flags {
/**
* Parse an integer.
*/
-static const char *parse_int(const struct parser_state *state, char **arg, const char *str, void *result, enum int_flags flags) {
- char *endptr;
+static const char *parse_int(const struct bfs_parser *parser, char **arg, const char *str, void *result, enum int_flags flags) {
+ // strtoll() skips leading spaces, but we want to reject them
+ if (xisspace(str[0])) {
+ goto bad;
+ }
int base = flags & IF_BASE_MASK;
if (base == 0) {
base = 10;
}
+ char *endptr;
errno = 0;
long long value = strtoll(str, &endptr, base);
if (errno != 0) {
@@ -654,6 +538,9 @@ static const char *parse_int(const struct parser_state *state, char **arg, const
}
}
+ // https://github.com/llvm/llvm-project/issues/64946
+ sanitize_init(&endptr);
+
if (endptr == str) {
goto bad;
}
@@ -686,7 +573,7 @@ static const char *parse_int(const struct parser_state *state, char **arg, const
break;
default:
- assert(!"Invalid int size");
+ bfs_bug("Invalid int size");
goto bad;
}
@@ -694,19 +581,19 @@ static const char *parse_int(const struct parser_state *state, char **arg, const
bad:
if (!(flags & IF_QUIET)) {
- parse_argv_error(state, arg, 1, "${bld}%s${rs} is not a valid integer.\n", str);
+ parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is not a valid integer.\n", str);
}
return NULL;
negative:
if (!(flags & IF_QUIET)) {
- parse_argv_error(state, arg, 1, "Negative integer ${bld}%s${rs} is not allowed here.\n", str);
+ parse_argv_error(parser, arg, 1, "Negative integer ${bld}%pq${rs} is not allowed here.\n", str);
}
return NULL;
range:
if (!(flags & IF_QUIET)) {
- parse_argv_error(state, arg, 1, "${bld}%s${rs} is too large an integer.\n", str);
+ parse_argv_error(parser, arg, 1, "${bld}%pq${rs} is too large an integer.\n", str);
}
return NULL;
}
@@ -714,7 +601,7 @@ range:
/**
* Parse an integer and a comparison flag.
*/
-static const char *parse_icmp(const struct parser_state *state, struct bfs_expr *expr, enum int_flags flags) {
+static const char *parse_icmp(const struct bfs_parser *parser, struct bfs_expr *expr, enum int_flags flags) {
char **arg = &expr->argv[1];
const char *str = *arg;
switch (str[0]) {
@@ -731,7 +618,7 @@ static const char *parse_icmp(const struct parser_state *state, struct bfs_expr
break;
}
- return parse_int(state, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED);
+ return parse_int(parser, arg, str, &expr->num, flags | IF_LONG_LONG | IF_UNSIGNED);
}
/**
@@ -753,136 +640,139 @@ static bool looks_like_icmp(const char *str) {
/**
* Parse a single flag.
*/
-static struct bfs_expr *parse_flag(struct parser_state *state, size_t argc) {
- parser_advance(state, T_FLAG, argc);
- return &bfs_true;
+static struct bfs_expr *parse_flag(struct bfs_parser *parser, size_t argc) {
+ char **argv = parser_advance(parser, T_FLAG, argc);
+ return parse_new_expr(parser, eval_true, argc, argv);
}
/**
* Parse a flag that doesn't take a value.
*/
-static struct bfs_expr *parse_nullary_flag(struct parser_state *state) {
- return parse_flag(state, 1);
+static struct bfs_expr *parse_nullary_flag(struct bfs_parser *parser) {
+ return parse_flag(parser, 1);
+}
+
+/**
+ * Parse a flag that takes a value.
+ */
+static struct bfs_expr *parse_unary_flag(struct bfs_parser *parser) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${cyn}%s${rs} needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_flag(parser, 2);
}
/**
* Parse a single option.
*/
-static struct bfs_expr *parse_option(struct parser_state *state, size_t argc) {
- parser_advance(state, T_OPTION, argc);
- return &bfs_true;
+static struct bfs_expr *parse_option(struct bfs_parser *parser, size_t argc) {
+ char **argv = parser_advance(parser, T_OPTION, argc);
+ return parse_new_expr(parser, eval_true, argc, argv);
}
/**
* Parse an option that doesn't take a value.
*/
-static struct bfs_expr *parse_nullary_option(struct parser_state *state) {
- return parse_option(state, 1);
+static struct bfs_expr *parse_nullary_option(struct bfs_parser *parser) {
+ return parse_option(parser, 1);
}
/**
* Parse an option that takes a value.
*/
-static struct bfs_expr *parse_unary_option(struct parser_state *state) {
- return parse_option(state, 2);
+static struct bfs_expr *parse_unary_option(struct bfs_parser *parser) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
+ if (!value) {
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
+ return NULL;
+ }
+
+ return parse_option(parser, 2);
}
/**
* Parse a single test.
*/
-static struct bfs_expr *parse_test(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) {
- char **argv = parser_advance(state, T_TEST, argc);
- struct bfs_expr *expr = bfs_expr_new(eval_fn, argc, argv);
- if (expr) {
- expr->pure = true;
- }
- return expr;
+static struct bfs_expr *parse_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
+ char **argv = parser_advance(parser, T_TEST, argc);
+ return parse_new_expr(parser, eval_fn, argc, argv);
}
/**
* Parse a test that doesn't take a value.
*/
-static struct bfs_expr *parse_nullary_test(struct parser_state *state, bfs_eval_fn *eval_fn) {
- return parse_test(state, eval_fn, 1);
+static struct bfs_expr *parse_nullary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ return parse_test(parser, eval_fn, 1);
}
/**
* Parse a test that takes a value.
*/
-static struct bfs_expr *parse_unary_test(struct parser_state *state, bfs_eval_fn *eval_fn) {
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
+static struct bfs_expr *parse_unary_test(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
return NULL;
}
- return parse_test(state, eval_fn, 2);
+ return parse_test(parser, eval_fn, 2);
}
/**
* Parse a single action.
*/
-static struct bfs_expr *parse_action(struct parser_state *state, bfs_eval_fn *eval_fn, size_t argc) {
- char **argv = parser_advance(state, T_ACTION, argc);
+static struct bfs_expr *parse_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc) {
+ char **argv = parser_advance(parser, T_ACTION, argc);
- if (state->excluding) {
- parse_argv_error(state, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n");
+ if (parser->excluding) {
+ parse_argv_error(parser, argv, argc, "This action is not supported within ${red}-exclude${rs}.\n");
return NULL;
}
- if (eval_fn != eval_prune && eval_fn != eval_quit) {
- state->implicit_print = false;
+ if (eval_fn != eval_limit && eval_fn != eval_prune && eval_fn != eval_quit) {
+ parser->implicit_print = false;
}
- return bfs_expr_new(eval_fn, argc, argv);
+ return parse_new_expr(parser, eval_fn, argc, argv);
}
/**
* Parse an action that takes no arguments.
*/
-static struct bfs_expr *parse_nullary_action(struct parser_state *state, bfs_eval_fn *eval_fn) {
- return parse_action(state, eval_fn, 1);
+static struct bfs_expr *parse_nullary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ return parse_action(parser, eval_fn, 1);
}
/**
* Parse an action that takes one argument.
*/
-static struct bfs_expr *parse_unary_action(struct parser_state *state, bfs_eval_fn *eval_fn) {
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
+static struct bfs_expr *parse_unary_action(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ const char *arg = parser->argv[0];
+ const char *value = parser->argv[1];
if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
+ parse_error(parser, "${blu}%s${rs} needs a value.\n", arg);
return NULL;
}
- return parse_action(state, eval_fn, 2);
-}
-
-/**
- * Add an expression to the exclusions.
- */
-static int parse_exclude(struct parser_state *state, struct bfs_expr *expr) {
- struct bfs_ctx *ctx = state->ctx;
- ctx->exclude = new_binary_expr(eval_or, ctx->exclude, expr, &fake_or_arg);
- if (ctx->exclude) {
- return 0;
- } else {
- return -1;
- }
+ return parse_action(parser, eval_fn, 2);
}
/**
* Parse a test expression with integer data and a comparison flag.
*/
-static struct bfs_expr *parse_test_icmp(struct parser_state *state, bfs_eval_fn *eval_fn) {
- struct bfs_expr *expr = parse_unary_test(state, eval_fn);
+static struct bfs_expr *parse_test_icmp(struct bfs_parser *parser, bfs_eval_fn *eval_fn) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_fn);
if (!expr) {
return NULL;
}
- if (!parse_icmp(state, expr, 0)) {
- bfs_expr_free(expr);
+ if (!parse_icmp(parser, expr, 0)) {
return NULL;
}
@@ -918,22 +808,19 @@ static bool parse_debug_flag(const char *flag, size_t len, const char *expected)
/**
* Parse -D FLAG.
*/
-static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
+static struct bfs_expr *parse_debug(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
- const char *arg = state->argv[0];
- const char *flags = state->argv[1];
- if (!flags) {
- parse_error(state, "${cyn}%s${rs} needs a flag.\n\n", arg);
+ struct bfs_expr *expr = parse_unary_flag(parser);
+ if (!expr) {
+ cfprintf(ctx->cerr, "\n");
debug_help(ctx->cerr);
return NULL;
}
- parser_advance(state, T_FLAG, 1);
-
bool unrecognized = false;
- for (const char *flag = flags, *next; flag; flag = next) {
+ for (const char *flag = expr->argv[1], *next; flag; flag = next) {
size_t len = strcspn(flag, ",");
if (flag[len]) {
next = flag + len + 1;
@@ -943,7 +830,7 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar
if (parse_debug_flag(flag, len, "help")) {
debug_help(ctx->cout);
- state->just_info = true;
+ parser->just_info = true;
return NULL;
} else if (parse_debug_flag(flag, len, "all")) {
ctx->debug = DEBUG_ALL;
@@ -961,7 +848,7 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar
if (DEBUG_ALL & i) {
ctx->debug |= i;
} else {
- if (parse_warning(state, "Unrecognized debug flag ${bld}")) {
+ if (parse_expr_warning(parser, expr, "Unrecognized debug flag ${bld}")) {
fwrite(flag, 1, len, stderr);
cfprintf(ctx->cerr, "${rs}.\n\n");
unrecognized = true;
@@ -974,91 +861,74 @@ static struct bfs_expr *parse_debug(struct parser_state *state, int arg1, int ar
cfprintf(ctx->cerr, "\n");
}
- parser_advance(state, T_FLAG, 1);
- return &bfs_true;
+ return expr;
}
/**
* Parse -On.
*/
-static struct bfs_expr *parse_optlevel(struct parser_state *state, int arg1, int arg2) {
- int *optlevel = &state->ctx->optlevel;
+static struct bfs_expr *parse_optlevel(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_flag(parser);
+ if (!expr) {
+ return NULL;
+ }
- if (strcmp(state->argv[0], "-Ofast") == 0) {
+ int *optlevel = &parser->ctx->optlevel;
+
+ if (strcmp(expr->argv[0], "-Ofast") == 0) {
*optlevel = 4;
- } else if (!parse_int(state, state->argv, state->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) {
+ } else if (!parse_int(parser, expr->argv, expr->argv[0] + 2, optlevel, IF_INT | IF_UNSIGNED)) {
return NULL;
}
if (*optlevel > 4) {
- parse_warning(state, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", state->argv[0] + 2);
+ parse_expr_warning(parser, expr, "${cyn}-O${bld}%s${rs} is the same as ${cyn}-O${bld}4${rs}.\n\n", expr->argv[0] + 2);
}
- return parse_nullary_flag(state);
+ return expr;
}
/**
* Parse -[PHL], -follow.
*/
-static struct bfs_expr *parse_follow(struct parser_state *state, int flags, int option) {
- struct bfs_ctx *ctx = state->ctx;
+static struct bfs_expr *parse_follow(struct bfs_parser *parser, int flags, int option) {
+ struct bfs_ctx *ctx = parser->ctx;
ctx->flags &= ~(BFTW_FOLLOW_ROOTS | BFTW_FOLLOW_ALL);
ctx->flags |= flags;
if (option) {
- return parse_nullary_option(state);
+ return parse_nullary_option(parser);
} else {
- return parse_nullary_flag(state);
+ return parse_nullary_flag(parser);
}
}
/**
* Parse -X.
*/
-static struct bfs_expr *parse_xargs_safe(struct parser_state *state, int arg1, int arg2) {
- state->ctx->xargs_safe = true;
- return parse_nullary_flag(state);
+static struct bfs_expr *parse_xargs_safe(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->xargs_safe = true;
+ return parse_nullary_flag(parser);
}
/**
* Parse -executable, -readable, -writable
*/
-static struct bfs_expr *parse_access(struct parser_state *state, int flag, int arg2) {
- struct bfs_expr *expr = parse_nullary_test(state, eval_access);
- if (!expr) {
- return NULL;
- }
-
- expr->num = flag;
- expr->cost = STAT_COST;
-
- switch (flag) {
- case R_OK:
- expr->probability = 0.99;
- break;
- case W_OK:
- expr->probability = 0.8;
- break;
- case X_OK:
- expr->probability = 0.2;
- break;
+static struct bfs_expr *parse_access(struct bfs_parser *parser, int flag, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_access);
+ if (expr) {
+ expr->num = flag;
}
-
return expr;
}
/**
* Parse -acl.
*/
-static struct bfs_expr *parse_acl(struct parser_state *state, int flag, int arg2) {
+static struct bfs_expr *parse_acl(struct bfs_parser *parser, int flag, int arg2) {
#if BFS_CAN_CHECK_ACL
- struct bfs_expr *expr = parse_nullary_test(state, eval_acl);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.00002;
- }
- return expr;
+ return parse_nullary_test(parser, eval_acl);
#else
- parse_error(state, "Missing platform support.\n");
+ parse_error(parser, "Missing platform support.\n");
return NULL;
#endif
}
@@ -1066,38 +936,32 @@ static struct bfs_expr *parse_acl(struct parser_state *state, int flag, int arg2
/**
* Parse -[aBcm]?newer.
*/
-static struct bfs_expr *parse_newer(struct parser_state *state, int field, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_newer);
+static struct bfs_expr *parse_newer(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
if (!expr) {
return NULL;
}
struct bfs_stat sb;
- if (stat_arg(state, &expr->argv[1], &sb) != 0) {
- goto fail;
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
+ return NULL;
}
- expr->cost = STAT_COST;
expr->reftime = sb.mtime;
expr->stat_field = field;
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -[aBcm]min.
*/
-static struct bfs_expr *parse_min(struct parser_state *state, int field, int arg2) {
- struct bfs_expr *expr = parse_test_icmp(state, eval_time);
+static struct bfs_expr *parse_min(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_test_icmp(parser, eval_time);
if (!expr) {
return NULL;
}
- expr->cost = STAT_COST;
- expr->reftime = state->now;
+ expr->reftime = parser->now;
expr->stat_field = field;
expr->time_unit = BFS_MINUTES;
return expr;
@@ -1106,19 +970,18 @@ static struct bfs_expr *parse_min(struct parser_state *state, int field, int arg
/**
* Parse -[aBcm]time.
*/
-static struct bfs_expr *parse_time(struct parser_state *state, int field, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_time);
+static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_time);
if (!expr) {
return NULL;
}
- expr->cost = STAT_COST;
- expr->reftime = state->now;
+ expr->reftime = parser->now;
expr->stat_field = field;
- const char *tail = parse_icmp(state, expr, IF_PARTIAL_OK);
+ const char *tail = parse_icmp(parser, expr, IF_PARTIAL_OK);
if (!tail) {
- goto fail;
+ return NULL;
}
if (!*tail) {
@@ -1133,21 +996,21 @@ static struct bfs_expr *parse_time(struct parser_state *state, int field, int ar
switch (*tail) {
case 'w':
time *= 7;
- BFS_FALLTHROUGH;
+ fallthru;
case 'd':
time *= 24;
- BFS_FALLTHROUGH;
+ fallthru;
case 'h':
time *= 60;
- BFS_FALLTHROUGH;
+ fallthru;
case 'm':
time *= 60;
- BFS_FALLTHROUGH;
+ fallthru;
case 's':
break;
default:
- parse_expr_error(state, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail);
- goto fail;
+ parse_expr_error(parser, expr, "Unknown time unit ${bld}%c${rs}.\n", *tail);
+ return NULL;
}
expr->num += time;
@@ -1156,37 +1019,28 @@ static struct bfs_expr *parse_time(struct parser_state *state, int field, int ar
break;
}
- tail = parse_int(state, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED);
+ tail = parse_int(parser, &expr->argv[1], tail, &time, IF_PARTIAL_OK | IF_LONG_LONG | IF_UNSIGNED);
if (!tail) {
- goto fail;
+ return NULL;
}
if (!*tail) {
- parse_expr_error(state, expr, "Missing time unit.\n");
- goto fail;
+ parse_expr_error(parser, expr, "Missing time unit.\n");
+ return NULL;
}
}
expr->time_unit = BFS_SECONDS;
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -capable.
*/
-static struct bfs_expr *parse_capable(struct parser_state *state, int flag, int arg2) {
+static struct bfs_expr *parse_capable(struct bfs_parser *parser, int flag, int arg2) {
#if BFS_CAN_CHECK_CAPABILITIES
- struct bfs_expr *expr = parse_nullary_test(state, eval_capable);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.000002;
- }
- return expr;
+ return parse_nullary_test(parser, eval_capable);
#else
- parse_error(state, "Missing platform support.\n");
+ parse_error(parser, "Missing platform support.\n");
return NULL;
#endif
}
@@ -1194,47 +1048,112 @@ static struct bfs_expr *parse_capable(struct parser_state *state, int flag, int
/**
* Parse -(no)?color.
*/
-static struct bfs_expr *parse_color(struct parser_state *state, int color, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
+static struct bfs_expr *parse_color(struct bfs_parser *parser, int color, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ struct bfs_ctx *ctx = parser->ctx;
struct colors *colors = ctx->colors;
if (color) {
if (!colors) {
- parse_error(state, "%s.\n", strerror(ctx->colors_error));
+ parse_expr_error(parser, expr, "Error parsing $$LS_COLORS: %s.\n", xstrerror(ctx->colors_error));
return NULL;
}
- state->use_color = COLOR_ALWAYS;
+ parser->use_color = COLOR_ALWAYS;
ctx->cout->colors = colors;
ctx->cerr->colors = colors;
} else {
- state->use_color = COLOR_NEVER;
+ parser->use_color = COLOR_NEVER;
ctx->cout->colors = NULL;
ctx->cerr->colors = NULL;
}
- return parse_nullary_option(state);
+ return expr;
+}
+
+/**
+ * Common code for fnmatch() tests.
+ */
+static struct bfs_expr *parse_fnmatch(const struct bfs_parser *parser, struct bfs_expr *expr, bool casefold) {
+ if (!expr) {
+ return NULL;
+ }
+
+ expr->pattern = expr->argv[1];
+
+ if (casefold) {
+#ifdef FNM_CASEFOLD
+ expr->fnm_flags = FNM_CASEFOLD;
+#else
+ parse_expr_error(parser, expr, "Missing platform support.\n");
+ return NULL;
+#endif
+ } else {
+ expr->fnm_flags = 0;
+ }
+
+ // POSIX says, about fnmatch():
+ //
+ // If pattern ends with an unescaped <backslash>, fnmatch() shall
+ // return a non-zero value (indicating either no match or an error).
+ //
+ // But not all implementations obey this, so check for it ourselves.
+ size_t i, len = strlen(expr->pattern);
+ for (i = 0; i < len; ++i) {
+ if (expr->pattern[len - i - 1] != '\\') {
+ break;
+ }
+ }
+ if (i % 2 != 0) {
+ parse_expr_warning(parser, expr, "Unescaped trailing backslash.\n\n");
+ expr->eval_fn = eval_false;
+ return expr;
+ }
+
+ // strcmp() can be much faster than fnmatch() since it doesn't have to
+ // parse the pattern, so special-case patterns with no wildcards.
+ //
+ // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
+ expr->literal = strcspn(expr->pattern, "?*\\[") == len;
+
+ return expr;
+}
+
+/**
+ * Parse -context.
+ */
+static struct bfs_expr *parse_context(struct bfs_parser *parser, int flag, int arg2) {
+#if BFS_CAN_CHECK_CONTEXT
+ struct bfs_expr *expr = parse_unary_test(parser, eval_context);
+ return parse_fnmatch(parser, expr, false);
+#else
+ parse_error(parser, "Missing platform support.\n");
+ return NULL;
+#endif
}
/**
* Parse -{false,true}.
*/
-static struct bfs_expr *parse_const(struct parser_state *state, int value, int arg2) {
- parser_advance(state, T_TEST, 1);
- return value ? &bfs_true : &bfs_false;
+static struct bfs_expr *parse_const(struct bfs_parser *parser, int value, int arg2) {
+ return parse_nullary_test(parser, value ? eval_true : eval_false);
}
/**
* Parse -daystart.
*/
-static struct bfs_expr *parse_daystart(struct parser_state *state, int arg1, int arg2) {
+static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int arg2) {
struct tm tm;
- if (xlocaltime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xlocaltime()");
+ if (!localtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "localtime_r()");
return NULL;
}
- if (tm.tm_hour || tm.tm_min || tm.tm_sec || state->now.tv_nsec) {
+ if (tm.tm_hour || tm.tm_min || tm.tm_sec || parser->now.tv_nsec) {
++tm.tm_mday;
}
tm.tm_hour = 0;
@@ -1243,100 +1162,87 @@ static struct bfs_expr *parse_daystart(struct parser_state *state, int arg1, int
time_t time;
if (xmktime(&tm, &time) != 0) {
- parse_perror(state, "xmktime()");
+ parse_perror(parser, "xmktime()");
return NULL;
}
- state->now.tv_sec = time;
- state->now.tv_nsec = 0;
+ parser->now.tv_sec = time;
+ parser->now.tv_nsec = 0;
- return parse_nullary_option(state);
+ return parse_nullary_option(parser);
}
/**
* Parse -delete.
*/
-static struct bfs_expr *parse_delete(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_POST_ORDER;
- state->depth_arg = state->argv;
- return parse_nullary_action(state, eval_delete);
+static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->flags |= BFTW_POST_ORDER;
+ parser->depth_arg = parser->argv;
+ return parse_nullary_action(parser, eval_delete);
}
/**
* Parse -d.
*/
-static struct bfs_expr *parse_depth(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_POST_ORDER;
- state->depth_arg = state->argv;
- return parse_nullary_flag(state);
+static struct bfs_expr *parse_depth(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->flags |= BFTW_POST_ORDER;
+ parser->depth_arg = parser->argv;
+ return parse_nullary_flag(parser);
}
/**
* Parse -depth [N].
*/
-static struct bfs_expr *parse_depth_n(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[1];
+static struct bfs_expr *parse_depth_n(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[1];
if (arg && looks_like_icmp(arg)) {
- return parse_test_icmp(state, eval_depth);
+ return parse_test_icmp(parser, eval_depth);
} else {
- return parse_depth(state, arg1, arg2);
+ return parse_depth(parser, arg1, arg2);
}
}
/**
* Parse -{min,max}depth N.
*/
-static struct bfs_expr *parse_depth_limit(struct parser_state *state, int is_min, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
- const char *arg = state->argv[0];
- const char *value = state->argv[1];
- if (!value) {
- parse_error(state, "${blu}%s${rs} needs a value.\n", arg);
+static struct bfs_expr *parse_depth_limit(struct bfs_parser *parser, int is_min, int arg2) {
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
return NULL;
}
+ struct bfs_ctx *ctx = parser->ctx;
int *depth = is_min ? &ctx->mindepth : &ctx->maxdepth;
- if (!parse_int(state, &state->argv[1], value, depth, IF_INT | IF_UNSIGNED)) {
+ char **arg = &expr->argv[1];
+ if (!parse_int(parser, arg, *arg, depth, IF_INT | IF_UNSIGNED)) {
return NULL;
}
- return parse_unary_option(state);
+ return expr;
}
/**
* Parse -empty.
*/
-static struct bfs_expr *parse_empty(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_test(state, eval_empty);
- if (!expr) {
- return NULL;
- }
-
- expr->cost = 2000.0;
- expr->probability = 0.01;
-
- if (state->ctx->optlevel < 4) {
- // Since -empty attempts to open and read directories, it may
- // have side effects such as reporting permission errors, and
- // thus shouldn't be re-ordered without aggressive optimizations
- expr->pure = false;
+static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_empty);
+ if (expr) {
+ // For opendir()
+ expr->ephemeral_fds = 1;
}
-
- expr->ephemeral_fds = 1;
-
return expr;
}
/**
* Parse -exec(dir)?/-ok(dir)?.
*/
-static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int arg2) {
- struct bfs_exec *execbuf = bfs_exec_parse(state->ctx, state->argv, flags);
+static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg2) {
+ struct bfs_exec *execbuf = bfs_exec_parse(parser->ctx, parser->argv, flags);
if (!execbuf) {
return NULL;
}
- struct bfs_expr *expr = parse_action(state, eval_exec, execbuf->tmpl_argc + 2);
+ struct bfs_expr *expr = parse_action(parser, eval_exec, execbuf->tmpl_argc + 2);
if (!expr) {
bfs_exec_free(execbuf);
return NULL;
@@ -1344,23 +1250,42 @@ static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int ar
expr->exec = execbuf;
- if (execbuf->flags & BFS_EXEC_MULTI) {
- expr_set_always_true(expr);
- } else {
- expr->cost = 1000000.0;
- }
-
+ // For pipe() in bfs_spawn()
expr->ephemeral_fds = 2;
+
if (execbuf->flags & BFS_EXEC_CHDIR) {
+ // Check for relative paths in $PATH
+ const char *path = getenv("PATH");
+ while (path) {
+ if (*path != '/') {
+ size_t len = strcspn(path, ":");
+ char *comp = strndup(path, len);
+ if (comp) {
+ parse_expr_error(parser, expr,
+ "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp);
+ free(comp);
+ } else {
+ parse_perror(parser, "strndup()");
+ }
+ return NULL;
+ }
+
+ path = strchr(path, ':');
+ if (path) {
+ ++path;
+ }
+ }
+
+ // To dup() the parent directory
if (execbuf->flags & BFS_EXEC_MULTI) {
- expr->persistent_fds = 1;
+ ++expr->persistent_fds;
} else {
++expr->ephemeral_fds;
}
}
if (execbuf->flags & BFS_EXEC_CONFIRM) {
- state->ok_expr = expr;
+ parser->ok_expr = expr;
}
return expr;
@@ -1369,18 +1294,17 @@ static struct bfs_expr *parse_exec(struct parser_state *state, int flags, int ar
/**
* Parse -exit [STATUS].
*/
-static struct bfs_expr *parse_exit(struct parser_state *state, int arg1, int arg2) {
+static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2) {
size_t argc = 1;
- const char *value = state->argv[1];
+ const char *value = parser->argv[1];
int status = EXIT_SUCCESS;
- if (value && parse_int(state, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ if (value && parse_int(parser, NULL, value, &status, IF_INT | IF_UNSIGNED | IF_QUIET)) {
argc = 2;
}
- struct bfs_expr *expr = parse_action(state, eval_exit, argc);
+ struct bfs_expr *expr = parse_action(parser, eval_exit, argc);
if (expr) {
- expr_set_never_returns(expr);
expr->num = status;
}
return expr;
@@ -1389,34 +1313,29 @@ static struct bfs_expr *parse_exit(struct parser_state *state, int arg1, int arg
/**
* Parse -f PATH.
*/
-static struct bfs_expr *parse_f(struct parser_state *state, int arg1, int arg2) {
- const char *path = state->argv[1];
- if (!path) {
- parse_error(state, "${cyn}-f${rs} requires a path.\n");
+static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_flag(parser);
+ if (!expr) {
return NULL;
}
- if (parse_root(state, path) != 0) {
+ if (parse_root(parser, expr->argv[1]) != 0) {
return NULL;
}
- parser_advance(state, T_FLAG, 1);
- parser_advance(state, T_PATH, 1);
- return &bfs_true;
+ return expr;
}
/**
* Parse -files0-from PATH.
*/
-static struct bfs_expr *parse_files0_from(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
- const char *from = state->argv[1];
- if (!from) {
- parse_error(state, "${blu}%s${rs} requires a path.\n", arg);
+static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
return NULL;
}
- state->files0_arg = parser_advance(state, T_OPTION, 1);
+ const char *from = expr->argv[1];
FILE *file;
if (strcmp(from, "-") == 0) {
@@ -1425,46 +1344,48 @@ static struct bfs_expr *parse_files0_from(struct parser_state *state, int arg1,
file = xfopen(from, O_RDONLY | O_CLOEXEC);
}
if (!file) {
- parse_error(state, "%m.\n");
+ parse_expr_error(parser, expr, "%m.\n");
return NULL;
}
- struct bfs_expr *expr = &bfs_true;
-
while (true) {
char *path = xgetdelim(file, '\0');
if (!path) {
if (errno) {
- parse_error(state, "%m.\n");
- expr = NULL;
+ goto fail;
+ } else {
+ break;
}
- break;
}
- int ret = parse_root(state, path);
+ int ret = parse_root(parser, path);
free(path);
if (ret != 0) {
- expr = NULL;
- break;
+ goto fail;
}
}
if (file == stdin) {
- state->files0_stdin_arg = state->files0_arg;
+ parser->files0_stdin_arg = expr->argv;
} else {
fclose(file);
}
- state->implicit_root = false;
- parser_advance(state, T_OPTION, 1);
+ parser->implicit_root = false;
return expr;
+
+fail:
+ if (file != stdin) {
+ fclose(file);
+ }
+ return NULL;
}
/**
* Parse -flags FLAGS.
*/
-static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_flags);
+static struct bfs_expr *parse_flags(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_flags);
if (!expr) {
return NULL;
}
@@ -1486,11 +1407,10 @@ static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int ar
if (xstrtofflags(&flags, &expr->set_flags, &expr->clear_flags) != 0) {
if (errno == ENOTSUP) {
- parse_expr_error(state, expr, "Missing platform support.\n");
+ parse_expr_error(parser, expr, "Missing platform support.\n");
} else {
- parse_expr_error(state, expr, "Invalid flags.\n");
+ parse_expr_error(parser, expr, "Invalid flags.\n");
}
- bfs_expr_free(expr);
return NULL;
}
@@ -1500,371 +1420,300 @@ static struct bfs_expr *parse_flags(struct parser_state *state, int arg1, int ar
/**
* Parse -fls FILE.
*/
-static struct bfs_expr *parse_fls(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_action(state, eval_fls);
+static struct bfs_expr *parse_fls(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fls);
if (!expr) {
- goto fail;
+ return NULL;
}
- if (expr_open(state, expr, expr->argv[1]) != 0) {
- goto fail;
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
}
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- expr->reftime = state->now;
-
- // We'll need these for user/group names, so initialize them now to
- // avoid EMFILE later
- bfs_ctx_users(state->ctx);
- bfs_ctx_groups(state->ctx);
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -fprint FILE.
*/
-static struct bfs_expr *parse_fprint(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_action(state, eval_fprint);
- if (expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- if (expr_open(state, expr, expr->argv[1]) != 0) {
- goto fail;
- }
+static struct bfs_expr *parse_fprint(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprint);
+ if (!expr) {
+ return NULL;
}
- return expr;
-fail:
- bfs_expr_free(expr);
- return NULL;
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
}
/**
* Parse -fprint0 FILE.
*/
-static struct bfs_expr *parse_fprint0(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_action(state, eval_fprint0);
- if (expr) {
- expr_set_always_true(expr);
- expr->cost = PRINT_COST;
- if (expr_open(state, expr, expr->argv[1]) != 0) {
- goto fail;
- }
+static struct bfs_expr *parse_fprint0(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprint0);
+ if (!expr) {
+ return NULL;
}
- return expr;
-fail:
- bfs_expr_free(expr);
- return NULL;
+ if (expr_open(parser, expr, expr->argv[1]) != 0) {
+ return NULL;
+ }
+
+ return expr;
}
/**
* Parse -fprintf FILE FORMAT.
*/
-static struct bfs_expr *parse_fprintf(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
+static struct bfs_expr *parse_fprintf(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[0];
- const char *file = state->argv[1];
+ const char *file = parser->argv[1];
if (!file) {
- parse_error(state, "${blu}%s${rs} needs a file.\n", arg);
+ parse_error(parser, "${blu}%s${rs} needs a file.\n", arg);
return NULL;
}
- const char *format = state->argv[2];
+ const char *format = parser->argv[2];
if (!format) {
- parse_error(state, "${blu}%s${rs} needs a format string.\n", arg);
+ parse_error(parser, "${blu}%s${rs} needs a format string.\n", arg);
return NULL;
}
- struct bfs_expr *expr = parse_action(state, eval_fprintf, 3);
+ struct bfs_expr *expr = parse_action(parser, eval_fprintf, 3);
if (!expr) {
return NULL;
}
- expr_set_always_true(expr);
-
- expr->cost = PRINT_COST;
-
- if (expr_open(state, expr, file) != 0) {
- goto fail;
+ if (expr_open(parser, expr, file) != 0) {
+ return NULL;
}
- if (bfs_printf_parse(state->ctx, expr, format) != 0) {
- goto fail;
+ if (bfs_printf_parse(parser->ctx, expr, format) != 0) {
+ return NULL;
}
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -fstype TYPE.
*/
-static struct bfs_expr *parse_fstype(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_fstype);
+static struct bfs_expr *parse_fstype(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_fstype);
if (!expr) {
return NULL;
}
- if (!bfs_ctx_mtab(state->ctx)) {
- parse_expr_error(state, expr, "Couldn't parse the mount table: %m.\n");
- bfs_expr_free(expr);
+ if (!bfs_ctx_mtab(parser->ctx)) {
+ parse_expr_error(parser, expr, "Couldn't parse the mount table: %m.\n");
return NULL;
}
- expr->cost = STAT_COST;
return expr;
}
/**
* Parse -gid/-group.
*/
-static struct bfs_expr *parse_group(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_gid);
+static struct bfs_expr *parse_group(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_gid);
if (!expr) {
return NULL;
}
- const struct bfs_groups *groups = bfs_ctx_groups(state->ctx);
- if (!groups) {
- parse_expr_error(state, expr, "Couldn't parse the group table: %m.\n");
- goto fail;
- }
-
- const struct group *grp = bfs_getgrnam(groups, expr->argv[1]);
+ const struct group *grp = bfs_getgrnam(parser->ctx->groups, expr->argv[1]);
if (grp) {
expr->num = grp->gr_gid;
expr->int_cmp = BFS_INT_EQUAL;
} else if (looks_like_icmp(expr->argv[1])) {
- if (!parse_icmp(state, expr, 0)) {
- goto fail;
+ if (!parse_icmp(parser, expr, 0)) {
+ return NULL;
}
+ } else if (errno) {
+ parse_expr_error(parser, expr, "%m.\n");
+ return NULL;
} else {
- parse_expr_error(state, expr, "No such group.\n");
- goto fail;
+ parse_expr_error(parser, expr, "No such group.\n");
+ return NULL;
}
- expr->cost = STAT_COST;
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -unique.
*/
-static struct bfs_expr *parse_unique(struct parser_state *state, int arg1, int arg2) {
- state->ctx->unique = true;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_unique(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->unique = true;
+ return parse_nullary_option(parser);
}
/**
* Parse -used N.
*/
-static struct bfs_expr *parse_used(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_test_icmp(state, eval_used);
- if (expr) {
- expr->cost = STAT_COST;
- }
- return expr;
+static struct bfs_expr *parse_used(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_used);
}
/**
* Parse -uid/-user.
*/
-static struct bfs_expr *parse_user(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_uid);
+static struct bfs_expr *parse_user(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_uid);
if (!expr) {
return NULL;
}
- const struct bfs_users *users = bfs_ctx_users(state->ctx);
- if (!users) {
- parse_expr_error(state, expr, "Couldn't parse the user table: %m.\n");
- goto fail;
- }
-
- const struct passwd *pwd = bfs_getpwnam(users, expr->argv[1]);
+ const struct passwd *pwd = bfs_getpwnam(parser->ctx->users, expr->argv[1]);
if (pwd) {
expr->num = pwd->pw_uid;
expr->int_cmp = BFS_INT_EQUAL;
} else if (looks_like_icmp(expr->argv[1])) {
- if (!parse_icmp(state, expr, 0)) {
- goto fail;
+ if (!parse_icmp(parser, expr, 0)) {
+ return NULL;
}
+ } else if (errno) {
+ parse_expr_error(parser, expr, "%m.\n");
+ return NULL;
} else {
- parse_expr_error(state, expr, "No such user.\n");
- goto fail;
+ parse_expr_error(parser, expr, "No such user.\n");
+ return NULL;
}
- expr->cost = STAT_COST;
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -hidden.
*/
-static struct bfs_expr *parse_hidden(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_test(state, eval_hidden);
- if (expr) {
- expr->probability = 0.01;
- }
- return expr;
+static struct bfs_expr *parse_hidden(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_test(parser, eval_hidden);
}
/**
* Parse -(no)?ignore_readdir_race.
*/
-static struct bfs_expr *parse_ignore_races(struct parser_state *state, int ignore, int arg2) {
- state->ctx->ignore_races = ignore;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_ignore_races(struct bfs_parser *parser, int ignore, int arg2) {
+ parser->ctx->ignore_races = ignore;
+ return parse_nullary_option(parser);
}
/**
* Parse -inum N.
*/
-static struct bfs_expr *parse_inum(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_test_icmp(state, eval_inum);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50;
- }
- return expr;
+static struct bfs_expr *parse_inum(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_inum);
}
/**
- * Parse -links N.
+ * Parse -j<n>.
*/
-static struct bfs_expr *parse_links(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_test_icmp(state, eval_links);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = bfs_expr_cmp(expr, 1) ? 0.99 : 0.01;
+static struct bfs_expr *parse_jobs(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_flag(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ unsigned int n;
+ if (!parse_int(parser, expr->argv, expr->argv[0] + 2, &n, IF_INT | IF_UNSIGNED)) {
+ return NULL;
}
+
+ if (n == 0) {
+ parse_expr_error(parser, expr, "${bld}0${rs} is not enough threads.\n");
+ return NULL;
+ }
+
+ parser->ctx->threads = n;
return expr;
}
/**
- * Parse -ls.
+ * Parse -limit N.
*/
-static struct bfs_expr *parse_ls(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_action(state, eval_fls);
+static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_limit);
if (!expr) {
return NULL;
}
- init_print_expr(state, expr);
- expr->reftime = state->now;
+ char **arg = &expr->argv[1];
+ if (!parse_int(parser, arg, *arg, &expr->num, IF_LONG_LONG)) {
+ return NULL;
+ }
- // We'll need these for user/group names, so initialize them now to
- // avoid EMFILE later
- bfs_ctx_users(state->ctx);
- bfs_ctx_groups(state->ctx);
+ if (expr->num <= 0) {
+ parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]);
+ return NULL;
+ }
+ parser->limit_arg = expr->argv;
return expr;
}
/**
- * Parse -mount.
+ * Parse -links N.
*/
-static struct bfs_expr *parse_mount(struct parser_state *state, int arg1, int arg2) {
- parse_warning(state, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", state->argv[0]);
- bfs_warning(state->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n");
-
- state->ctx->flags |= BFTW_PRUNE_MOUNTS;
- state->mount_arg = state->argv;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_links(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_test_icmp(parser, eval_links);
}
/**
- * Common code for fnmatch() tests.
+ * Parse -ls.
*/
-static struct bfs_expr *parse_fnmatch(const struct parser_state *state, struct bfs_expr *expr, bool casefold) {
+static struct bfs_expr *parse_ls(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fls);
if (!expr) {
return NULL;
}
- if (casefold) {
-#ifdef FNM_CASEFOLD
- expr->num = FNM_CASEFOLD;
-#else
- parse_expr_error(state, expr, "Missing platform support.\n");
- bfs_expr_free(expr);
- return NULL;
-#endif
- } else {
- expr->num = 0;
- }
+ init_print_expr(parser, expr);
+ return expr;
+}
- // POSIX says, about fnmatch():
- //
- // If pattern ends with an unescaped <backslash>, fnmatch() shall
- // return a non-zero value (indicating either no match or an error).
- //
- // But not all implementations obey this, so check for it ourselves.
- const char *pattern = expr->argv[1];
- size_t i, len = strlen(pattern);
- for (i = 0; i < len; ++i) {
- if (pattern[len - i - 1] != '\\') {
- break;
- }
- }
- if (i % 2 != 0) {
- parse_expr_warning(state, expr, "Unescaped trailing backslash.\n\n");
- bfs_expr_free(expr);
- return &bfs_false;
+/**
+ * Parse -mount.
+ */
+static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
}
- expr->cost = 400.0;
-
- if (strchr(pattern, '*')) {
- expr->probability = 0.5;
- } else {
- expr->probability = 0.1;
- }
+ parse_expr_warning(parser, expr, "In the future, ${blu}%s${rs} will skip mount points entirely, unlike\n", expr->argv[0]);
+ bfs_warning(parser->ctx, "${blu}-xdev${rs}, due to http://austingroupbugs.net/view.php?id=1133.\n\n");
+ parser->ctx->flags |= BFTW_PRUNE_MOUNTS;
+ parser->mount_arg = expr->argv;
return expr;
}
/**
* Parse -i?name.
*/
-static struct bfs_expr *parse_name(struct parser_state *state, int casefold, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_name);
- return parse_fnmatch(state, expr, casefold);
+static struct bfs_expr *parse_name(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_name);
+ return parse_fnmatch(parser, expr, casefold);
}
/**
* Parse -i?path, -i?wholename.
*/
-static struct bfs_expr *parse_path(struct parser_state *state, int casefold, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_path);
- return parse_fnmatch(state, expr, casefold);
+static struct bfs_expr *parse_path(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_path);
+ return parse_fnmatch(parser, expr, casefold);
}
/**
* Parse -i?lname.
*/
-static struct bfs_expr *parse_lname(struct parser_state *state, int casefold, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_lname);
- return parse_fnmatch(state, expr, casefold);
+static struct bfs_expr *parse_lname(struct bfs_parser *parser, int casefold, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_lname);
+ return parse_fnmatch(parser, expr, casefold);
}
/** Get the bfs_stat_field for X/Y in -newerXY. */
@@ -1884,20 +1733,20 @@ static enum bfs_stat_field parse_newerxy_field(char c) {
}
/** Parse an explicit reference timestamp for -newerXt and -*since. */
-static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr) {
- if (parse_timestamp(expr->argv[1], &expr->reftime) == 0) {
+static int parse_reftime(const struct bfs_parser *parser, struct bfs_expr *expr) {
+ if (xgetdate(expr->argv[1], &expr->reftime) == 0) {
return 0;
} else if (errno != EINVAL) {
- parse_expr_error(state, expr, "%m.\n");
+ parse_expr_error(parser, expr, "%m.\n");
return -1;
}
- parse_expr_error(state, expr, "Invalid timestamp.\n\n");
+ parse_expr_error(parser, expr, "Invalid timestamp.\n\n");
fprintf(stderr, "Supported timestamp formats are ISO 8601-like, e.g.\n\n");
struct tm tm;
- if (xlocaltime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xlocaltime()");
+ if (!localtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "localtime_r()");
return -1;
}
@@ -1906,18 +1755,18 @@ static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr
fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday);
fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
-#if __FreeBSD__
+#if BFS_HAS_TM_GMTOFF
int gmtoff = tm.tm_gmtoff;
#else
int gmtoff = -timezone;
#endif
- int tz_hour = gmtoff/3600;
- int tz_min = (labs(gmtoff)/60)%60;
+ int tz_hour = gmtoff / 3600;
+ int tz_min = (labs(gmtoff) / 60) % 60;
fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n",
- year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min);
+ year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min);
- if (xgmtime(&state->now.tv_sec, &tm) != 0) {
- parse_perror(state, "xgmtime()");
+ if (!gmtime_r(&parser->now.tv_sec, &tm)) {
+ parse_perror(parser, "gmtime_r()");
return -1;
}
@@ -1931,76 +1780,64 @@ static int parse_reftime(const struct parser_state *state, struct bfs_expr *expr
/**
* Parse -newerXY.
*/
-static struct bfs_expr *parse_newerxy(struct parser_state *state, int arg1, int arg2) {
- const char *arg = state->argv[0];
+static struct bfs_expr *parse_newerxy(struct bfs_parser *parser, int arg1, int arg2) {
+ const char *arg = parser->argv[0];
if (strlen(arg) != 8) {
- parse_error(state, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%s${rs}.\n", arg + 6);
+ parse_error(parser, "Expected ${blu}-newer${bld}XY${rs}; found ${blu}-newer${bld}%pq${rs}.\n", arg + 6);
return NULL;
}
- struct bfs_expr *expr = parse_unary_test(state, eval_newer);
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
if (!expr) {
- goto fail;
+ return NULL;
}
expr->stat_field = parse_newerxy_field(arg[6]);
if (!expr->stat_field) {
- parse_expr_error(state, expr,
- "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n",
- arg[6]);
- goto fail;
+ parse_expr_error(parser, expr,
+ "For ${blu}-newer${bld}XY${rs}, ${bld}X${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, or ${bld}B${rs}, not ${err}%c${rs}.\n",
+ arg[6]);
+ return NULL;
}
if (arg[7] == 't') {
- if (parse_reftime(state, expr) != 0) {
- goto fail;
+ if (parse_reftime(parser, expr) != 0) {
+ return NULL;
}
} else {
enum bfs_stat_field field = parse_newerxy_field(arg[7]);
if (!field) {
- parse_expr_error(state, expr,
- "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n",
- arg[7]);
- goto fail;
+ parse_expr_error(parser, expr,
+ "For ${blu}-newer${bld}XY${rs}, ${bld}Y${rs} should be ${bld}a${rs}, ${bld}c${rs}, ${bld}m${rs}, ${bld}B${rs}, or ${bld}t${rs}, not ${err}%c${rs}.\n",
+ arg[7]);
+ return NULL;
}
struct bfs_stat sb;
- if (stat_arg(state, &expr->argv[1], &sb) != 0) {
- goto fail;
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
+ return NULL;
}
-
const struct timespec *reftime = bfs_stat_time(&sb, field);
if (!reftime) {
- parse_expr_error(state, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field));
- goto fail;
+ parse_expr_error(parser, expr, "Couldn't get file %s.\n", bfs_stat_field_name(field));
+ return NULL;
}
expr->reftime = *reftime;
}
- expr->cost = STAT_COST;
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -nogroup.
*/
-static struct bfs_expr *parse_nogroup(struct parser_state *state, int arg1, int arg2) {
- if (!bfs_ctx_groups(state->ctx)) {
- parse_error(state, "Couldn't parse the group table: %m.\n");
- return NULL;
- }
-
- struct bfs_expr *expr = parse_nullary_test(state, eval_nogroup);
+static struct bfs_expr *parse_nogroup(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_nogroup);
if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
+ // Who knows how many FDs getgrgid_r() needs?
+ expr->ephemeral_fds = 3;
}
return expr;
}
@@ -2008,45 +1845,33 @@ static struct bfs_expr *parse_nogroup(struct parser_state *state, int arg1, int
/**
* Parse -nohidden.
*/
-static struct bfs_expr *parse_nohidden(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *hidden = bfs_expr_new(eval_hidden, 1, &fake_hidden_arg);
+static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *hidden = parse_new_expr(parser, eval_hidden, 1, &fake_hidden_arg);
if (!hidden) {
return NULL;
}
- hidden->probability = 0.01;
- hidden->pure = true;
- hidden->synthetic = true;
-
- if (parse_exclude(state, hidden) != 0) {
- return NULL;
- }
-
- parser_advance(state, T_OPTION, 1);
- return &bfs_true;
+ bfs_expr_append(parser->ctx->exclude, hidden);
+ return parse_nullary_option(parser);
}
/**
* Parse -noleaf.
*/
-static struct bfs_expr *parse_noleaf(struct parser_state *state, int arg1, int arg2) {
- parse_warning(state, "${ex}bfs${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n", state->argv[0]);
- return parse_nullary_option(state);
+static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) {
+ parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n",
+ BFS_COMMAND, parser->argv[0]);
+ return parse_nullary_option(parser);
}
/**
* Parse -nouser.
*/
-static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int arg2) {
- if (!bfs_ctx_users(state->ctx)) {
- parse_error(state, "Couldn't parse the user table: %m.\n");
- return NULL;
- }
-
- struct bfs_expr *expr = parse_nullary_test(state, eval_nouser);
+static struct bfs_expr *parse_nouser(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_test(parser, eval_nouser);
if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
+ // Who knows how many FDs getpwuid_r() needs?
+ expr->ephemeral_fds = 3;
}
return expr;
}
@@ -2054,10 +1879,10 @@ static struct bfs_expr *parse_nouser(struct parser_state *state, int arg1, int a
/**
* Parse a permission mode like chmod(1).
*/
-static int parse_mode(const struct parser_state *state, const char *mode, struct bfs_expr *expr) {
+static int parse_mode(const struct bfs_parser *parser, const char *mode, struct bfs_expr *expr) {
if (mode[0] >= '0' && mode[0] <= '9') {
unsigned int parsed;
- if (!parse_int(state, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
+ if (!parse_int(parser, NULL, mode, &parsed, 8 | IF_INT | IF_UNSIGNED | IF_QUIET)) {
goto fail;
}
if (parsed > 07777) {
@@ -2089,7 +1914,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
//
// PERMCOPY : "u" | "g" | "o"
- // State machine state
+ // Parser machine parser
enum {
MODE_CLAUSE,
MODE_WHO,
@@ -2097,25 +1922,25 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
MODE_ACTION_APPLY,
MODE_OP,
MODE_PERM,
- } mstate = MODE_CLAUSE;
+ } mparser = MODE_CLAUSE;
enum {
MODE_PLUS,
MODE_MINUS,
MODE_EQUALS,
- } op;
+ } op uninit(MODE_EQUALS);
- mode_t who;
- mode_t file_change;
- mode_t dir_change;
+ mode_t who uninit(0);
+ mode_t file_change uninit(0);
+ mode_t dir_change uninit(0);
const char *i = mode;
while (true) {
- switch (mstate) {
+ switch (mparser) {
case MODE_CLAUSE:
who = 0;
- mstate = MODE_WHO;
- BFS_FALLTHROUGH;
+ mparser = MODE_WHO;
+ fallthru;
case MODE_WHO:
switch (*i) {
@@ -2132,7 +1957,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
who |= 0777;
break;
default:
- mstate = MODE_ACTION;
+ mparser = MODE_ACTION;
continue;
}
break;
@@ -2142,7 +1967,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
case MODE_EQUALS:
expr->file_mode &= ~who;
expr->dir_mode &= ~who;
- BFS_FALLTHROUGH;
+ fallthru;
case MODE_PLUS:
expr->file_mode |= file_change;
expr->dir_mode |= dir_change;
@@ -2152,7 +1977,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
expr->dir_mode &= ~dir_change;
break;
}
- BFS_FALLTHROUGH;
+ fallthru;
case MODE_ACTION:
if (who == 0) {
@@ -2162,27 +1987,27 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
switch (*i) {
case '+':
op = MODE_PLUS;
- mstate = MODE_OP;
+ mparser = MODE_OP;
break;
case '-':
op = MODE_MINUS;
- mstate = MODE_OP;
+ mparser = MODE_OP;
break;
case '=':
op = MODE_EQUALS;
- mstate = MODE_OP;
+ mparser = MODE_OP;
break;
case ',':
- if (mstate == MODE_ACTION_APPLY) {
- mstate = MODE_CLAUSE;
+ if (mparser == MODE_ACTION_APPLY) {
+ mparser = MODE_CLAUSE;
} else {
goto fail;
}
break;
case '\0':
- if (mstate == MODE_ACTION_APPLY) {
+ if (mparser == MODE_ACTION_APPLY) {
goto done;
} else {
goto fail;
@@ -2211,7 +2036,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
default:
file_change = 0;
dir_change = 0;
- mstate = MODE_PERM;
+ mparser = MODE_PERM;
continue;
}
@@ -2219,7 +2044,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
file_change &= who;
dir_change |= (dir_change << 6) | (dir_change << 3);
dir_change &= who;
- mstate = MODE_ACTION_APPLY;
+ mparser = MODE_ACTION_APPLY;
break;
case MODE_PERM:
@@ -2234,7 +2059,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
break;
case 'x':
file_change |= who & 0111;
- BFS_FALLTHROUGH;
+ fallthru;
case 'X':
dir_change |= who & 0111;
break;
@@ -2255,7 +2080,7 @@ static int parse_mode(const struct parser_state *state, const char *mode, struct
}
break;
default:
- mstate = MODE_ACTION_APPLY;
+ mparser = MODE_ACTION_APPLY;
continue;
}
break;
@@ -2268,15 +2093,15 @@ done:
return 0;
fail:
- parse_expr_error(state, expr, "Invalid mode.\n");
+ parse_expr_error(parser, expr, "Invalid mode.\n");
return -1;
}
/**
* Parse -perm MODE.
*/
-static struct bfs_expr *parse_perm(struct parser_state *state, int field, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_perm);
+static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_perm);
if (!expr) {
return NULL;
}
@@ -2297,32 +2122,26 @@ static struct bfs_expr *parse_perm(struct parser_state *state, int field, int ar
++mode;
break;
}
- BFS_FALLTHROUGH;
+ fallthru;
default:
expr->mode_cmp = BFS_MODE_EQUAL;
break;
}
- if (parse_mode(state, mode, expr) != 0) {
- goto fail;
+ if (parse_mode(parser, mode, expr) != 0) {
+ return NULL;
}
- expr->cost = STAT_COST;
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -print.
*/
-static struct bfs_expr *parse_print(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_action(state, eval_fprint);
+static struct bfs_expr *parse_print(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint);
if (expr) {
- init_print_expr(state, expr);
+ init_print_expr(parser, expr);
}
return expr;
}
@@ -2330,10 +2149,10 @@ static struct bfs_expr *parse_print(struct parser_state *state, int arg1, int ar
/**
* Parse -print0.
*/
-static struct bfs_expr *parse_print0(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_action(state, eval_fprint0);
+static struct bfs_expr *parse_print0(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprint0);
if (expr) {
- init_print_expr(state, expr);
+ init_print_expr(parser, expr);
}
return expr;
}
@@ -2341,16 +2160,15 @@ static struct bfs_expr *parse_print0(struct parser_state *state, int arg1, int a
/**
* Parse -printf FORMAT.
*/
-static struct bfs_expr *parse_printf(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_action(state, eval_fprintf);
+static struct bfs_expr *parse_printf(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_action(parser, eval_fprintf);
if (!expr) {
return NULL;
}
- init_print_expr(state, expr);
+ init_print_expr(parser, expr);
- if (bfs_printf_parse(state->ctx, expr, expr->argv[1]) != 0) {
- bfs_expr_free(expr);
+ if (bfs_printf_parse(parser->ctx, expr, expr->argv[1]) != 0) {
return NULL;
}
@@ -2360,10 +2178,10 @@ static struct bfs_expr *parse_printf(struct parser_state *state, int arg1, int a
/**
* Parse -printx.
*/
-static struct bfs_expr *parse_printx(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_action(state, eval_fprintx);
+static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_fprintx);
if (expr) {
- init_print_expr(state, expr);
+ init_print_expr(parser, expr);
}
return expr;
}
@@ -2371,115 +2189,98 @@ static struct bfs_expr *parse_printx(struct parser_state *state, int arg1, int a
/**
* Parse -prune.
*/
-static struct bfs_expr *parse_prune(struct parser_state *state, int arg1, int arg2) {
- state->prune_arg = state->argv;
-
- struct bfs_expr *expr = parse_nullary_action(state, eval_prune);
- if (expr) {
- expr_set_always_true(expr);
- }
- return expr;
+static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->prune_arg = parser->argv;
+ return parse_nullary_action(parser, eval_prune);
}
/**
* Parse -quit.
*/
-static struct bfs_expr *parse_quit(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_action(state, eval_quit);
- if (expr) {
- expr_set_never_returns(expr);
- }
- return expr;
+static struct bfs_expr *parse_quit(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_action(parser, eval_quit);
}
/**
* Parse -i?regex.
*/
-static struct bfs_expr *parse_regex(struct parser_state *state, int flags, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_regex);
+static struct bfs_expr *parse_regex(struct bfs_parser *parser, int flags, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_regex);
if (!expr) {
- goto fail;
+ return NULL;
}
- if (bfs_regcomp(&expr->regex, expr->argv[1], state->regex_type, flags) != 0) {
- if (!expr->regex) {
- parse_perror(state, "bfs_regcomp()");
- goto fail;
- }
-
- char *str = bfs_regerror(expr->regex);
- if (!str) {
- parse_perror(state, "bfs_regerror()");
- goto fail;
+ if (bfs_regcomp(&expr->regex, expr->argv[1], parser->regex_type, flags) != 0) {
+ if (expr->regex) {
+ char *str = bfs_regerror(expr->regex);
+ if (str) {
+ parse_expr_error(parser, expr, "%s.\n", str);
+ free(str);
+ } else {
+ parse_perror(parser, "bfs_regerror()");
+ }
+ } else {
+ parse_perror(parser, "bfs_regcomp()");
}
- parse_expr_error(state, expr, "%s.\n", str);
- free(str);
- goto fail;
+ return NULL;
}
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -E.
*/
-static struct bfs_expr *parse_regex_extended(struct parser_state *state, int arg1, int arg2) {
- state->regex_type = BFS_REGEX_POSIX_EXTENDED;
- return parse_nullary_flag(state);
+static struct bfs_expr *parse_regex_extended(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
+ return parse_nullary_flag(parser);
}
/**
* Parse -regextype TYPE.
*/
-static struct bfs_expr *parse_regextype(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
+static struct bfs_expr *parse_regextype(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
CFILE *cfile = ctx->cerr;
- const char *arg = state->argv[0];
- const char *type = state->argv[1];
- if (!type) {
- parse_error(state, "${blu}%s${rs} needs a value.\n\n", arg);
+ struct bfs_expr *expr = parse_unary_option(parser);
+ if (!expr) {
+ cfprintf(cfile, "\n");
goto list_types;
}
- parser_advance(state, T_OPTION, 1);
-
// See https://www.gnu.org/software/gnulib/manual/html_node/Predefined-Syntaxes.html
+ const char *type = expr->argv[1];
if (strcmp(type, "posix-basic") == 0
|| strcmp(type, "ed") == 0
|| strcmp(type, "sed") == 0) {
- state->regex_type = BFS_REGEX_POSIX_BASIC;
+ parser->regex_type = BFS_REGEX_POSIX_BASIC;
} else if (strcmp(type, "posix-extended") == 0) {
- state->regex_type = BFS_REGEX_POSIX_EXTENDED;
-#if BFS_WITH_ONIGURUMA
+ parser->regex_type = BFS_REGEX_POSIX_EXTENDED;
+#if BFS_USE_ONIGURUMA
} else if (strcmp(type, "emacs") == 0) {
- state->regex_type = BFS_REGEX_EMACS;
+ parser->regex_type = BFS_REGEX_EMACS;
} else if (strcmp(type, "grep") == 0) {
- state->regex_type = BFS_REGEX_GREP;
+ parser->regex_type = BFS_REGEX_GREP;
#endif
} else if (strcmp(type, "help") == 0) {
- state->just_info = true;
+ parser->just_info = true;
cfile = ctx->cout;
goto list_types;
} else {
- parse_error(state, "Unsupported regex type.\n\n");
+ parse_expr_error(parser, expr, "Unsupported regex type.\n\n");
goto list_types;
}
- parser_advance(state, T_OPTION, 1);
- return &bfs_true;
+ return expr;
list_types:
cfprintf(cfile, "Supported types are:\n\n");
cfprintf(cfile, " ${bld}posix-basic${rs}: POSIX basic regular expressions (BRE)\n");
cfprintf(cfile, " ${bld}posix-extended${rs}: POSIX extended regular expressions (ERE)\n");
cfprintf(cfile, " ${bld}ed${rs}: Like ${grn}ed${rs} (same as ${bld}posix-basic${rs})\n");
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
cfprintf(cfile, " ${bld}emacs${rs}: Like ${grn}emacs${rs}\n");
cfprintf(cfile, " ${bld}grep${rs}: Like ${grn}grep${rs}\n");
#endif
@@ -2490,51 +2291,44 @@ list_types:
/**
* Parse -s.
*/
-static struct bfs_expr *parse_s(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_SORT;
- return parse_nullary_flag(state);
+static struct bfs_expr *parse_s(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->flags |= BFTW_SORT;
+ return parse_nullary_flag(parser);
}
/**
* Parse -samefile FILE.
*/
-static struct bfs_expr *parse_samefile(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_samefile);
+static struct bfs_expr *parse_samefile(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_samefile);
if (!expr) {
return NULL;
}
struct bfs_stat sb;
- if (stat_arg(state, &expr->argv[1], &sb) != 0) {
- bfs_expr_free(expr);
+ if (stat_arg(parser, &expr->argv[1], &sb) != 0) {
return NULL;
}
expr->dev = sb.dev;
expr->ino = sb.ino;
-
- expr->cost = STAT_COST;
- expr->probability = 0.01;
-
return expr;
}
/**
* Parse -S STRATEGY.
*/
-static struct bfs_expr *parse_search_strategy(struct parser_state *state, int arg1, int arg2) {
- struct bfs_ctx *ctx = state->ctx;
+static struct bfs_expr *parse_search_strategy(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
CFILE *cfile = ctx->cerr;
- const char *flag = state->argv[0];
- const char *arg = state->argv[1];
- if (!arg) {
- parse_error(state, "${cyn}%s${rs} needs an argument.\n\n", flag);
+ struct bfs_expr *expr = parse_unary_flag(parser);
+ if (!expr) {
+ cfprintf(cfile, "\n");
goto list_strategies;
}
- parser_advance(state, T_FLAG, 1);
-
+ const char *arg = expr->argv[1];
if (strcmp(arg, "bfs") == 0) {
ctx->strategy = BFTW_BFS;
} else if (strcmp(arg, "dfs") == 0) {
@@ -2544,16 +2338,15 @@ static struct bfs_expr *parse_search_strategy(struct parser_state *state, int ar
} else if (strcmp(arg, "eds") == 0) {
ctx->strategy = BFTW_EDS;
} else if (strcmp(arg, "help") == 0) {
- state->just_info = true;
+ parser->just_info = true;
cfile = ctx->cout;
goto list_strategies;
} else {
- parse_error(state, "Unrecognized search strategy.\n\n");
+ parse_expr_error(parser, expr, "Unrecognized search strategy.\n\n");
goto list_strategies;
}
- parser_advance(state, T_FLAG, 1);
- return &bfs_true;
+ return expr;
list_strategies:
cfprintf(cfile, "Supported search strategies:\n\n");
@@ -2567,37 +2360,32 @@ list_strategies:
/**
* Parse -[aBcm]?since.
*/
-static struct bfs_expr *parse_since(struct parser_state *state, int field, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_newer);
+static struct bfs_expr *parse_since(struct bfs_parser *parser, int field, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_newer);
if (!expr) {
return NULL;
}
- if (parse_reftime(state, expr) != 0) {
- goto fail;
+ if (parse_reftime(parser, expr) != 0) {
+ return NULL;
}
- expr->cost = STAT_COST;
expr->stat_field = field;
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -size N[cwbkMGTP]?.
*/
-static struct bfs_expr *parse_size(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_unary_test(state, eval_size);
+static struct bfs_expr *parse_size(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_unary_test(parser, eval_size);
if (!expr) {
return NULL;
}
- const char *unit = parse_icmp(state, expr, IF_PARTIAL_OK);
+ const char *unit = parse_icmp(parser, expr, IF_PARTIAL_OK);
if (!unit) {
- goto fail;
+ return NULL;
}
if (strlen(unit) > 1) {
@@ -2632,109 +2420,82 @@ static struct bfs_expr *parse_size(struct parser_state *state, int arg1, int arg
break;
default:
- goto bad_unit;
+ bad_unit:
+ parse_expr_error(parser, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%pq${rs}.\n", unit);
+ return NULL;
}
- expr->cost = STAT_COST;
- expr->probability = expr->int_cmp == BFS_INT_EQUAL ? 0.01 : 0.50;
-
return expr;
-
-bad_unit:
- parse_expr_error(state, expr, "Expected a size unit (one of ${bld}cwbkMGTP${rs}); found ${err}%s${rs}.\n", unit);
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -sparse.
*/
-static struct bfs_expr *parse_sparse(struct parser_state *state, int arg1, int arg2) {
- struct bfs_expr *expr = parse_nullary_test(state, eval_sparse);
- if (expr) {
- expr->cost = STAT_COST;
- }
- return expr;
+static struct bfs_expr *parse_sparse(struct bfs_parser *parser, int arg1, int arg2) {
+ return parse_nullary_test(parser, eval_sparse);
}
/**
* Parse -status.
*/
-static struct bfs_expr *parse_status(struct parser_state *state, int arg1, int arg2) {
- state->ctx->status = true;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_status(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->status = true;
+ return parse_nullary_option(parser);
}
/**
* Parse -x?type [bcdpflsD].
*/
-static struct bfs_expr *parse_type(struct parser_state *state, int x, int arg2) {
+static struct bfs_expr *parse_type(struct bfs_parser *parser, int x, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
bfs_eval_fn *eval = x ? eval_xtype : eval_type;
- struct bfs_expr *expr = parse_unary_test(state, eval);
+ struct bfs_expr *expr = parse_unary_test(parser, eval);
if (!expr) {
return NULL;
}
- unsigned int types = 0;
- float probability = 0.0;
+ expr->num = 0;
const char *c = expr->argv[1];
while (true) {
- enum bfs_type type;
- float type_prob;
-
switch (*c) {
case 'b':
- type = BFS_BLK;
- type_prob = 0.00000721183;
+ expr->num |= 1 << BFS_BLK;
break;
case 'c':
- type = BFS_CHR;
- type_prob = 0.0000499855;
+ expr->num |= 1 << BFS_CHR;
break;
case 'd':
- type = BFS_DIR;
- type_prob = 0.114475;
+ expr->num |= 1 << BFS_DIR;
break;
case 'D':
- type = BFS_DOOR;
- type_prob = 0.000001;
+ expr->num |= 1 << BFS_DOOR;
break;
case 'p':
- type = BFS_FIFO;
- type_prob = 0.00000248684;
+ expr->num |= 1 << BFS_FIFO;
break;
case 'f':
- type = BFS_REG;
- type_prob = 0.859772;
+ expr->num |= 1 << BFS_REG;
break;
case 'l':
- type = BFS_LNK;
- type_prob = 0.0256816;
+ expr->num |= 1 << BFS_LNK;
break;
case 's':
- type = BFS_SOCK;
- type_prob = 0.0000116881;
+ expr->num |= 1 << BFS_SOCK;
break;
case 'w':
- type = BFS_WHT;
- type_prob = 0.000001;
+ expr->num |= 1 << BFS_WHT;
+ ctx->flags |= BFTW_WHITEOUTS;
break;
case '\0':
- parse_expr_error(state, expr, "Expected a type flag.\n");
- goto fail;
+ parse_expr_error(parser, expr, "Expected a type flag.\n");
+ return NULL;
default:
- parse_expr_error(state, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c);
- goto fail;
- }
-
- unsigned int flag = 1 << type;
- if (!(types & flag)) {
- types |= flag;
- probability += type_prob;
+ parse_expr_error(parser, expr, "Unknown type flag ${err}%c${rs}; expected one of [${bld}bcdpflsD${rs}].\n", *c);
+ return NULL;
}
++c;
@@ -2744,49 +2505,30 @@ static struct bfs_expr *parse_type(struct parser_state *state, int x, int arg2)
++c;
continue;
} else {
- parse_expr_error(state, expr, "Types must be comma-separated.\n");
- goto fail;
+ parse_expr_error(parser, expr, "Types must be comma-separated.\n");
+ return NULL;
}
}
- expr->num = types;
- expr->probability = probability;
-
- if (x && state->ctx->optlevel < 4) {
- // Since -xtype dereferences symbolic links, it may have side
- // effects such as reporting permission errors, and thus
- // shouldn't be re-ordered without aggressive optimizations
- expr->pure = false;
- }
-
return expr;
-
-fail:
- bfs_expr_free(expr);
- return NULL;
}
/**
* Parse -(no)?warn.
*/
-static struct bfs_expr *parse_warn(struct parser_state *state, int warn, int arg2) {
- state->ctx->warn = warn;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_warn(struct bfs_parser *parser, int warn, int arg2) {
+ parser->ctx->warn = warn;
+ return parse_nullary_option(parser);
}
/**
* Parse -xattr.
*/
-static struct bfs_expr *parse_xattr(struct parser_state *state, int arg1, int arg2) {
+static struct bfs_expr *parse_xattr(struct bfs_parser *parser, int arg1, int arg2) {
#if BFS_CAN_CHECK_XATTRS
- struct bfs_expr *expr = parse_nullary_test(state, eval_xattr);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
+ return parse_nullary_test(parser, eval_xattr);
#else
- parse_error(state, "Missing platform support.\n");
+ parse_error(parser, "Missing platform support.\n");
return NULL;
#endif
}
@@ -2794,16 +2536,11 @@ static struct bfs_expr *parse_xattr(struct parser_state *state, int arg1, int ar
/**
* Parse -xattrname.
*/
-static struct bfs_expr *parse_xattrname(struct parser_state *state, int arg1, int arg2) {
+static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int arg2) {
#if BFS_CAN_CHECK_XATTRS
- struct bfs_expr *expr = parse_unary_test(state, eval_xattrname);
- if (expr) {
- expr->cost = STAT_COST;
- expr->probability = 0.01;
- }
- return expr;
+ return parse_unary_test(parser, eval_xattrname);
#else
- parse_error(state, "Missing platform support.\n");
+ parse_error(parser, "Missing platform support.\n");
return NULL;
#endif
}
@@ -2811,10 +2548,10 @@ static struct bfs_expr *parse_xattrname(struct parser_state *state, int arg1, in
/**
* Parse -xdev.
*/
-static struct bfs_expr *parse_xdev(struct parser_state *state, int arg1, int arg2) {
- state->ctx->flags |= BFTW_PRUNE_MOUNTS;
- state->xdev_arg = state->argv;
- return parse_nullary_option(state);
+static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) {
+ parser->ctx->flags |= BFTW_PRUNE_MOUNTS;
+ parser->xdev_arg = parser->argv;
+ return parse_nullary_option(parser);
}
/**
@@ -2874,7 +2611,8 @@ static CFILE *launch_pager(pid_t *pid, CFILE *cout) {
NULL,
};
- if (strcmp(xbasename(exe), "less") == 0) {
+ const char *cmd = exe + xbaseoff(exe);
+ if (strcmp(cmd, "less") == 0) {
// We know less supports colors, other pagers may not
ret->colors = cout->colors;
argv[1] = "-FKRX";
@@ -2914,20 +2652,21 @@ fail:
/**
* "Parse" -help.
*/
-static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg2) {
- CFILE *cout = state->ctx->cout;
+static struct bfs_expr *parse_help(struct bfs_parser *parser, int arg1, int arg2) {
+ CFILE *cout = parser->ctx->cout;
pid_t pager = -1;
- if (state->stdout_tty) {
+ if (parser->stdout_tty) {
cout = launch_pager(&pager, cout);
}
cfprintf(cout, "Usage: ${ex}%s${rs} [${cyn}flags${rs}...] [${mag}paths${rs}...] [${blu}expression${rs}...]\n\n",
- state->command);
+ parser->command);
- cfprintf(cout, "${ex}bfs${rs} is compatible with ${ex}find${rs}, with some extensions. "
- "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n"
- "and ${blu}expressions${rs} may be freely mixed in any order.\n\n");
+ cfprintf(cout, "${ex}%s${rs} is compatible with ${ex}find${rs}, with some extensions. "
+ "${cyn}Flags${rs} (${cyn}-H${rs}/${cyn}-L${rs}/${cyn}-P${rs} etc.), ${mag}paths${rs},\n"
+ "and ${blu}expressions${rs} may be freely mixed in any order.\n\n",
+ BFS_COMMAND);
cfprintf(cout, "${bld}Flags:${rs}\n\n");
@@ -2957,7 +2696,9 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg
cfprintf(cout, " Enable optimization level ${bld}N${rs} (default: ${bld}3${rs})\n");
cfprintf(cout, " ${cyn}-S${rs} ${bld}bfs${rs}|${bld}dfs${rs}|${bld}ids${rs}|${bld}eds${rs}\n");
cfprintf(cout, " Use ${bld}b${rs}readth-${bld}f${rs}irst/${bld}d${rs}epth-${bld}f${rs}irst/${bld}i${rs}terative/${bld}e${rs}xponential ${bld}d${rs}eepening ${bld}s${rs}earch\n");
- cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n\n");
+ cfprintf(cout, " (default: ${cyn}-S${rs} ${bld}bfs${rs})\n");
+ cfprintf(cout, " ${cyn}-j${bld}N${rs}\n");
+ cfprintf(cout, " Search with ${bld}N${rs} threads in parallel (default: number of CPUs, up to ${bld}8${rs})\n\n");
cfprintf(cout, "${bld}Operators:${rs}\n\n");
@@ -2996,7 +2737,8 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg
cfprintf(cout, " Follow all symbolic links (same as ${cyn}-L${rs})\n");
cfprintf(cout, " ${blu}-ignore_readdir_race${rs}\n");
cfprintf(cout, " ${blu}-noignore_readdir_race${rs}\n");
- cfprintf(cout, " Whether to report an error if ${ex}bfs${rs} detects that the file tree is modified\n");
+ cfprintf(cout, " Whether to report an error if ${ex}%s${rs} detects that the file tree is modified\n",
+ BFS_COMMAND);
cfprintf(cout, " during the search (default: ${blu}-noignore_readdir_race${rs})\n");
cfprintf(cout, " ${blu}-maxdepth${rs} ${bld}N${rs}\n");
cfprintf(cout, " ${blu}-mindepth${rs} ${bld}N${rs}\n");
@@ -3039,6 +2781,10 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg
cfprintf(cout, " ${blu}-capable${rs}\n");
cfprintf(cout, " Find files with POSIX.1e capabilities set\n");
#endif
+#if BFS_CAN_CHECK_CONTEXT
+ cfprintf(cout, " ${blu}-context${rs} ${bld}GLOB${rs}\n");
+ cfprintf(cout, " Find files with SELinux context matching a glob pattern\n");
+#endif
cfprintf(cout, " ${blu}-depth${rs} ${bld}[-+]N${rs}\n");
cfprintf(cout, " Find files with depth ${bld}N${rs}\n");
cfprintf(cout, " ${blu}-empty${rs}\n");
@@ -3141,6 +2887,8 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg
cfprintf(cout, " ${blu}-fprintf${rs} ${bld}FILE${rs} ${bld}FORMAT${rs}\n");
cfprintf(cout, " Like ${blu}-ls${rs}/${blu}-print${rs}/${blu}-print0${rs}/${blu}-printf${rs}, but write to ${bld}FILE${rs} instead of standard\n"
" output\n");
+ cfprintf(cout, " ${blu}-limit${rs} ${bld}N${rs}\n");
+ cfprintf(cout, " Quit after this action is evaluated ${bld}N${rs} times\n");
cfprintf(cout, " ${blu}-ls${rs}\n");
cfprintf(cout, " List files like ${ex}ls${rs} ${bld}-dils${rs}\n");
cfprintf(cout, " ${blu}-print${rs}\n");
@@ -3167,26 +2915,26 @@ static struct bfs_expr *parse_help(struct parser_state *state, int arg1, int arg
if (pager > 0) {
cfclose(cout);
- waitpid(pager, NULL, 0);
+ xwaitpid(pager, NULL, 0);
}
- state->just_info = true;
+ parser->just_info = true;
return NULL;
}
/**
* "Parse" -version.
*/
-static struct bfs_expr *parse_version(struct parser_state *state, int arg1, int arg2) {
- cfprintf(state->ctx->cout, "${ex}bfs${rs} ${bld}%s${rs}\n\n", BFS_VERSION);
+static struct bfs_expr *parse_version(struct bfs_parser *parser, int arg1, int arg2) {
+ cfprintf(parser->ctx->cout, "${ex}%s${rs} ${bld}%s${rs}\n\n", BFS_COMMAND, bfs_version);
printf("%s\n", BFS_HOMEPAGE);
- state->just_info = true;
+ parser->just_info = true;
return NULL;
}
-typedef struct bfs_expr *parse_fn(struct parser_state *state, int arg1, int arg2);
+typedef struct bfs_expr *parse_fn(struct bfs_parser *parser, int arg1, int arg2);
/**
* An entry in the parse table for primary expressions.
@@ -3230,6 +2978,7 @@ static const struct table_entry parse_table[] = {
{"-cmin", T_TEST, parse_min, BFS_STAT_CTIME},
{"-cnewer", T_TEST, parse_newer, BFS_STAT_CTIME},
{"-color", T_OPTION, parse_color, true},
+ {"-context", T_TEST, parse_context, true},
{"-csince", T_TEST, parse_since, BFS_STAT_CTIME},
{"-ctime", T_TEST, parse_time, BFS_STAT_CTIME},
{"-d", T_FLAG, parse_depth},
@@ -3263,6 +3012,8 @@ static const struct table_entry parse_table[] = {
{"-ipath", T_TEST, parse_path, true},
{"-iregex", T_TEST, parse_regex, BFS_REGEX_ICASE},
{"-iwholename", T_TEST, parse_path, true},
+ {"-j", T_FLAG, parse_jobs, 0, 0, true},
+ {"-limit", T_ACTION, parse_limit},
{"-links", T_TEST, parse_links},
{"-lname", T_TEST, parse_lname, false},
{"-ls", T_ACTION, parse_ls},
@@ -3344,7 +3095,7 @@ static const struct table_entry *table_lookup(const char *arg) {
/** Search for a fuzzy match in the parse table. */
static const struct table_entry *table_lookup_fuzzy(const char *arg) {
const struct table_entry *best = NULL;
- int best_dist;
+ int best_dist = INT_MAX;
for (const struct table_entry *entry = parse_table; entry->arg; ++entry) {
int dist = typo_distance(arg, entry->arg);
@@ -3362,9 +3113,9 @@ static const struct table_entry *table_lookup_fuzzy(const char *arg) {
* | TEST
* | ACTION
*/
-static struct bfs_expr *parse_primary(struct parser_state *state) {
+static struct bfs_expr *parse_primary(struct bfs_parser *parser) {
// Paths are already skipped at this point
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (arg[0] != '-') {
goto unexpected;
@@ -3381,8 +3132,8 @@ static struct bfs_expr *parse_primary(struct parser_state *state) {
match = table_lookup_fuzzy(arg);
- CFILE *cerr = state->ctx->cerr;
- parse_error(state, "Unknown argument; did you mean ");
+ CFILE *cerr = parser->ctx->cerr;
+ parse_error(parser, "Unknown argument; did you mean ");
switch (match->type) {
case T_FLAG:
cfprintf(cerr, "${cyn}%s${rs}?", match->arg);
@@ -3395,7 +3146,7 @@ static struct bfs_expr *parse_primary(struct parser_state *state) {
break;
}
- if (!state->interactive || !match->parse) {
+ if (!parser->interactive || !match->parse) {
fprintf(stderr, "\n");
goto unmatched;
}
@@ -3406,16 +3157,16 @@ static struct bfs_expr *parse_primary(struct parser_state *state) {
}
fprintf(stderr, "\n");
- state->argv[0] = match->arg;
+ parser->argv[0] = match->arg;
matched:
- return match->parse(state, match->arg1, match->arg2);
+ return match->parse(parser, match->arg1, match->arg2);
unmatched:
return NULL;
unexpected:
- parse_error(state, "Expected a predicate.\n");
+ parse_error(parser, "Expected a predicate.\n");
return NULL;
}
@@ -3425,71 +3176,66 @@ unexpected:
* | "-exclude" FACTOR
* | PRIMARY
*/
-static struct bfs_expr *parse_factor(struct parser_state *state) {
- if (skip_paths(state) != 0) {
+static struct bfs_expr *parse_factor(struct bfs_parser *parser) {
+ if (skip_paths(parser) != 0) {
return NULL;
}
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (!arg) {
- parse_argv_error(state, state->last_arg, 1, "Expression terminated prematurely here.\n");
+ parse_argv_error(parser, parser->last_arg, 1, "Expression terminated prematurely here.\n");
return NULL;
}
if (strcmp(arg, "(") == 0) {
- parser_advance(state, T_OPERATOR, 1);
+ parser_advance(parser, T_OPERATOR, 1);
- struct bfs_expr *expr = parse_expr(state);
+ struct bfs_expr *expr = parse_expr(parser);
if (!expr) {
return NULL;
}
- if (skip_paths(state) != 0) {
- bfs_expr_free(expr);
+ if (skip_paths(parser) != 0) {
return NULL;
}
- arg = state->argv[0];
+ arg = parser->argv[0];
if (!arg || strcmp(arg, ")") != 0) {
- parse_argv_error(state, state->last_arg, 1, "Expected a ${red})${rs}.\n");
- bfs_expr_free(expr);
+ parse_argv_error(parser, parser->last_arg, 1, "Expected a ${red})${rs}.\n");
return NULL;
}
- parser_advance(state, T_OPERATOR, 1);
+ parser_advance(parser, T_OPERATOR, 1);
return expr;
} else if (strcmp(arg, "-exclude") == 0) {
- if (state->excluding) {
- parse_error(state, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg);
+ if (parser->excluding) {
+ parse_error(parser, "${err}%s${rs} is not supported within ${red}-exclude${rs}.\n", arg);
return NULL;
}
- parser_advance(state, T_OPERATOR, 1);
- state->excluding = true;
+ char **argv = parser_advance(parser, T_OPERATOR, 1);
+ parser->excluding = true;
- struct bfs_expr *factor = parse_factor(state);
+ struct bfs_expr *factor = parse_factor(parser);
if (!factor) {
return NULL;
}
- state->excluding = false;
+ parser->excluding = false;
- if (parse_exclude(state, factor) != 0) {
- return NULL;
- }
-
- return &bfs_true;
+ bfs_expr_append(parser->ctx->exclude, factor);
+ return parse_new_expr(parser, eval_true, parser->argv - argv, argv);
} else if (strcmp(arg, "!") == 0 || strcmp(arg, "-not") == 0) {
- char **argv = parser_advance(state, T_OPERATOR, 1);
+ char **argv = parser_advance(parser, T_OPERATOR, 1);
- struct bfs_expr *factor = parse_factor(state);
+ struct bfs_expr *factor = parse_factor(parser);
if (!factor) {
return NULL;
}
- return new_unary_expr(eval_not, factor, argv);
+ return new_unary_expr(parser, eval_not, factor, argv);
} else {
- return parse_primary(state);
+ return parse_primary(parser);
}
}
@@ -3499,16 +3245,15 @@ static struct bfs_expr *parse_factor(struct parser_state *state) {
* | TERM "-a" FACTOR
* | TERM "-and" FACTOR
*/
-static struct bfs_expr *parse_term(struct parser_state *state) {
- struct bfs_expr *term = parse_factor(state);
+static struct bfs_expr *parse_term(struct bfs_parser *parser) {
+ struct bfs_expr *term = parse_factor(parser);
while (term) {
- if (skip_paths(state) != 0) {
- bfs_expr_free(term);
+ if (skip_paths(parser) != 0) {
return NULL;
}
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (!arg) {
break;
}
@@ -3521,17 +3266,16 @@ static struct bfs_expr *parse_term(struct parser_state *state) {
char **argv = &fake_and_arg;
if (strcmp(arg, "-a") == 0 || strcmp(arg, "-and") == 0) {
- argv = parser_advance(state, T_OPERATOR, 1);
+ argv = parser_advance(parser, T_OPERATOR, 1);
}
struct bfs_expr *lhs = term;
- struct bfs_expr *rhs = parse_factor(state);
+ struct bfs_expr *rhs = parse_factor(parser);
if (!rhs) {
- bfs_expr_free(lhs);
return NULL;
}
- term = new_binary_expr(eval_and, lhs, rhs, argv);
+ term = new_binary_expr(parser, eval_and, lhs, rhs, argv);
}
return term;
@@ -3542,16 +3286,15 @@ static struct bfs_expr *parse_term(struct parser_state *state) {
* | CLAUSE "-o" TERM
* | CLAUSE "-or" TERM
*/
-static struct bfs_expr *parse_clause(struct parser_state *state) {
- struct bfs_expr *clause = parse_term(state);
+static struct bfs_expr *parse_clause(struct bfs_parser *parser) {
+ struct bfs_expr *clause = parse_term(parser);
while (clause) {
- if (skip_paths(state) != 0) {
- bfs_expr_free(clause);
+ if (skip_paths(parser) != 0) {
return NULL;
}
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (!arg) {
break;
}
@@ -3560,16 +3303,15 @@ static struct bfs_expr *parse_clause(struct parser_state *state) {
break;
}
- char **argv = parser_advance(state, T_OPERATOR, 1);
+ char **argv = parser_advance(parser, T_OPERATOR, 1);
struct bfs_expr *lhs = clause;
- struct bfs_expr *rhs = parse_term(state);
+ struct bfs_expr *rhs = parse_term(parser);
if (!rhs) {
- bfs_expr_free(lhs);
return NULL;
}
- clause = new_binary_expr(eval_or, lhs, rhs, argv);
+ clause = new_binary_expr(parser, eval_or, lhs, rhs, argv);
}
return clause;
@@ -3579,16 +3321,15 @@ static struct bfs_expr *parse_clause(struct parser_state *state) {
* EXPR : CLAUSE
* | EXPR "," CLAUSE
*/
-static struct bfs_expr *parse_expr(struct parser_state *state) {
- struct bfs_expr *expr = parse_clause(state);
+static struct bfs_expr *parse_expr(struct bfs_parser *parser) {
+ struct bfs_expr *expr = parse_clause(parser);
while (expr) {
- if (skip_paths(state) != 0) {
- bfs_expr_free(expr);
+ if (skip_paths(parser) != 0) {
return NULL;
}
- const char *arg = state->argv[0];
+ const char *arg = parser->argv[0];
if (!arg) {
break;
}
@@ -3597,16 +3338,15 @@ static struct bfs_expr *parse_expr(struct parser_state *state) {
break;
}
- char **argv = parser_advance(state, T_OPERATOR, 1);
+ char **argv = parser_advance(parser, T_OPERATOR, 1);
struct bfs_expr *lhs = expr;
- struct bfs_expr *rhs = parse_clause(state);
+ struct bfs_expr *rhs = parse_clause(parser);
if (!rhs) {
- bfs_expr_free(lhs);
return NULL;
}
- expr = new_binary_expr(eval_comma, lhs, rhs, argv);
+ expr = new_binary_expr(parser, eval_comma, lhs, rhs, argv);
}
return expr;
@@ -3615,71 +3355,129 @@ static struct bfs_expr *parse_expr(struct parser_state *state) {
/**
* Parse the top-level expression.
*/
-static struct bfs_expr *parse_whole_expr(struct parser_state *state) {
- if (skip_paths(state) != 0) {
+static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
+ if (skip_paths(parser) != 0) {
return NULL;
}
- struct bfs_expr *expr = &bfs_true;
- if (state->argv[0]) {
- expr = parse_expr(state);
- if (!expr) {
- return NULL;
- }
+ struct bfs_expr *expr;
+ if (parser->argv[0]) {
+ expr = parse_expr(parser);
+ } else {
+ expr = parse_new_expr(parser, eval_true, 1, &fake_true_arg);
+ }
+ if (!expr) {
+ return NULL;
}
- if (state->argv[0]) {
- parse_error(state, "Unexpected argument.\n");
- goto fail;
+ if (parser->argv[0]) {
+ parse_error(parser, "Unexpected argument.\n");
+ return NULL;
}
- if (state->implicit_print) {
- struct bfs_expr *print = bfs_expr_new(eval_fprint, 1, &fake_print_arg);
+ if (parser->implicit_print) {
+ char **limit = parser->limit_arg;
+ if (limit) {
+ parse_argv_error(parser, parser->limit_arg, 2,
+ "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n",
+ limit[0], limit[0], limit[1]);
+ return NULL;
+ }
+
+ struct bfs_expr *print = parse_new_expr(parser, eval_fprint, 1, &fake_print_arg);
if (!print) {
- goto fail;
+ return NULL;
}
- init_print_expr(state, print);
- print->synthetic = true;
+ init_print_expr(parser, print);
- expr = new_binary_expr(eval_and, expr, print, &fake_and_arg);
+ expr = new_binary_expr(parser, eval_and, expr, print, &fake_and_arg);
if (!expr) {
- goto fail;
+ return NULL;
}
}
- if (state->mount_arg && state->xdev_arg) {
- parse_conflict_warning(state, state->mount_arg, 1, state->xdev_arg, 1,
- "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n",
- state->xdev_arg[0], state->mount_arg[0]);
+ if (parser->mount_arg && parser->xdev_arg) {
+ parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1,
+ "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n",
+ parser->xdev_arg[0], parser->mount_arg[0]);
}
- if (state->ctx->warn && state->depth_arg && state->prune_arg) {
- parse_conflict_warning(state, state->depth_arg, 1, state->prune_arg, 1,
- "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n",
- state->prune_arg[0], state->depth_arg[0]);
+ if (parser->ctx->warn && parser->depth_arg && parser->prune_arg) {
+ parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1,
+ "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n",
+ parser->prune_arg[0], parser->depth_arg[0]);
- if (state->interactive) {
- bfs_warning(state->ctx, "Do you want to continue? ");
+ if (parser->interactive) {
+ bfs_warning(parser->ctx, "Do you want to continue? ");
if (ynprompt() == 0) {
- goto fail;
+ return NULL;
}
}
fprintf(stderr, "\n");
}
- if (state->ok_expr && state->files0_stdin_arg) {
- parse_conflict_error(state, state->ok_expr->argv, state->ok_expr->argc, state->files0_stdin_arg, 2,
- "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n",
- state->ok_expr->argv[0], state->files0_stdin_arg[0], state->files0_stdin_arg[1]);
- goto fail;
+ if (parser->ok_expr && parser->files0_stdin_arg) {
+ parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2,
+ "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n",
+ parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]);
+ return NULL;
}
return expr;
+}
-fail:
- bfs_expr_free(expr);
- return NULL;
+static const char *bftw_strategy_name(enum bftw_strategy strategy) {
+ switch (strategy) {
+ case BFTW_BFS:
+ return "bfs";
+ case BFTW_DFS:
+ return "dfs";
+ case BFTW_IDS:
+ return "ids";
+ case BFTW_EDS:
+ return "eds";
+ }
+
+ bfs_bug("Invalid strategy");
+ return "???";
+}
+
+static void dump_expr_multiline(const struct bfs_ctx *ctx, enum debug_flags flag, const struct bfs_expr *expr, int indent, int rparens) {
+ bfs_debug_prefix(ctx, flag);
+
+ for (int i = 0; i < indent; ++i) {
+ cfprintf(ctx->cerr, " ");
+ }
+
+ bool close = true;
+
+ if (bfs_expr_is_parent(expr)) {
+ if (SLIST_EMPTY(&expr->children)) {
+ cfprintf(ctx->cerr, "(${red}%s${rs}", expr->argv[0]);
+ ++rparens;
+ } else {
+ cfprintf(ctx->cerr, "(${red}%s${rs}\n", expr->argv[0]);
+ for (struct bfs_expr *child = bfs_expr_children(expr); child; child = child->next) {
+ int parens = child->next ? 0 : rparens + 1;
+ dump_expr_multiline(ctx, flag, child, indent + 1, parens);
+ }
+ close = false;
+ }
+ } else {
+ if (flag == DEBUG_RATES) {
+ cfprintf(ctx->cerr, "%pE", expr);
+ } else {
+ cfprintf(ctx->cerr, "%pe", expr);
+ }
+ }
+
+ if (close) {
+ for (int i = 0; i < rparens; ++i) {
+ cfprintf(ctx->cerr, ")");
+ }
+ cfprintf(ctx->cerr, "\n");
+ }
}
void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
@@ -3689,51 +3487,37 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
CFILE *cerr = ctx->cerr;
- cfprintf(cerr, "${ex}%s${rs} ", ctx->argv[0]);
+ cfprintf(cerr, "${ex}%s${rs}", ctx->argv[0]);
if (ctx->flags & BFTW_FOLLOW_ALL) {
- cfprintf(cerr, "${cyn}-L${rs} ");
+ cfprintf(cerr, " ${cyn}-L${rs}");
} else if (ctx->flags & BFTW_FOLLOW_ROOTS) {
- cfprintf(cerr, "${cyn}-H${rs} ");
+ cfprintf(cerr, " ${cyn}-H${rs}");
} else {
- cfprintf(cerr, "${cyn}-P${rs} ");
+ cfprintf(cerr, " ${cyn}-P${rs}");
}
if (ctx->xargs_safe) {
- cfprintf(cerr, "${cyn}-X${rs} ");
+ cfprintf(cerr, " ${cyn}-X${rs}");
}
if (ctx->flags & BFTW_SORT) {
- cfprintf(cerr, "${cyn}-s${rs} ");
+ cfprintf(cerr, " ${cyn}-s${rs}");
}
+ cfprintf(cerr, " ${cyn}-j${bld}%d${rs}", ctx->threads);
+
if (ctx->optlevel != 3) {
- cfprintf(cerr, "${cyn}-O${bld}%d${rs} ", ctx->optlevel);
+ cfprintf(cerr, " ${cyn}-O${bld}%d${rs}", ctx->optlevel);
}
- const char *strategy = NULL;
- switch (ctx->strategy) {
- case BFTW_BFS:
- strategy = "bfs";
- break;
- case BFTW_DFS:
- strategy = "dfs";
- break;
- case BFTW_IDS:
- strategy = "ids";
- break;
- case BFTW_EDS:
- strategy = "eds";
- break;
- }
- assert(strategy);
- cfprintf(cerr, "${cyn}-S${rs} ${bld}%s${rs} ", strategy);
+ cfprintf(cerr, " ${cyn}-S${rs} ${bld}%s${rs}", bftw_strategy_name(ctx->strategy));
enum debug_flags debug = ctx->debug;
if (debug == DEBUG_ALL) {
- cfprintf(cerr, "${cyn}-D${rs} ${bld}all${rs} ");
+ cfprintf(cerr, " ${cyn}-D${rs} ${bld}all${rs}");
} else if (debug) {
- cfprintf(cerr, "${cyn}-D${rs} ");
+ cfprintf(cerr, " ${cyn}-D${rs} ");
for (enum debug_flags i = 1; DEBUG_ALL & i; i <<= 1) {
if (debug & i) {
cfprintf(cerr, "${bld}%s${rs}", debug_flag_name(i));
@@ -3743,61 +3527,53 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
}
}
}
- cfprintf(cerr, " ");
}
- for (size_t i = 0; i < darray_length(ctx->paths); ++i) {
+ for (size_t i = 0; i < ctx->npaths; ++i) {
const char *path = ctx->paths[i];
char c = path[0];
if (c == '-' || c == '(' || c == ')' || c == '!' || c == ',') {
- cfprintf(cerr, "${cyn}-f${rs} ");
+ cfprintf(cerr, " ${cyn}-f${rs}");
}
- cfprintf(cerr, "${mag}%s${rs} ", path);
+ cfprintf(cerr, " ${mag}%pq${rs}", path);
}
if (ctx->cout->colors) {
- cfprintf(cerr, "${blu}-color${rs} ");
+ cfprintf(cerr, " ${blu}-color${rs}");
} else {
- cfprintf(cerr, "${blu}-nocolor${rs} ");
+ cfprintf(cerr, " ${blu}-nocolor${rs}");
}
if (ctx->flags & BFTW_POST_ORDER) {
- cfprintf(cerr, "${blu}-depth${rs} ");
+ cfprintf(cerr, " ${blu}-depth${rs}");
}
if (ctx->ignore_races) {
- cfprintf(cerr, "${blu}-ignore_readdir_race${rs} ");
+ cfprintf(cerr, " ${blu}-ignore_readdir_race${rs}");
}
if (ctx->mindepth != 0) {
- cfprintf(cerr, "${blu}-mindepth${rs} ${bld}%d${rs} ", ctx->mindepth);
+ cfprintf(cerr, " ${blu}-mindepth${rs} ${bld}%d${rs}", ctx->mindepth);
}
if (ctx->maxdepth != INT_MAX) {
- cfprintf(cerr, "${blu}-maxdepth${rs} ${bld}%d${rs} ", ctx->maxdepth);
+ cfprintf(cerr, " ${blu}-maxdepth${rs} ${bld}%d${rs}", ctx->maxdepth);
}
if (ctx->flags & BFTW_SKIP_MOUNTS) {
- cfprintf(cerr, "${blu}-mount${rs} ");
+ cfprintf(cerr, " ${blu}-mount${rs}");
}
if (ctx->status) {
- cfprintf(cerr, "${blu}-status${rs} ");
+ cfprintf(cerr, " ${blu}-status${rs}");
}
if (ctx->unique) {
- cfprintf(cerr, "${blu}-unique${rs} ");
+ cfprintf(cerr, " ${blu}-unique${rs}");
}
if ((ctx->flags & (BFTW_SKIP_MOUNTS | BFTW_PRUNE_MOUNTS)) == BFTW_PRUNE_MOUNTS) {
- cfprintf(cerr, "${blu}-xdev${rs} ");
- }
-
- if (flag == DEBUG_RATES) {
- if (ctx->exclude != &bfs_false) {
- cfprintf(cerr, "(${red}-exclude${rs} %pE) ", ctx->exclude);
- }
- cfprintf(cerr, "%pE", ctx->expr);
- } else {
- if (ctx->exclude != &bfs_false) {
- cfprintf(cerr, "(${red}-exclude${rs} %pe) ", ctx->exclude);
- }
- cfprintf(cerr, "%pe", ctx->expr);
+ cfprintf(cerr, " ${blu}-xdev${rs}");
}
fputs("\n", stderr);
+
+ bfs_debug(ctx, flag, "(${red}-exclude${rs}\n");
+ dump_expr_multiline(ctx, flag, ctx->exclude, 1, 1);
+
+ dump_expr_multiline(ctx, flag, ctx->expr, 0, 0);
}
/**
@@ -3806,57 +3582,32 @@ void bfs_ctx_dump(const struct bfs_ctx *ctx, enum debug_flags flag) {
static void dump_costs(const struct bfs_ctx *ctx) {
const struct bfs_expr *expr = ctx->expr;
bfs_debug(ctx, DEBUG_COST, " Cost: ~${ylw}%g${rs}\n", expr->cost);
- bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0*expr->probability);
-}
-
-/**
- * Get the current time.
- */
-static int parse_gettime(const struct bfs_ctx *ctx, struct timespec *ts) {
-#if _POSIX_TIMERS > 0
- int ret = clock_gettime(CLOCK_REALTIME, ts);
- if (ret != 0) {
- bfs_perror(ctx, "clock_gettime()");
- }
- return ret;
-#else
- struct timeval tv;
- int ret = gettimeofday(&tv, NULL);
- if (ret == 0) {
- ts->tv_sec = tv.tv_sec;
- ts->tv_nsec = tv.tv_usec * 1000L;
- } else {
- bfs_perror(ctx, "gettimeofday()");
- }
- return ret;
-#endif
+ bfs_debug(ctx, DEBUG_COST, "Probability: ~${ylw}%g%%${rs}\n", 100.0 * expr->probability);
}
struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
struct bfs_ctx *ctx = bfs_ctx_new();
if (!ctx) {
- perror("bfs_new_ctx()");
+ perror("bfs_ctx_new()");
goto fail;
}
- static char* default_argv[] = {"bfs", NULL};
+ static char *default_argv[] = {BFS_COMMAND, NULL};
if (argc < 1) {
argc = 1;
argv = default_argv;
}
ctx->argc = argc;
- ctx->argv = malloc((argc + 1)*sizeof(*ctx->argv));
+ ctx->argv = xmemdup(argv, sizeof_array(char *, argc + 1));
if (!ctx->argv) {
- perror("malloc()");
+ perror("xmemdup()");
goto fail;
}
- for (int i = 0; i <= argc; ++i) {
- ctx->argv[i] = argv[i];
- }
enum use_color use_color = COLOR_AUTO;
- if (getenv("NO_COLOR")) {
+ const char *no_color = getenv("NO_COLOR");
+ if (no_color && *no_color) {
// https://no-color.org/
use_color = COLOR_NEVER;
}
@@ -3893,7 +3644,7 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
ctx->warn = stdin_tty;
}
- struct parser_state state = {
+ struct bfs_parser parser = {
.ctx = ctx,
.argv = ctx->argv + 1,
.command = ctx->argv[0],
@@ -3910,39 +3661,36 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
.prune_arg = NULL,
.mount_arg = NULL,
.xdev_arg = NULL,
- .files0_arg = NULL,
.files0_stdin_arg = NULL,
.ok_expr = NULL,
+ .now = ctx->now,
};
- if (strcmp(xbasename(state.command), "find") == 0) {
- // Operate depth-first when invoked as "find"
- ctx->strategy = BFTW_DFS;
- }
-
- if (parse_gettime(ctx, &state.now) != 0) {
+ ctx->exclude = parse_new_expr(&parser, eval_or, 1, &fake_or_arg);
+ if (!ctx->exclude) {
goto fail;
}
- ctx->exclude = &bfs_false;
- ctx->expr = parse_whole_expr(&state);
+ ctx->expr = parse_whole_expr(&parser);
if (!ctx->expr) {
- if (state.just_info) {
+ if (parser.just_info) {
goto done;
} else {
goto fail;
}
}
+ if (parser.use_color == COLOR_AUTO && !ctx->colors) {
+ bfs_warning(ctx, "Error parsing $$LS_COLORS: %s.\n\n", xstrerror(ctx->colors_error));
+ }
+
if (bfs_optimize(ctx) != 0) {
+ bfs_perror(ctx, "bfs_optimize()");
goto fail;
}
- if (darray_length(ctx->paths) == 0) {
- if (!state.implicit_root) {
- parse_argv_error(&state, state.files0_arg, 2, "No root paths specified.\n");
- goto fail;
- } else if (parse_root(&state, ".") != 0) {
+ if (ctx->npaths == 0 && parser.implicit_root) {
+ if (parse_root(&parser, ".") != 0) {
goto fail;
}
}
diff --git a/src/parse.h b/src/parse.h
index 7e29a03..6895c9f 100644
--- a/src/parse.h
+++ b/src/parse.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* bfs command line parsing.
diff --git a/src/prelude.h b/src/prelude.h
new file mode 100644
index 0000000..ddeacbd
--- /dev/null
+++ b/src/prelude.h
@@ -0,0 +1,385 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Configuration and feature/platform detection.
+ */
+
+#ifndef BFS_PRELUDE_H
+#define BFS_PRELUDE_H
+
+// Possible __STDC_VERSION__ values
+
+#define C95 199409L
+#define C99 199901L
+#define C11 201112L
+#define C17 201710L
+#define C23 202311L
+
+#include <stddef.h>
+
+#if __STDC_VERSION__ < C23
+# include <stdalign.h>
+# include <stdbool.h>
+# include <stdnoreturn.h>
+#endif
+
+// bfs packaging configuration
+
+#include "config.h"
+
+#ifndef BFS_COMMAND
+# define BFS_COMMAND "bfs"
+#endif
+#ifndef BFS_HOMEPAGE
+# define BFS_HOMEPAGE "https://tavianator.com/projects/bfs.html"
+#endif
+
+// This is a symbol instead of a literal so we don't have to rebuild everything
+// when the version number changes
+extern const char bfs_version[];
+
+// Check for system headers
+
+#ifdef __has_include
+
+#if __has_include(<mntent.h>)
+# define BFS_HAS_MNTENT_H true
+#endif
+#if __has_include(<paths.h>)
+# define BFS_HAS_PATHS_H true
+#endif
+#if __has_include(<sys/extattr.h>)
+# define BFS_HAS_SYS_EXTATTR_H true
+#endif
+#if __has_include(<sys/mkdev.h>)
+# define BFS_HAS_SYS_MKDEV_H true
+#endif
+#if __has_include(<sys/param.h>)
+# define BFS_HAS_SYS_PARAM_H true
+#endif
+#if __has_include(<sys/sysmacros.h>)
+# define BFS_HAS_SYS_SYSMACROS_H true
+#endif
+#if __has_include(<sys/xattr.h>)
+# define BFS_HAS_SYS_XATTR_H true
+#endif
+#if __has_include(<threads.h>)
+# define BFS_HAS_THREADS_H true
+#endif
+#if __has_include(<util.h>)
+# define BFS_HAS_UTIL_H true
+#endif
+
+#else // !__has_include
+
+#define BFS_HAS_MNTENT_H __GLIBC__
+#define BFS_HAS_PATHS_H true
+#define BFS_HAS_SYS_EXTATTR_H __FreeBSD__
+#define BFS_HAS_SYS_MKDEV_H false
+#define BFS_HAS_SYS_PARAM_H true
+#define BFS_HAS_SYS_SYSMACROS_H __GLIBC__
+#define BFS_HAS_SYS_XATTR_H __linux__
+#define BFS_HAS_THREADS_H (!__STDC_NO_THREADS__)
+#define BFS_HAS_UTIL_H __NetBSD__
+
+#endif // !__has_include
+
+#ifndef BFS_USE_MNTENT_H
+# define BFS_USE_MNTENT_H BFS_HAS_MNTENT_H
+#endif
+#ifndef BFS_USE_PATHS_H
+# define BFS_USE_PATHS_H BFS_HAS_PATHS_H
+#endif
+#ifndef BFS_USE_SYS_EXTATTR_H
+# define BFS_USE_SYS_EXTATTR_H BFS_HAS_SYS_EXTATTR_H
+#endif
+#ifndef BFS_USE_SYS_MKDEV_H
+# define BFS_USE_SYS_MKDEV_H BFS_HAS_SYS_MKDEV_H
+#endif
+#ifndef BFS_USE_SYS_PARAM_H
+# define BFS_USE_SYS_PARAM_H BFS_HAS_SYS_PARAM_H
+#endif
+#ifndef BFS_USE_SYS_SYSMACROS_H
+# define BFS_USE_SYS_SYSMACROS_H BFS_HAS_SYS_SYSMACROS_H
+#endif
+#ifndef BFS_USE_SYS_XATTR_H
+# define BFS_USE_SYS_XATTR_H BFS_HAS_SYS_XATTR_H
+#endif
+#ifndef BFS_USE_THREADS_H
+# define BFS_USE_THREADS_H BFS_HAS_THREADS_H
+#endif
+#ifndef BFS_USE_UTIL_H
+# define BFS_USE_UTIL_H BFS_HAS_UTIL_H
+#endif
+
+// Stub out feature detection on old/incompatible compilers
+
+#ifndef __has_feature
+# define __has_feature(feat) false
+#endif
+
+#ifndef __has_c_attribute
+# define __has_c_attribute(attr) false
+#endif
+
+#ifndef __has_attribute
+# define __has_attribute(attr) false
+#endif
+
+// Platform detection
+
+// Get the definition of BSD if available
+#if BFS_USE_SYS_PARAM_H
+# include <sys/param.h>
+#endif
+
+#ifndef __GLIBC_PREREQ
+# define __GLIBC_PREREQ(maj, min) false
+#endif
+
+#ifndef __NetBSD_Prereq__
+# define __NetBSD_Prereq__(maj, min, patch) false
+#endif
+
+// Fundamental utilities
+
+/**
+ * Get the length of an array.
+ */
+#define countof(array) (sizeof(array) / sizeof(0[array]))
+
+/**
+ * False sharing/destructive interference/largest cache line size.
+ */
+#ifdef __GCC_DESTRUCTIVE_SIZE
+# define FALSE_SHARING_SIZE __GCC_DESTRUCTIVE_SIZE
+#else
+# define FALSE_SHARING_SIZE 64
+#endif
+
+/**
+ * True sharing/constructive interference/smallest cache line size.
+ */
+#ifdef __GCC_CONSTRUCTIVE_SIZE
+# define TRUE_SHARING_SIZE __GCC_CONSTRUCTIVE_SIZE
+#else
+# define TRUE_SHARING_SIZE 64
+#endif
+
+/**
+ * Polyfill max_align_t if we don't already have it.
+ */
+#if !BFS_HAS_MAX_ALIGN_T
+typedef union {
+# ifdef __BIGGEST_ALIGNMENT__
+ alignas(__BIGGEST_ALIGNMENT__) char c;
+# else
+ long double ld;
+ long long ll;
+ void *ptr;
+# endif
+} max_align_t;
+#endif
+
+/**
+ * Alignment specifier that avoids false sharing.
+ */
+#define cache_align alignas(FALSE_SHARING_SIZE)
+
+// Wrappers for attributes
+
+/**
+ * Silence warnings about switch/case fall-throughs.
+ */
+#if __has_attribute(fallthrough)
+# define fallthru __attribute__((fallthrough))
+#else
+# define fallthru ((void)0)
+#endif
+
+/**
+ * Silence warnings about unused declarations.
+ */
+#if __has_attribute(unused)
+# define attr_maybe_unused __attribute__((unused))
+#else
+# define attr_maybe_unused
+#endif
+
+/**
+ * Warn if a value is unused.
+ */
+#if __has_attribute(warn_unused_result)
+# define attr_nodiscard __attribute__((warn_unused_result))
+#else
+# define attr_nodiscard
+#endif
+
+/**
+ * Hint to avoid inlining a function.
+ */
+#if __has_attribute(noinline)
+# define attr_noinline __attribute__((noinline))
+#else
+# define attr_noinline
+#endif
+
+/**
+ * Hint that a function is unlikely to be called.
+ */
+#if __has_attribute(cold)
+# define attr_cold attr_noinline __attribute__((cold))
+#else
+# define attr_cold attr_noinline
+#endif
+
+/**
+ * Adds compiler warnings for bad printf()-style function calls, if supported.
+ */
+#if __has_attribute(format)
+# define attr_printf(fmt, args) __attribute__((format(printf, fmt, args)))
+#else
+# define attr_printf(fmt, args)
+#endif
+
+/**
+ * Annotates functions that potentially modify and return format strings.
+ */
+#if __has_attribute(format_arg)
+# define attr_format_arg(arg) __attribute__((format_arg(arg)))
+#else
+# define attr_format_arg(args)
+#endif
+
+/**
+ * Annotates allocator-like functions.
+ */
+#if __has_attribute(malloc)
+# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC
+# define attr_malloc(...) attr_nodiscard __attribute__((malloc(__VA_ARGS__)))
+# else
+# define attr_malloc(...) attr_nodiscard __attribute__((malloc))
+# endif
+#else
+# define attr_malloc(...) attr_nodiscard
+#endif
+
+/**
+ * Specifies that a function returns allocations with a given alignment.
+ */
+#if __has_attribute(alloc_align)
+# define attr_alloc_align(param) __attribute__((alloc_align(param)))
+#else
+# define attr_alloc_align(param)
+#endif
+
+/**
+ * Specifies that a function returns allocations with a given size.
+ */
+#if __has_attribute(alloc_size)
+# define attr_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+# define attr_alloc_size(...)
+#endif
+
+/**
+ * Shorthand for attr_alloc_align() and attr_alloc_size().
+ */
+#define attr_aligned_alloc(align, ...) \
+ attr_alloc_align(align) \
+ attr_alloc_size(__VA_ARGS__)
+
+/**
+ * Check if function multiversioning via GNU indirect functions (ifunc) is supported.
+ */
+#ifndef BFS_USE_TARGET_CLONES
+# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__)
+# define BFS_USE_TARGET_CLONES true
+# endif
+#endif
+
+/**
+ * Apply the target_clones attribute, if available.
+ */
+#if BFS_USE_TARGET_CLONES
+# define attr_target_clones(...) __attribute__((target_clones(__VA_ARGS__)))
+#else
+# define attr_target_clones(...)
+#endif
+
+/**
+ * Shorthand for multiple attributes at once. attr(a, b(c), d) is equivalent to
+ *
+ * attr_a
+ * attr_b(c)
+ * attr_d
+ */
+#define attr(...) \
+ attr__(attr_##__VA_ARGS__, none, none, none, none, none, none, none, none, none, )
+
+/**
+ * attr() helper. For exposition, pretend we support only 2 args, instead of 9.
+ * There are a few cases:
+ *
+ * attr()
+ * => attr__(attr_, none, none)
+ * => attr_ =>
+ * attr_none =>
+ * attr_too_many_none() =>
+ *
+ * attr(a)
+ * => attr__(attr_a, none, none)
+ * => attr_a => __attribute__((a))
+ * attr_none =>
+ * attr_too_many_none() =>
+ *
+ * attr(a, b(c))
+ * => attr__(attr_a, b(c), none, none)
+ * => attr_a => __attribute__((a))
+ * attr_b(c) => __attribute__((b(c)))
+ * attr_too_many_none(none) =>
+ *
+ * attr(a, b(c), d)
+ * => attr__(attr_a, b(c), d, none, none)
+ * => attr_a => __attribute__((a))
+ * attr_b(c) => __attribute__((b(c)))
+ * attr_too_many_d(none, none) => error
+ *
+ * Some attribute names are the same as standard library functions, e.g. printf.
+ * Standard libraries are permitted to define these functions as macros, like
+ *
+ * #define printf(...) __builtin_printf(__VA_ARGS__)
+ *
+ * The token paste in
+ *
+ * #define attr(...) attr__(attr_##__VA_ARGS__, none, none)
+ *
+ * is necessary to prevent macro expansion before evaluating attr__().
+ * Otherwise, we could get
+ *
+ * attr(printf(1, 2))
+ * => attr__(__builtin_printf(1, 2), none, none)
+ * => attr____builtin_printf(1, 2)
+ * => error
+ */
+#define attr__(a1, a2, a3, a4, a5, a6, a7, a8, a9, none, ...) \
+ a1 \
+ attr_##a2 \
+ attr_##a3 \
+ attr_##a4 \
+ attr_##a5 \
+ attr_##a6 \
+ attr_##a7 \
+ attr_##a8 \
+ attr_##a9 \
+ attr_too_many_##none(__VA_ARGS__)
+
+// Ignore `attr_none` from expanding 1-9 argument attr(a1, a2, ...)
+#define attr_none
+// Ignore `attr_` from expanding 0-argument attr()
+#define attr_
+// Only trigger an error on more than 9 arguments
+#define attr_too_many_none(...)
+
+#endif // BFS_PRELUDE_H
diff --git a/src/printf.c b/src/printf.c
index 8fdde41..f8428f7 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -1,70 +1,68 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "printf.h"
+#include "alloc.h"
+#include "bfstd.h"
#include "bftw.h"
#include "color.h"
#include "ctx.h"
-#include "darray.h"
#include "diag.h"
#include "dir.h"
#include "dstring.h"
#include "expr.h"
+#include "fsade.h"
#include "mtab.h"
#include "pwcache.h"
#include "stat.h"
-#include "util.h"
-#include "xtime.h"
-#include <assert.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
-#include <stdbool.h>
+#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
+struct bfs_fmt;
+
/**
* A function implementing a printf directive.
*/
-typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf);
+typedef int bfs_printf_fn(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf);
/**
- * A single printf directive like %f or %#4m. The whole format string is stored
- * as a darray of these.
+ * A single formatting directive like %f or %#4m.
*/
-struct bfs_printf {
+struct bfs_fmt {
/** The printing function to invoke. */
bfs_printf_fn *fn;
/** String data associated with this directive. */
- char *str;
+ dchar *str;
/** The stat field to print. */
enum bfs_stat_field stat_field;
/** Character data associated with this directive. */
char c;
/** Some data used by the directive. */
- const void *ptr;
+ void *ptr;
+};
+
+/**
+ * An entire format string.
+ */
+struct bfs_printf {
+ /** An array of formatting directives. */
+ struct bfs_fmt *fmts;
+ /** The number of directives. */
+ size_t nfmts;
};
/** Print some text as-is. */
-static int bfs_printf_literal(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- size_t len = dstrlen(directive->str);
- if (fwrite(directive->str, 1, len, cfile->file) == len) {
+static int bfs_printf_literal(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ size_t len = dstrlen(fmt->str);
+ if (fwrite(fmt->str, 1, len, cfile->file) == len) {
return 0;
} else {
return -1;
@@ -72,26 +70,44 @@ static int bfs_printf_literal(CFILE *cfile, const struct bfs_printf *directive,
}
/** \c: flush */
-static int bfs_printf_flush(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_flush(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
return fflush(cfile->file);
}
/** Check if we can safely colorize this directive. */
-static bool should_color(CFILE *cfile, const struct bfs_printf *directive) {
- return cfile->colors && strcmp(directive->str, "%s") == 0;
+static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
+ return cfile->colors && strcmp(fmt->str, "%s") == 0;
}
/**
* Print a value to a temporary buffer before formatting it.
*/
-#define BFS_PRINTF_BUF(buf, format, ...) \
- char buf[256]; \
- int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \
- assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
+#define BFS_PRINTF_BUF(buf, format, ...) \
+ char buf[256]; \
+ int ret = snprintf(buf, sizeof(buf), format, __VA_ARGS__); \
+ bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf)); \
(void)ret
+/** Return a dynamic format string. */
+attr(format_arg(2))
+static const char *dyn_fmt(const char *str, const char *fake) {
+ bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
+ "Mismatched format specifiers: '%s' vs. '%s'", str, fake);
+ return str;
+}
+
+/** Wrapper for fprintf(). */
+attr(printf(3, 4))
+static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
+ va_list args;
+ va_start(args, fake);
+ int ret = vfprintf(cfile->file, dyn_fmt(fmt->str, fake), args);
+ va_end(args);
+ return ret;
+}
+
/** %a, %c, %t: ctime() */
-static int bfs_printf_ctime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_ctime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
// Not using ctime() itself because GNU find adds nanoseconds
static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
@@ -101,69 +117,69 @@ static int bfs_printf_ctime(CFILE *cfile, const struct bfs_printf *directive, co
return -1;
}
- const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field);
+ const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
if (!ts) {
return -1;
}
struct tm tm;
- if (xlocaltime(&ts->tv_sec, &tm) != 0) {
+ if (!localtime_r(&ts->tv_sec, &tm)) {
return -1;
}
BFS_PRINTF_BUF(buf, "%s %s %2d %.2d:%.2d:%.2d.%09ld0 %4d",
- days[tm.tm_wday],
- months[tm.tm_mon],
- tm.tm_mday,
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec,
- 1900 + tm.tm_year);
+ days[tm.tm_wday],
+ months[tm.tm_mon],
+ tm.tm_mday,
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec,
+ 1900 + tm.tm_year);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %A, %B/%W, %C, %T: strftime() */
-static int bfs_printf_strftime(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_strftime(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- const struct timespec *ts = bfs_stat_time(statbuf, directive->stat_field);
+ const struct timespec *ts = bfs_stat_time(statbuf, fmt->stat_field);
if (!ts) {
return -1;
}
struct tm tm;
- if (xlocaltime(&ts->tv_sec, &tm) != 0) {
+ if (!localtime_r(&ts->tv_sec, &tm)) {
return -1;
}
int ret;
char buf[256];
char format[] = "% ";
- switch (directive->c) {
+ switch (fmt->c) {
// Non-POSIX strftime() features
case '@':
ret = snprintf(buf, sizeof(buf), "%lld.%09ld0", (long long)ts->tv_sec, (long)ts->tv_nsec);
break;
case '+':
ret = snprintf(buf, sizeof(buf), "%4d-%.2d-%.2d+%.2d:%.2d:%.2d.%09ld0",
- 1900 + tm.tm_year,
- tm.tm_mon + 1,
- tm.tm_mday,
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec);
+ 1900 + tm.tm_year,
+ tm.tm_mon + 1,
+ tm.tm_mday,
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec);
break;
case 'k':
ret = snprintf(buf, sizeof(buf), "%2d", tm.tm_hour);
break;
case 'l':
- ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11)%12 + 1);
+ ret = snprintf(buf, sizeof(buf), "%2d", (tm.tm_hour + 11) % 12 + 1);
break;
case 's':
ret = snprintf(buf, sizeof(buf), "%lld", (long long)ts->tv_sec);
@@ -173,102 +189,113 @@ static int bfs_printf_strftime(CFILE *cfile, const struct bfs_printf *directive,
break;
case 'T':
ret = snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d.%09ld0",
- tm.tm_hour,
- tm.tm_min,
- tm.tm_sec,
- (long)ts->tv_nsec);
+ tm.tm_hour,
+ tm.tm_min,
+ tm.tm_sec,
+ (long)ts->tv_nsec);
break;
// POSIX strftime() features
default:
- format[1] = directive->c;
+ format[1] = fmt->c;
+#if __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
ret = strftime(buf, sizeof(buf), format, &tm);
+#if __GNUC__
+# pragma GCC diagnostic pop
+#endif
break;
}
- assert(ret >= 0 && (size_t)ret < sizeof(buf));
+ bfs_assert(ret >= 0 && (size_t)ret < sizeof(buf));
(void)ret;
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %b: blocks */
-static int bfs_printf_b(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_b(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 511)/512;
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 511) / 512;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %d: depth */
-static int bfs_printf_d(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- return fprintf(cfile->file, directive->str, (intmax_t)ftwbuf->depth);
+static int bfs_printf_d(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ return bfs_fprintf(cfile, fmt, "%jd", (intmax_t)ftwbuf->depth);
}
/** %D: device */
-static int bfs_printf_D(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_D(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->dev);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %f: file name */
-static int bfs_printf_f(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- if (should_color(cfile, directive)) {
+static int bfs_printf_f(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pF", ftwbuf);
} else {
- return fprintf(cfile->file, directive->str, ftwbuf->path + ftwbuf->nameoff);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + ftwbuf->nameoff);
}
}
/** %F: file system type */
-static int bfs_printf_F(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_F(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- const char *type = bfs_fstype(directive->ptr, statbuf);
- return fprintf(cfile->file, directive->str, type);
+ const char *type = bfs_fstype(fmt->ptr, statbuf);
+ if (!type) {
+ return -1;
+ }
+
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %G: gid */
-static int bfs_printf_G(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_G(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->gid);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %g: group name */
-static int bfs_printf_g(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_g(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- const struct bfs_groups *groups = directive->ptr;
- const struct group *grp = groups ? bfs_getgrgid(groups, statbuf->gid) : NULL;
+ struct bfs_groups *groups = fmt->ptr;
+ const struct group *grp = bfs_getgrgid(groups, statbuf->gid);
if (!grp) {
- return bfs_printf_G(cfile, directive, ftwbuf);
+ return bfs_printf_G(cfile, fmt, ftwbuf);
}
- return fprintf(cfile->file, directive->str, grp->gr_name);
+ return bfs_fprintf(cfile, fmt, "%s", grp->gr_name);
}
/** %h: leading directories */
-static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_h(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
char *copy = NULL;
const char *buf;
@@ -290,10 +317,10 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const
}
int ret;
- if (should_color(cfile, directive)) {
- ret = cfprintf(cfile, "${di}%s${rs}", buf);
+ if (should_color(cfile, fmt)) {
+ ret = cfprintf(cfile, "${di}%pQ${rs}", buf);
} else {
- ret = fprintf(cfile->file, directive->str, buf);
+ ret = bfs_fprintf(cfile, fmt, "%s", buf);
}
free(copy);
@@ -301,48 +328,48 @@ static int bfs_printf_h(CFILE *cfile, const struct bfs_printf *directive, const
}
/** %H: current root */
-static int bfs_printf_H(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- if (should_color(cfile, directive)) {
+static int bfs_printf_H(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
if (ftwbuf->depth == 0) {
return cfprintf(cfile, "%pP", ftwbuf);
} else {
- return cfprintf(cfile, "${di}%s${rs}", ftwbuf->root);
+ return cfprintf(cfile, "${di}%pQ${rs}", ftwbuf->root);
}
} else {
- return fprintf(cfile->file, directive->str, ftwbuf->root);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->root);
}
}
/** %i: inode */
-static int bfs_printf_i(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_i(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->ino);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %k: 1K blocks */
-static int bfs_printf_k(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_k(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- uintmax_t blocks = ((uintmax_t)statbuf->blocks*BFS_STAT_BLKSIZE + 1023)/1024;
+ uintmax_t blocks = ((uintmax_t)statbuf->blocks * BFS_STAT_BLKSIZE + 1023) / 1024;
BFS_PRINTF_BUF(buf, "%ju", blocks);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %l: link target */
-static int bfs_printf_l(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_l(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
char *buf = NULL;
const char *target = "";
if (ftwbuf->type == BFS_LNK) {
- if (should_color(cfile, directive)) {
+ if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pL", ftwbuf);
}
@@ -355,23 +382,23 @@ static int bfs_printf_l(CFILE *cfile, const struct bfs_printf *directive, const
}
}
- int ret = fprintf(cfile->file, directive->str, target);
+ int ret = bfs_fprintf(cfile, fmt, "%s", target);
free(buf);
return ret;
}
/** %m: mode */
-static int bfs_printf_m(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_m(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- return fprintf(cfile->file, directive->str, (unsigned int)(statbuf->mode & 07777));
+ return bfs_fprintf(cfile, fmt, "%o", (unsigned int)(statbuf->mode & 07777));
}
/** %M: symbolic mode */
-static int bfs_printf_M(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_M(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
@@ -379,37 +406,37 @@ static int bfs_printf_M(CFILE *cfile, const struct bfs_printf *directive, const
char buf[11];
xstrmode(statbuf->mode, buf);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %n: link count */
-static int bfs_printf_n(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_n(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->nlink);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %p: full path */
-static int bfs_printf_p(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- if (should_color(cfile, directive)) {
+static int bfs_printf_p(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ if (should_color(cfile, fmt)) {
return cfprintf(cfile, "%pP", ftwbuf);
} else {
- return fprintf(cfile->file, directive->str, ftwbuf->path);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path);
}
}
/** %P: path after root */
-static int bfs_printf_P(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_P(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
size_t offset = strlen(ftwbuf->root);
if (ftwbuf->path[offset] == '/') {
++offset;
}
- if (should_color(cfile, directive)) {
+ if (should_color(cfile, fmt)) {
if (ftwbuf->depth == 0) {
return 0;
}
@@ -419,23 +446,23 @@ static int bfs_printf_P(CFILE *cfile, const struct bfs_printf *directive, const
copybuf.nameoff -= offset;
return cfprintf(cfile, "%pP", &copybuf);
} else {
- return fprintf(cfile->file, directive->str, ftwbuf->path + offset);
+ return bfs_fprintf(cfile, fmt, "%s", ftwbuf->path + offset);
}
}
/** %s: size */
-static int bfs_printf_s(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_s(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->size);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %S: sparseness */
-static int bfs_printf_S(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_S(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
@@ -445,36 +472,36 @@ static int bfs_printf_S(CFILE *cfile, const struct bfs_printf *directive, const
if (statbuf->size == 0 && statbuf->blocks == 0) {
sparsity = 1.0;
} else {
- sparsity = (double)BFS_STAT_BLKSIZE*statbuf->blocks/statbuf->size;
+ sparsity = (double)BFS_STAT_BLKSIZE * statbuf->blocks / statbuf->size;
}
- return fprintf(cfile->file, directive->str, sparsity);
+ return bfs_fprintf(cfile, fmt, "%g", sparsity);
}
/** %U: uid */
-static int bfs_printf_U(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_U(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
BFS_PRINTF_BUF(buf, "%ju", (uintmax_t)statbuf->uid);
- return fprintf(cfile->file, directive->str, buf);
+ return bfs_fprintf(cfile, fmt, "%s", buf);
}
/** %u: user name */
-static int bfs_printf_u(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_u(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const struct bfs_stat *statbuf = bftw_stat(ftwbuf, ftwbuf->stat_flags);
if (!statbuf) {
return -1;
}
- const struct bfs_users *users = directive->ptr;
- const struct passwd *pwd = users ? bfs_getpwuid(users, statbuf->uid) : NULL;
+ struct bfs_users *users = fmt->ptr;
+ const struct passwd *pwd = bfs_getpwuid(users, statbuf->uid);
if (!pwd) {
- return bfs_printf_U(cfile, directive, ftwbuf);
+ return bfs_printf_U(cfile, fmt, ftwbuf);
}
- return fprintf(cfile->file, directive->str, pwd->pw_name);
+ return bfs_fprintf(cfile, fmt, "%s", pwd->pw_name);
}
static const char *bfs_printf_type(enum bfs_type type) {
@@ -491,51 +518,45 @@ static const char *bfs_printf_type(enum bfs_type type) {
return "p";
case BFS_LNK:
return "l";
+ case BFS_PORT:
+ return "P";
case BFS_REG:
return "f";
case BFS_SOCK:
return "s";
+ case BFS_WHT:
+ return "w";
default:
return "U";
}
}
/** %y: type */
-static int bfs_printf_y(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
+static int bfs_printf_y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
const char *type = bfs_printf_type(ftwbuf->type);
- return fprintf(cfile->file, directive->str, type);
+ return bfs_fprintf(cfile, fmt, "%s", type);
}
/** %Y: target type */
-static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const struct BFTW *ftwbuf) {
- int error = 0;
-
- if (ftwbuf->type != BFS_LNK) {
- return bfs_printf_y(cfile, directive, ftwbuf);
- }
+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;
- const char *type = "U";
-
- const struct bfs_stat *statbuf = bftw_stat(ftwbuf, BFS_STAT_FOLLOW);
- if (statbuf) {
- type = bfs_printf_type(bfs_mode_to_type(statbuf->mode));
- } else {
- switch (errno) {
- case ELOOP:
- type = "L";
- break;
- case ENOENT:
- case ENOTDIR:
- type = "N";
- break;
- default:
- type = "?";
+ int error = 0;
+ if (type == BFS_ERROR) {
+ if (errno_is_like(ELOOP)) {
+ str = "L";
+ } else if (errno_is_like(ENOENT)) {
+ str = "N";
+ } else {
+ str = "?";
error = errno;
- break;
}
+ } else {
+ str = bfs_printf_type(type);
}
- int ret = fprintf(cfile->file, directive->str, type);
+ int ret = bfs_fprintf(cfile, fmt, "%s", str);
if (error != 0) {
ret = -1;
errno = error;
@@ -543,24 +564,36 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_printf *directive, const
return ret;
}
+/** %Z: SELinux context */
+attr(maybe_unused)
+static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
+ char *con = bfs_getfilecon(ftwbuf);
+ if (!con) {
+ return -1;
+ }
+
+ int ret = bfs_fprintf(cfile, fmt, "%s", con);
+ bfs_freecon(con);
+ return ret;
+}
+
/**
* Append a literal string to the chain.
*/
-static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal) {
+static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal) {
if (dstrlen(*literal) == 0) {
return 0;
}
- struct bfs_printf directive = {
- .fn = bfs_printf_literal,
- .str = *literal,
- };
-
- if (DARRAY_PUSH(format, &directive) != 0) {
- bfs_perror(ctx, "DARRAY_PUSH()");
+ struct bfs_fmt *fmt = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
+ if (!fmt) {
+ bfs_perror(ctx, "RESERVE()");
return -1;
}
+ fmt->fn = bfs_printf_literal;
+ fmt->str = *literal;
+
*literal = dstralloc(0);
if (!*literal) {
bfs_perror(ctx, "dstralloc()");
@@ -573,23 +606,29 @@ static int append_literal(const struct bfs_ctx *ctx, struct bfs_printf **format,
/**
* Append a printf directive to the chain.
*/
-static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf **format, char **literal, struct bfs_printf *directive) {
+static int append_directive(const struct bfs_ctx *ctx, struct bfs_printf *format, dchar **literal, struct bfs_fmt *fmt) {
if (append_literal(ctx, format, literal) != 0) {
return -1;
}
- if (DARRAY_PUSH(format, directive) != 0) {
- bfs_perror(ctx, "DARRAY_PUSH()");
+ struct bfs_fmt *dest = RESERVE(struct bfs_fmt, &format->fmts, &format->nfmts);
+ if (!dest) {
+ bfs_perror(ctx, "RESERVE()");
return -1;
}
+ *dest = *fmt;
return 0;
}
int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const char *format) {
- expr->printf = NULL;
+ expr->printf = ZALLOC(struct bfs_printf);
+ if (!expr->printf) {
+ bfs_perror(ctx, "zalloc()");
+ return -1;
+ }
- char *literal = dstralloc(0);
+ dchar *literal = dstralloc(0);
if (!literal) {
bfs_perror(ctx, "dstralloc()");
goto error;
@@ -623,10 +662,10 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
case 'c':
{
- struct bfs_printf directive = {
+ struct bfs_fmt fmt = {
.fn = bfs_printf_flush,
};
- if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) {
+ if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
goto error;
}
goto done;
@@ -648,15 +687,15 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
goto one_char;
}
- struct bfs_printf directive = {
+ struct bfs_fmt fmt = {
.str = dstralloc(2),
};
- if (!directive.str) {
- goto directive_error;
+ if (!fmt.str) {
+ goto fmt_error;
}
- if (dstrapp(&directive.str, c) != 0) {
+ if (dstrapp(&fmt.str, c) != 0) {
bfs_perror(ctx, "dstrapp()");
- goto directive_error;
+ goto fmt_error;
}
const char *specifier = "s";
@@ -671,17 +710,17 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
case '0':
case '+':
must_be_numeric = true;
- BFS_FALLTHROUGH;
+ fallthru;
case ' ':
case '-':
- if (strchr(directive.str, c)) {
+ if (strchr(fmt.str, c)) {
bfs_expr_error(ctx, expr);
bfs_error(ctx, "Duplicate flag '%c'.\n", c);
- goto directive_error;
+ goto fmt_error;
}
- if (dstrapp(&directive.str, c) != 0) {
+ if (dstrapp(&fmt.str, c) != 0) {
bfs_perror(ctx, "dstrapp()");
- goto directive_error;
+ goto fmt_error;
}
continue;
}
@@ -691,9 +730,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
// Parse the field width
while (c >= '0' && c <= '9') {
- if (dstrapp(&directive.str, c) != 0) {
+ if (dstrapp(&fmt.str, c) != 0) {
bfs_perror(ctx, "dstrapp()");
- goto directive_error;
+ goto fmt_error;
}
c = *++i;
}
@@ -701,9 +740,9 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
// Parse the precision
if (c == '.') {
do {
- if (dstrapp(&directive.str, c) != 0) {
+ if (dstrapp(&fmt.str, c) != 0) {
bfs_perror(ctx, "dstrapp()");
- goto directive_error;
+ goto fmt_error;
}
c = *++i;
} while (c >= '0' && c <= '9');
@@ -711,175 +750,172 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
switch (c) {
case 'a':
- directive.fn = bfs_printf_ctime;
- directive.stat_field = BFS_STAT_ATIME;
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_ATIME;
break;
case 'b':
- directive.fn = bfs_printf_b;
+ fmt.fn = bfs_printf_b;
break;
case 'c':
- directive.fn = bfs_printf_ctime;
- directive.stat_field = BFS_STAT_CTIME;
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_CTIME;
break;
case 'd':
- directive.fn = bfs_printf_d;
+ fmt.fn = bfs_printf_d;
specifier = "jd";
break;
case 'D':
- directive.fn = bfs_printf_D;
+ fmt.fn = bfs_printf_D;
break;
case 'f':
- directive.fn = bfs_printf_f;
+ fmt.fn = bfs_printf_f;
break;
case 'F':
- directive.ptr = bfs_ctx_mtab(ctx);
- if (!directive.ptr) {
+ fmt.fn = bfs_printf_F;
+ fmt.ptr = (void *)bfs_ctx_mtab(ctx);
+ if (!fmt.ptr) {
int error = errno;
bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Couldn't parse the mount table: %s.\n", strerror(error));
- goto directive_error;
+ bfs_error(ctx, "Couldn't parse the mount table: %s.\n", xstrerror(error));
+ goto fmt_error;
}
- directive.fn = bfs_printf_F;
break;
case 'g':
- directive.ptr = bfs_ctx_groups(ctx);
- if (!directive.ptr) {
- int error = errno;
- bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Couldn't parse the group table: %s.\n", strerror(error));
- goto directive_error;
- }
- directive.fn = bfs_printf_g;
+ fmt.fn = bfs_printf_g;
+ fmt.ptr = ctx->groups;
break;
case 'G':
- directive.fn = bfs_printf_G;
+ fmt.fn = bfs_printf_G;
break;
case 'h':
- directive.fn = bfs_printf_h;
+ fmt.fn = bfs_printf_h;
break;
case 'H':
- directive.fn = bfs_printf_H;
+ fmt.fn = bfs_printf_H;
break;
case 'i':
- directive.fn = bfs_printf_i;
+ fmt.fn = bfs_printf_i;
break;
case 'k':
- directive.fn = bfs_printf_k;
+ fmt.fn = bfs_printf_k;
break;
case 'l':
- directive.fn = bfs_printf_l;
+ fmt.fn = bfs_printf_l;
break;
case 'm':
- directive.fn = bfs_printf_m;
+ fmt.fn = bfs_printf_m;
specifier = "o";
break;
case 'M':
- directive.fn = bfs_printf_M;
+ fmt.fn = bfs_printf_M;
break;
case 'n':
- directive.fn = bfs_printf_n;
+ fmt.fn = bfs_printf_n;
break;
case 'p':
- directive.fn = bfs_printf_p;
+ fmt.fn = bfs_printf_p;
break;
case 'P':
- directive.fn = bfs_printf_P;
+ fmt.fn = bfs_printf_P;
break;
case 's':
- directive.fn = bfs_printf_s;
+ fmt.fn = bfs_printf_s;
break;
case 'S':
- directive.fn = bfs_printf_S;
+ fmt.fn = bfs_printf_S;
specifier = "g";
break;
case 't':
- directive.fn = bfs_printf_ctime;
- directive.stat_field = BFS_STAT_MTIME;
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_MTIME;
break;
case 'u':
- directive.ptr = bfs_ctx_users(ctx);
- if (!directive.ptr) {
- int error = errno;
- bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Couldn't parse the user table: %s.\n", strerror(error));
- goto directive_error;
- }
- directive.fn = bfs_printf_u;
+ fmt.fn = bfs_printf_u;
+ fmt.ptr = ctx->users;
break;
case 'U':
- directive.fn = bfs_printf_U;
+ fmt.fn = bfs_printf_U;
break;
case 'w':
- directive.fn = bfs_printf_ctime;
- directive.stat_field = BFS_STAT_BTIME;
+ fmt.fn = bfs_printf_ctime;
+ fmt.stat_field = BFS_STAT_BTIME;
break;
case 'y':
- directive.fn = bfs_printf_y;
+ fmt.fn = bfs_printf_y;
break;
case 'Y':
- directive.fn = bfs_printf_Y;
+ fmt.fn = bfs_printf_Y;
+ break;
+ case 'Z':
+#if BFS_CAN_CHECK_CONTEXT
+ fmt.fn = bfs_printf_Z;
break;
+#else
+ bfs_expr_error(ctx, expr);
+ bfs_error(ctx, "Missing platform support for '%%%c'.\n", c);
+ goto fmt_error;
+#endif
case 'A':
- directive.stat_field = BFS_STAT_ATIME;
- goto directive_strftime;
+ fmt.stat_field = BFS_STAT_ATIME;
+ goto fmt_strftime;
case 'B':
case 'W':
- directive.stat_field = BFS_STAT_BTIME;
- goto directive_strftime;
+ fmt.stat_field = BFS_STAT_BTIME;
+ goto fmt_strftime;
case 'C':
- directive.stat_field = BFS_STAT_CTIME;
- goto directive_strftime;
+ fmt.stat_field = BFS_STAT_CTIME;
+ goto fmt_strftime;
case 'T':
- directive.stat_field = BFS_STAT_MTIME;
- goto directive_strftime;
+ fmt.stat_field = BFS_STAT_MTIME;
+ goto fmt_strftime;
- directive_strftime:
- directive.fn = bfs_printf_strftime;
+ fmt_strftime:
+ fmt.fn = bfs_printf_strftime;
c = *++i;
if (!c) {
bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", directive.str, i[-1]);
- goto directive_error;
+ bfs_error(ctx, "Incomplete time specifier '%s%c'.\n", fmt.str, i[-1]);
+ goto fmt_error;
} else if (strchr("%+@aAbBcCdDeFgGhHIjklmMnprRsStTuUVwWxXyYzZ", c)) {
- directive.c = c;
+ fmt.c = c;
} else {
bfs_expr_error(ctx, expr);
bfs_error(ctx, "Unrecognized time specifier '%%%c%c'.\n", i[-1], c);
- goto directive_error;
+ goto fmt_error;
}
break;
case '\0':
bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Incomplete format specifier '%s'.\n", directive.str);
- goto directive_error;
+ bfs_error(ctx, "Incomplete format specifier '%s'.\n", fmt.str);
+ goto fmt_error;
default:
bfs_expr_error(ctx, expr);
bfs_error(ctx, "Unrecognized format specifier '%%%c'.\n", c);
- goto directive_error;
+ goto fmt_error;
}
if (must_be_numeric && strcmp(specifier, "s") == 0) {
bfs_expr_error(ctx, expr);
- bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", directive.str + 1, c);
- goto directive_error;
+ bfs_error(ctx, "Invalid flags '%s' for string format '%%%c'.\n", fmt.str + 1, c);
+ goto fmt_error;
}
- if (dstrcat(&directive.str, specifier) != 0) {
+ if (dstrcat(&fmt.str, specifier) != 0) {
bfs_perror(ctx, "dstrcat()");
- goto directive_error;
+ goto fmt_error;
}
- if (append_directive(ctx, &expr->printf, &literal, &directive) != 0) {
- goto directive_error;
+ if (append_directive(ctx, expr->printf, &literal, &fmt) != 0) {
+ goto fmt_error;
}
continue;
- directive_error:
- dstrfree(directive.str);
+ fmt_error:
+ dstrfree(fmt.str);
goto error;
}
@@ -891,7 +927,7 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
}
done:
- if (append_literal(ctx, &expr->printf, &literal) != 0) {
+ if (append_literal(ctx, expr->printf, &literal) != 0) {
goto error;
}
dstrfree(literal);
@@ -907,9 +943,9 @@ error:
int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW *ftwbuf) {
int ret = 0, error = 0;
- for (size_t i = 0; i < darray_length(format); ++i) {
- const struct bfs_printf *directive = &format[i];
- if (directive->fn(cfile, directive, ftwbuf) < 0) {
+ for (size_t i = 0; i < format->nfmts; ++i) {
+ const struct bfs_fmt *fmt = &format->fmts[i];
+ if (fmt->fn(cfile, fmt, ftwbuf) < 0) {
ret = -1;
error = errno;
}
@@ -920,8 +956,13 @@ int bfs_printf(CFILE *cfile, const struct bfs_printf *format, const struct BFTW
}
void bfs_printf_free(struct bfs_printf *format) {
- for (size_t i = 0; i < darray_length(format); ++i) {
- dstrfree(format[i].str);
+ if (!format) {
+ return;
+ }
+
+ for (size_t i = 0; i < format->nfmts; ++i) {
+ dstrfree(format->fmts[i].str);
}
- darray_free(format);
+ free(format->fmts);
+ free(format);
}
diff --git a/src/printf.h b/src/printf.h
index a8c5f2a..2bff087 100644
--- a/src/printf.h
+++ b/src/printf.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2017-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Implementation of -printf/-fprintf.
diff --git a/src/pwcache.c b/src/pwcache.c
index 91435bd..af8c237 100644
--- a/src/pwcache.c
+++ b/src/pwcache.c
@@ -1,293 +1,220 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "pwcache.h"
-#include "darray.h"
+#include "alloc.h"
#include "trie.h"
#include <errno.h>
#include <grp.h>
#include <pwd.h>
-#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
+
+/** Represents cache hits for negative results. */
+static void *MISSING = &MISSING;
+
+/** Callback type for bfs_getent(). */
+typedef void *bfs_getent_fn(const void *key, void *ptr, size_t bufsize);
+
+/** Shared scaffolding for get{pw,gr}{nam,?id}_r(). */
+static void *bfs_getent(bfs_getent_fn *fn, const void *key, struct trie_leaf *leaf, struct varena *varena) {
+ if (leaf->value) {
+ errno = 0;
+ return leaf->value == MISSING ? NULL : leaf->value;
+ }
+
+ // _SC_GET{PW,GR}_R_SIZE_MAX tend to be fairly large (~1K). That's okay
+ // for temporary allocations, but for these long-lived ones, let's start
+ // with a smaller buffer.
+ size_t bufsize = 128;
+ void *ptr = varena_alloc(varena, bufsize);
+ if (!ptr) {
+ return NULL;
+ }
+
+ while (true) {
+ void *ret = fn(key, ptr, bufsize);
+ if (ret) {
+ leaf->value = ret;
+ return ret;
+ } else if (errno == 0) {
+ leaf->value = MISSING;
+ break;
+ } else if (errno == ERANGE) {
+ void *next = varena_grow(varena, ptr, &bufsize);
+ if (!next) {
+ break;
+ }
+ ptr = next;
+ } else {
+ break;
+ }
+ }
+
+ varena_free(varena, ptr, bufsize);
+ return NULL;
+}
+
+/**
+ * An arena-allocated struct passwd.
+ */
+struct bfs_passwd {
+ struct passwd pwd;
+ char buf[];
+};
struct bfs_users {
- /** The array of passwd entries. */
- struct passwd *entries;
+ /** bfs_passwd arena. */
+ struct varena varena;
/** A map from usernames to entries. */
struct trie by_name;
/** A map from UIDs to entries. */
struct trie by_uid;
};
-struct bfs_users *bfs_users_parse(void) {
- int error;
-
- struct bfs_users *users = malloc(sizeof(*users));
+struct bfs_users *bfs_users_new(void) {
+ struct bfs_users *users = ALLOC(struct bfs_users);
if (!users) {
return NULL;
}
- users->entries = NULL;
+ VARENA_INIT(&users->varena, struct bfs_passwd, buf);
trie_init(&users->by_name);
trie_init(&users->by_uid);
+ return users;
+}
- setpwent();
-
- while (true) {
- errno = 0;
- struct passwd *ent = getpwent();
- if (!ent) {
- if (errno) {
- error = errno;
- goto fail_end;
- } else {
- break;
- }
- }
+/** bfs_getent() callback for getpwnam_r(). */
+static void *bfs_getpwnam_impl(const void *key, void *ptr, size_t bufsize) {
+ struct bfs_passwd *storage = ptr;
- if (DARRAY_PUSH(&users->entries, ent) != 0) {
- error = errno;
- goto fail_end;
- }
+ struct passwd *ret = NULL;
+ errno = getpwnam_r(key, &storage->pwd, storage->buf, bufsize, &ret);
+ return ret;
+}
- ent = users->entries + darray_length(users->entries) - 1;
- ent->pw_name = strdup(ent->pw_name);
- ent->pw_dir = strdup(ent->pw_dir);
- ent->pw_shell = strdup(ent->pw_shell);
- if (!ent->pw_name || !ent->pw_dir || !ent->pw_shell) {
- error = ENOMEM;
- goto fail_end;
- }
+const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name) {
+ struct trie_leaf *leaf = trie_insert_str(&users->by_name, name);
+ if (!leaf) {
+ return NULL;
}
- endpwent();
-
- for (size_t i = 0; i < darray_length(users->entries); ++i) {
- struct passwd *entry = &users->entries[i];
- struct trie_leaf *leaf = trie_insert_str(&users->by_name, entry->pw_name);
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
-
- leaf = trie_insert_mem(&users->by_uid, &entry->pw_uid, sizeof(entry->pw_uid));
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
- }
+ return bfs_getent(bfs_getpwnam_impl, name, leaf, &users->varena);
+}
- return users;
+/** bfs_getent() callback for getpwuid_r(). */
+static void *bfs_getpwuid_impl(const void *key, void *ptr, size_t bufsize) {
+ const uid_t *uid = key;
+ struct bfs_passwd *storage = ptr;
-fail_end:
- endpwent();
-fail_free:
- bfs_users_free(users);
- errno = error;
- return NULL;
+ struct passwd *ret = NULL;
+ errno = getpwuid_r(*uid, &storage->pwd, storage->buf, bufsize, &ret);
+ return ret;
}
-const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name) {
- const struct trie_leaf *leaf = trie_find_str(&users->by_name, name);
- if (leaf) {
- return leaf->value;
- } else {
+const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid) {
+ struct trie_leaf *leaf = trie_insert_mem(&users->by_uid, &uid, sizeof(uid));
+ if (!leaf) {
return NULL;
}
+
+ return bfs_getent(bfs_getpwuid_impl, &uid, leaf, &users->varena);
}
-const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid) {
- const struct trie_leaf *leaf = trie_find_mem(&users->by_uid, &uid, sizeof(uid));
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
+void bfs_users_flush(struct bfs_users *users) {
+ trie_clear(&users->by_uid);
+ trie_clear(&users->by_name);
+ varena_clear(&users->varena);
}
void bfs_users_free(struct bfs_users *users) {
if (users) {
trie_destroy(&users->by_uid);
trie_destroy(&users->by_name);
-
- for (size_t i = 0; i < darray_length(users->entries); ++i) {
- struct passwd *entry = &users->entries[i];
- free(entry->pw_shell);
- free(entry->pw_dir);
- free(entry->pw_name);
- }
- darray_free(users->entries);
-
+ varena_destroy(&users->varena);
free(users);
}
}
+/**
+ * An arena-allocated struct group.
+ */
+struct bfs_group {
+ struct group grp;
+ char buf[];
+};
+
struct bfs_groups {
- /** The array of group entries. */
- struct group *entries;
+ /** bfs_group arena. */
+ struct varena varena;
/** A map from group names to entries. */
struct trie by_name;
/** A map from GIDs to entries. */
struct trie by_gid;
};
-/**
- * struct group::gr_mem isn't properly aligned on macOS, so do this to avoid
- * ASAN warnings.
- */
-static char *next_gr_mem(void **gr_mem) {
- char *mem;
- memcpy(&mem, *gr_mem, sizeof(mem));
- *gr_mem = (char *)*gr_mem + sizeof(mem);
- return mem;
-}
-
-struct bfs_groups *bfs_groups_parse(void) {
- int error;
-
- struct bfs_groups *groups = malloc(sizeof(*groups));
+struct bfs_groups *bfs_groups_new(void) {
+ struct bfs_groups *groups = ALLOC(struct bfs_groups);
if (!groups) {
return NULL;
}
- groups->entries = NULL;
+ VARENA_INIT(&groups->varena, struct bfs_group, buf);
trie_init(&groups->by_name);
trie_init(&groups->by_gid);
+ return groups;
+}
- setgrent();
-
- while (true) {
- errno = 0;
- struct group *ent = getgrent();
- if (!ent) {
- if (errno) {
- error = errno;
- goto fail_end;
- } else {
- break;
- }
- }
-
- if (DARRAY_PUSH(&groups->entries, ent) != 0) {
- error = errno;
- goto fail_end;
- }
- ent = groups->entries + darray_length(groups->entries) - 1;
-
- void *members = ent->gr_mem;
- ent->gr_mem = NULL;
-
- ent->gr_name = strdup(ent->gr_name);
- if (!ent->gr_name) {
- error = errno;
- goto fail_end;
- }
+/** bfs_getent() callback for getgrnam_r(). */
+static void *bfs_getgrnam_impl(const void *key, void *ptr, size_t bufsize) {
+ struct bfs_group *storage = ptr;
- for (char *mem = next_gr_mem(&members); mem; mem = next_gr_mem(&members)) {
- char *dup = strdup(mem);
- if (!dup) {
- error = errno;
- goto fail_end;
- }
+ struct group *ret = NULL;
+ errno = getgrnam_r(key, &storage->grp, storage->buf, bufsize, &ret);
+ return ret;
+}
- if (DARRAY_PUSH(&ent->gr_mem, &dup) != 0) {
- error = errno;
- free(dup);
- goto fail_end;
- }
- }
+const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name) {
+ struct trie_leaf *leaf = trie_insert_str(&groups->by_name, name);
+ if (!leaf) {
+ return NULL;
}
- endgrent();
-
- for (size_t i = 0; i < darray_length(groups->entries); ++i) {
- struct group *entry = &groups->entries[i];
- struct trie_leaf *leaf = trie_insert_str(&groups->by_name, entry->gr_name);
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
-
- leaf = trie_insert_mem(&groups->by_gid, &entry->gr_gid, sizeof(entry->gr_gid));
- if (leaf) {
- if (!leaf->value) {
- leaf->value = entry;
- }
- } else {
- error = errno;
- goto fail_free;
- }
- }
+ return bfs_getent(bfs_getgrnam_impl, name, leaf, &groups->varena);
+}
- return groups;
+/** bfs_getent() callback for getgrgid_r(). */
+static void *bfs_getgrgid_impl(const void *key, void *ptr, size_t bufsize) {
+ const gid_t *gid = key;
+ struct bfs_group *storage = ptr;
-fail_end:
- endgrent();
-fail_free:
- bfs_groups_free(groups);
- errno = error;
- return NULL;
+ struct group *ret = NULL;
+ errno = getgrgid_r(*gid, &storage->grp, storage->buf, bufsize, &ret);
+ return ret;
}
-const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name) {
- const struct trie_leaf *leaf = trie_find_str(&groups->by_name, name);
- if (leaf) {
- return leaf->value;
- } else {
+const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid) {
+ struct trie_leaf *leaf = trie_insert_mem(&groups->by_gid, &gid, sizeof(gid));
+ if (!leaf) {
return NULL;
}
+
+ return bfs_getent(bfs_getgrgid_impl, &gid, leaf, &groups->varena);
}
-const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid) {
- const struct trie_leaf *leaf = trie_find_mem(&groups->by_gid, &gid, sizeof(gid));
- if (leaf) {
- return leaf->value;
- } else {
- return NULL;
- }
+void bfs_groups_flush(struct bfs_groups *groups) {
+ trie_clear(&groups->by_gid);
+ trie_clear(&groups->by_name);
+ varena_clear(&groups->varena);
}
void bfs_groups_free(struct bfs_groups *groups) {
if (groups) {
trie_destroy(&groups->by_gid);
trie_destroy(&groups->by_name);
-
- for (size_t i = 0; i < darray_length(groups->entries); ++i) {
- struct group *entry = &groups->entries[i];
- for (size_t j = 0; j < darray_length(entry->gr_mem); ++j) {
- free(entry->gr_mem[j]);
- }
- darray_free(entry->gr_mem);
- free(entry->gr_name);
- }
- darray_free(groups->entries);
-
+ varena_destroy(&groups->varena);
free(groups);
}
}
diff --git a/src/pwcache.h b/src/pwcache.h
index f1a1db3..b6c0b67 100644
--- a/src/pwcache.h
+++ b/src/pwcache.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A caching wrapper for /etc/{passwd,group}.
@@ -25,92 +12,112 @@
#include <pwd.h>
/**
- * The user table.
+ * A user cache.
*/
struct bfs_users;
/**
- * Parse the user table.
+ * Create a user cache.
*
* @return
- * The parsed user table, or NULL on failure.
+ * A new user cache, or NULL on failure.
*/
-struct bfs_users *bfs_users_parse(void);
+struct bfs_users *bfs_users_new(void);
/**
* Get a user entry by name.
*
* @param users
- * The user table.
+ * The user cache.
* @param name
* The username to look up.
* @return
- * The matching user, or NULL if not found.
+ * The matching user, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
*/
-const struct passwd *bfs_getpwnam(const struct bfs_users *users, const char *name);
+const struct passwd *bfs_getpwnam(struct bfs_users *users, const char *name);
/**
* Get a user entry by ID.
*
* @param users
- * The user table.
+ * The user cache.
* @param uid
* The ID to look up.
* @return
- * The matching user, or NULL if not found.
+ * The matching user, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
*/
-const struct passwd *bfs_getpwuid(const struct bfs_users *users, uid_t uid);
+const struct passwd *bfs_getpwuid(struct bfs_users *users, uid_t uid);
/**
- * Free a user table.
+ * Flush a user cache.
*
* @param users
- * The user table to free.
+ * The cache to flush.
+ */
+void bfs_users_flush(struct bfs_users *users);
+
+/**
+ * Free a user cache.
+ *
+ * @param users
+ * The user cache to free.
*/
void bfs_users_free(struct bfs_users *users);
/**
- * The group table.
+ * A group cache.
*/
struct bfs_groups;
/**
- * Parse the group table.
+ * Create a group cache.
*
* @return
- * The parsed group table, or NULL on failure.
+ * A new group cache, or NULL on failure.
*/
-struct bfs_groups *bfs_groups_parse(void);
+struct bfs_groups *bfs_groups_new(void);
/**
* Get a group entry by name.
*
* @param groups
- * The group table.
+ * The group cache.
* @param name
* The group name to look up.
* @return
- * The matching group, or NULL if not found.
+ * The matching group, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
*/
-const struct group *bfs_getgrnam(const struct bfs_groups *groups, const char *name);
+const struct group *bfs_getgrnam(struct bfs_groups *groups, const char *name);
/**
* Get a group entry by ID.
*
* @param groups
- * The group table.
+ * The group cache.
* @param uid
* The ID to look up.
* @return
- * The matching group, or NULL if not found.
+ * The matching group, or NULL if not found (errno == 0) or an error
+ * occurred (errno != 0).
+ */
+const struct group *bfs_getgrgid(struct bfs_groups *groups, gid_t gid);
+
+/**
+ * Flush a group cache.
+ *
+ * @param groups
+ * The cache to flush.
*/
-const struct group *bfs_getgrgid(const struct bfs_groups *groups, gid_t gid);
+void bfs_groups_flush(struct bfs_groups *groups);
/**
- * Free a group table.
+ * Free a group cache.
*
* @param groups
- * The group table to free.
+ * The group cache to free.
*/
void bfs_groups_free(struct bfs_groups *groups);
diff --git a/src/sanity.h b/src/sanity.h
new file mode 100644
index 0000000..e168b8f
--- /dev/null
+++ b/src/sanity.h
@@ -0,0 +1,94 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Sanitizer interface.
+ */
+
+#ifndef BFS_SANITY_H
+#define BFS_SANITY_H
+
+#include "prelude.h"
+#include <stddef.h>
+
+#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
+# define SANITIZE_ADDRESS true
+#endif
+
+#if __has_feature(memory_sanitizer) || defined(__SANITIZE_MEMORY__)
+# define SANITIZE_MEMORY true
+#endif
+
+#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__)
+# define SANITIZE_THREAD true
+#endif
+
+// Call macro(ptr, size) or macro(ptr, sizeof(*ptr))
+#define SANITIZE_CALL(...) \
+ SANITIZE_CALL_(__VA_ARGS__, )
+
+#define SANITIZE_CALL_(macro, ptr, ...) \
+ SANITIZE_CALL__(macro, ptr, __VA_ARGS__ sizeof(*(ptr)), )
+
+#define SANITIZE_CALL__(macro, ptr, size, ...) \
+ macro(ptr, size)
+
+#if SANITIZE_ADDRESS
+# include <sanitizer/asan_interface.h>
+
+/**
+ * sanitize_alloc(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as allocated.
+ */
+#define sanitize_alloc(...) SANITIZE_CALL(__asan_unpoison_memory_region, __VA_ARGS__)
+
+/**
+ * sanitize_free(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as free.
+ */
+#define sanitize_free(...) SANITIZE_CALL(__asan_poison_memory_region, __VA_ARGS__)
+
+#else
+# define sanitize_alloc sanitize_uninit
+# define sanitize_free sanitize_uninit
+#endif
+
+#if SANITIZE_MEMORY
+# include <sanitizer/msan_interface.h>
+
+/**
+ * sanitize_init(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as initialized.
+ */
+#define sanitize_init(...) SANITIZE_CALL(__msan_unpoison, __VA_ARGS__)
+
+/**
+ * sanitize_uninit(ptr, size = sizeof(*ptr))
+ *
+ * Mark a memory region as uninitialized.
+ */
+#define sanitize_uninit(...) SANITIZE_CALL(__msan_allocated_memory, __VA_ARGS__)
+
+#else
+# define sanitize_init(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__)
+# define sanitize_uninit(...) SANITIZE_CALL(sanitize_ignore, __VA_ARGS__)
+#endif
+
+/**
+ * Squelch unused variable warnings when not sanitizing.
+ */
+#define sanitize_ignore(ptr, size) ((void)(ptr), (void)(size))
+
+/**
+ * Initialize a variable, unless sanitizers would detect uninitialized uses.
+ */
+#if SANITIZE_MEMORY
+# define uninit(value)
+#else
+# define uninit(value) = value
+#endif
+
+#endif // BFS_SANITY_H
diff --git a/src/stat.c b/src/stat.c
index b67ad0e..f5cf3fe 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -1,62 +1,32 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "stat.h"
-#include "util.h"
-#include <assert.h>
+#include "atomic.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
#include <errno.h>
#include <fcntl.h>
-#include <stdbool.h>
#include <string.h>
-#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/types.h>
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
-#endif
-
-#ifdef STATX_BASIC_STATS
-# define BFS_LIBC_STATX true
-#elif __linux__
-# include <linux/stat.h>
-# include <sys/syscall.h>
-# include <unistd.h>
-#endif
-
-#if BFS_LIBC_STATX || defined(__NR_statx)
-# define BFS_STATX true
-#endif
-
-#if __APPLE__
-# define st_atim st_atimespec
-# define st_ctim st_ctimespec
-# define st_mtim st_mtimespec
-# define st_birthtim st_birthtimespec
+#if BFS_USE_STATX && !BFS_HAS_STATX
+# include <linux/stat.h>
+# include <sys/syscall.h>
+# include <unistd.h>
#endif
const char *bfs_stat_field_name(enum bfs_stat_field field) {
switch (field) {
+ case BFS_STAT_MODE:
+ return "mode";
case BFS_STAT_DEV:
return "device number";
case BFS_STAT_INO:
return "inode nunmber";
- case BFS_STAT_TYPE:
- return "type";
- case BFS_STAT_MODE:
- return "mode";
case BFS_STAT_NLINK:
return "link count";
case BFS_STAT_GID:
@@ -81,60 +51,74 @@ const char *bfs_stat_field_name(enum bfs_stat_field field) {
return "modification time";
}
- assert(!"Unrecognized stat field");
+ bfs_bug("Unrecognized stat field");
return "???";
}
-/**
- * Convert a struct stat to a struct bfs_stat.
- */
-static void bfs_stat_convert(const struct stat *statbuf, struct bfs_stat *buf) {
- buf->mask = 0;
+int bfs_fstatat_flags(enum bfs_stat_flags flags) {
+ int ret = 0;
+
+ if (flags & BFS_STAT_NOFOLLOW) {
+ ret |= AT_SYMLINK_NOFOLLOW;
+ }
+
+#if defined(AT_NO_AUTOMOUNT) && (!__GNU__ || __GLIBC_PREREQ(2, 35))
+ ret |= AT_NO_AUTOMOUNT;
+#endif
- buf->dev = statbuf->st_dev;
- buf->mask |= BFS_STAT_DEV;
+ return ret;
+}
+
+void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src) {
+ dest->mask = 0;
- buf->ino = statbuf->st_ino;
- buf->mask |= BFS_STAT_INO;
+ dest->mode = src->st_mode;
+ dest->mask |= BFS_STAT_MODE;
- buf->mode = statbuf->st_mode;
- buf->mask |= BFS_STAT_TYPE | BFS_STAT_MODE;
+ dest->dev = src->st_dev;
+ dest->mask |= BFS_STAT_DEV;
- buf->nlink = statbuf->st_nlink;
- buf->mask |= BFS_STAT_NLINK;
+ dest->ino = src->st_ino;
+ dest->mask |= BFS_STAT_INO;
- buf->gid = statbuf->st_gid;
- buf->mask |= BFS_STAT_GID;
+ dest->nlink = src->st_nlink;
+ dest->mask |= BFS_STAT_NLINK;
- buf->uid = statbuf->st_uid;
- buf->mask |= BFS_STAT_UID;
+ dest->gid = src->st_gid;
+ dest->mask |= BFS_STAT_GID;
- buf->size = statbuf->st_size;
- buf->mask |= BFS_STAT_SIZE;
+ dest->uid = src->st_uid;
+ dest->mask |= BFS_STAT_UID;
- buf->blocks = statbuf->st_blocks;
- buf->mask |= BFS_STAT_BLOCKS;
+ dest->size = src->st_size;
+ dest->mask |= BFS_STAT_SIZE;
- buf->rdev = statbuf->st_rdev;
- buf->mask |= BFS_STAT_RDEV;
+ dest->blocks = src->st_blocks;
+ dest->mask |= BFS_STAT_BLOCKS;
-#if BSD
- buf->attrs = statbuf->st_flags;
- buf->mask |= BFS_STAT_ATTRS;
+ dest->rdev = src->st_rdev;
+ dest->mask |= BFS_STAT_RDEV;
+
+#if BFS_HAS_ST_FLAGS
+ dest->attrs = src->st_flags;
+ dest->mask |= BFS_STAT_ATTRS;
#endif
- buf->atime = statbuf->st_atim;
- buf->mask |= BFS_STAT_ATIME;
+ dest->atime = ST_ATIM(*src);
+ dest->mask |= BFS_STAT_ATIME;
- buf->ctime = statbuf->st_ctim;
- buf->mask |= BFS_STAT_CTIME;
+ dest->ctime = ST_CTIM(*src);
+ dest->mask |= BFS_STAT_CTIME;
- buf->mtime = statbuf->st_mtim;
- buf->mask |= BFS_STAT_MTIME;
+ dest->mtime = ST_MTIM(*src);
+ dest->mask |= BFS_STAT_MTIME;
-#if __APPLE__ || __FreeBSD__ || __NetBSD__
- buf->btime = statbuf->st_birthtim;
- buf->mask |= BFS_STAT_BTIME;
+#if BFS_HAS_ST_BIRTHTIM
+ dest->btime = src->st_birthtim;
+ dest->mask |= BFS_STAT_BTIME;
+#elif BFS_HAS_ST_BIRTHTIMESPEC
+ dest->btime = src->st_birthtimespec;
+ dest->mask |= BFS_STAT_BTIME;
#endif
}
@@ -145,143 +129,141 @@ static int bfs_stat_impl(int at_fd, const char *at_path, int at_flags, struct bf
struct stat statbuf;
int ret = fstatat(at_fd, at_path, &statbuf, at_flags);
if (ret == 0) {
- bfs_stat_convert(&statbuf, buf);
+ bfs_stat_convert(buf, &statbuf);
}
return ret;
}
-#if BFS_STATX
+#if BFS_USE_STATX
/**
* Wrapper for the statx() system call, which had no glibc wrapper prior to 2.28.
*/
static int bfs_statx(int at_fd, const char *at_path, int at_flags, unsigned int mask, struct statx *buf) {
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // -fsanitize=memory doesn't know about statx(), so tell it the memory
- // got initialized
- memset(buf, 0, sizeof(*buf));
-#endif
-
-#if BFS_LIBC_STATX
- return statx(at_fd, at_path, at_flags, mask, buf);
+#if BFS_HAS_STATX
+ int ret = statx(at_fd, at_path, at_flags, mask, buf);
#else
- return syscall(__NR_statx, at_fd, at_path, at_flags, mask, buf);
+ int ret = syscall(SYS_statx, at_fd, at_path, at_flags, mask, buf);
#endif
+
+ if (ret == 0) {
+ // -fsanitize=memory doesn't know about statx()
+ sanitize_init(buf);
+ }
+
+ return ret;
}
-/**
- * bfs_stat() implementation backed by statx().
- */
-static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
- unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
- struct statx xbuf;
- int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf);
- if (ret != 0) {
- return ret;
+int bfs_statx_flags(enum bfs_stat_flags flags) {
+ int ret = bfs_fstatat_flags(flags);
+
+ if (flags & BFS_STAT_NOSYNC) {
+ ret |= AT_STATX_DONT_SYNC;
}
+ return ret;
+}
+
+int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src) {
// Callers shouldn't have to check anything except the times
- const unsigned int guaranteed = STATX_BASIC_STATS ^ (STATX_ATIME | STATX_CTIME | STATX_MTIME);
- if ((xbuf.stx_mask & guaranteed) != guaranteed) {
+ const unsigned int guaranteed = STATX_BASIC_STATS & ~(STATX_ATIME | STATX_CTIME | STATX_MTIME);
+ if ((src->stx_mask & guaranteed) != guaranteed) {
errno = ENOTSUP;
return -1;
}
- buf->mask = 0;
+ dest->mask = 0;
- buf->dev = bfs_makedev(xbuf.stx_dev_major, xbuf.stx_dev_minor);
- buf->mask |= BFS_STAT_DEV;
+ dest->mode = src->stx_mode;
+ dest->mask |= BFS_STAT_MODE;
- if (xbuf.stx_mask & STATX_INO) {
- buf->ino = xbuf.stx_ino;
- buf->mask |= BFS_STAT_INO;
- }
+ dest->dev = xmakedev(src->stx_dev_major, src->stx_dev_minor);
+ dest->mask |= BFS_STAT_DEV;
- buf->mode = xbuf.stx_mode;
- if (xbuf.stx_mask & STATX_TYPE) {
- buf->mask |= BFS_STAT_TYPE;
- }
- if (xbuf.stx_mask & STATX_MODE) {
- buf->mask |= BFS_STAT_MODE;
- }
+ dest->ino = src->stx_ino;
+ dest->mask |= BFS_STAT_INO;
- if (xbuf.stx_mask & STATX_NLINK) {
- buf->nlink = xbuf.stx_nlink;
- buf->mask |= BFS_STAT_NLINK;
- }
+ dest->nlink = src->stx_nlink;
+ dest->mask |= BFS_STAT_NLINK;
- if (xbuf.stx_mask & STATX_GID) {
- buf->gid = xbuf.stx_gid;
- buf->mask |= BFS_STAT_GID;
- }
+ dest->gid = src->stx_gid;
+ dest->mask |= BFS_STAT_GID;
- if (xbuf.stx_mask & STATX_UID) {
- buf->uid = xbuf.stx_uid;
- buf->mask |= BFS_STAT_UID;
- }
+ dest->uid = src->stx_uid;
+ dest->mask |= BFS_STAT_UID;
- if (xbuf.stx_mask & STATX_SIZE) {
- buf->size = xbuf.stx_size;
- buf->mask |= BFS_STAT_SIZE;
- }
+ dest->size = src->stx_size;
+ dest->mask |= BFS_STAT_SIZE;
- if (xbuf.stx_mask & STATX_BLOCKS) {
- buf->blocks = xbuf.stx_blocks;
- buf->mask |= BFS_STAT_BLOCKS;
- }
+ dest->blocks = src->stx_blocks;
+ dest->mask |= BFS_STAT_BLOCKS;
- buf->rdev = bfs_makedev(xbuf.stx_rdev_major, xbuf.stx_rdev_minor);
- buf->mask |= BFS_STAT_RDEV;
+ dest->rdev = xmakedev(src->stx_rdev_major, src->stx_rdev_minor);
+ dest->mask |= BFS_STAT_RDEV;
- buf->attrs = xbuf.stx_attributes;
- buf->mask |= BFS_STAT_ATTRS;
+ dest->attrs = src->stx_attributes;
+ dest->mask |= BFS_STAT_ATTRS;
- if (xbuf.stx_mask & STATX_ATIME) {
- buf->atime.tv_sec = xbuf.stx_atime.tv_sec;
- buf->atime.tv_nsec = xbuf.stx_atime.tv_nsec;
- buf->mask |= BFS_STAT_ATIME;
+ if (src->stx_mask & STATX_ATIME) {
+ dest->atime.tv_sec = src->stx_atime.tv_sec;
+ dest->atime.tv_nsec = src->stx_atime.tv_nsec;
+ dest->mask |= BFS_STAT_ATIME;
}
- if (xbuf.stx_mask & STATX_BTIME) {
- buf->btime.tv_sec = xbuf.stx_btime.tv_sec;
- buf->btime.tv_nsec = xbuf.stx_btime.tv_nsec;
- buf->mask |= BFS_STAT_BTIME;
+ if (src->stx_mask & STATX_BTIME) {
+ dest->btime.tv_sec = src->stx_btime.tv_sec;
+ dest->btime.tv_nsec = src->stx_btime.tv_nsec;
+ dest->mask |= BFS_STAT_BTIME;
}
- if (xbuf.stx_mask & STATX_CTIME) {
- buf->ctime.tv_sec = xbuf.stx_ctime.tv_sec;
- buf->ctime.tv_nsec = xbuf.stx_ctime.tv_nsec;
- buf->mask |= BFS_STAT_CTIME;
+ if (src->stx_mask & STATX_CTIME) {
+ dest->ctime.tv_sec = src->stx_ctime.tv_sec;
+ dest->ctime.tv_nsec = src->stx_ctime.tv_nsec;
+ dest->mask |= BFS_STAT_CTIME;
}
- if (xbuf.stx_mask & STATX_MTIME) {
- buf->mtime.tv_sec = xbuf.stx_mtime.tv_sec;
- buf->mtime.tv_nsec = xbuf.stx_mtime.tv_nsec;
- buf->mask |= BFS_STAT_MTIME;
+ if (src->stx_mask & STATX_MTIME) {
+ dest->mtime.tv_sec = src->stx_mtime.tv_sec;
+ dest->mtime.tv_nsec = src->stx_mtime.tv_nsec;
+ dest->mask |= BFS_STAT_MTIME;
}
- return ret;
+ return 0;
+}
+
+/**
+ * bfs_stat() implementation backed by statx().
+ */
+static int bfs_statx_impl(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
+ unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
+ struct statx xbuf;
+ int ret = bfs_statx(at_fd, at_path, at_flags, mask, &xbuf);
+ if (ret != 0) {
+ return ret;
+ }
+
+ return bfs_statx_convert(buf, &xbuf);
}
-#endif // BFS_STATX
+#endif // BFS_USE_STATX
/**
* Calls the stat() implementation with explicit flags.
*/
-static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, int x_flags, struct bfs_stat *buf) {
-#if BFS_STATX
- static bool has_statx = true;
-
- if (has_statx) {
- int ret = bfs_statx_impl(at_fd, at_path, at_flags | x_flags, buf);
- // EPERM is commonly returned in a seccomp() sandbox that does
- // not allow statx()
- if (ret != 0 && (errno == ENOSYS || errno == EPERM)) {
- has_statx = false;
+static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, struct bfs_stat *buf) {
+#if BFS_USE_STATX
+ static atomic bool has_statx = true;
+
+ if (load(&has_statx, relaxed)) {
+ int ret = bfs_statx_impl(at_fd, at_path, at_flags, buf);
+ if (ret != 0 && errno_is_like(ENOSYS)) {
+ store(&has_statx, false, relaxed);
} else {
return ret;
}
}
+
+ at_flags &= ~AT_STATX_DONT_SYNC;
#endif
return bfs_stat_impl(at_fd, at_path, at_flags, buf);
@@ -290,49 +272,39 @@ static int bfs_stat_explicit(int at_fd, const char *at_path, int at_flags, int x
/**
* Implements the BFS_STAT_TRYFOLLOW retry logic.
*/
-static int bfs_stat_tryfollow(int at_fd, const char *at_path, int at_flags, int x_flags, enum bfs_stat_flags bfs_flags, struct bfs_stat *buf) {
- int ret = bfs_stat_explicit(at_fd, at_path, at_flags, x_flags, buf);
+static int bfs_stat_tryfollow(int at_fd, const char *at_path, int at_flags, enum bfs_stat_flags bfs_flags, struct bfs_stat *buf) {
+ int ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf);
if (ret != 0
&& (bfs_flags & (BFS_STAT_NOFOLLOW | BFS_STAT_TRYFOLLOW)) == BFS_STAT_TRYFOLLOW
- && is_nonexistence_error(errno))
+ && errno_is_like(ENOENT))
{
at_flags |= AT_SYMLINK_NOFOLLOW;
- ret = bfs_stat_explicit(at_fd, at_path, at_flags, x_flags, buf);
+ ret = bfs_stat_explicit(at_fd, at_path, at_flags, buf);
}
return ret;
}
int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf) {
- int at_flags = 0;
- if (flags & BFS_STAT_NOFOLLOW) {
- at_flags |= AT_SYMLINK_NOFOLLOW;
- }
-
-#if defined(AT_NO_AUTOMOUNT) && (!__GNU__ || BFS_GLIBC_PREREQ(2, 35))
- at_flags |= AT_NO_AUTOMOUNT;
-#endif
-
- int x_flags = 0;
-#ifdef AT_STATX_DONT_SYNC
- if (flags & BFS_STAT_NOSYNC) {
- x_flags |= AT_STATX_DONT_SYNC;
- }
+#if BFS_USE_STATX
+ int at_flags = bfs_statx_flags(flags);
+#else
+ int at_flags = bfs_fstatat_flags(flags);
#endif
if (at_path) {
- return bfs_stat_tryfollow(at_fd, at_path, at_flags, x_flags, flags, buf);
+ return bfs_stat_tryfollow(at_fd, at_path, at_flags, flags, buf);
}
// Check __GNU__ to work around https://lists.gnu.org/archive/html/bug-hurd/2021-12/msg00001.html
#if defined(AT_EMPTY_PATH) && !__GNU__
- static bool has_at_ep = true;
- if (has_at_ep) {
+ static atomic bool has_at_ep = true;
+ if (load(&has_at_ep, relaxed)) {
at_flags |= AT_EMPTY_PATH;
- int ret = bfs_stat_explicit(at_fd, "", at_flags, x_flags, buf);
+ int ret = bfs_stat_explicit(at_fd, "", at_flags, buf);
if (ret != 0 && errno == EINVAL) {
- has_at_ep = false;
+ store(&has_at_ep, false, relaxed);
} else {
return ret;
}
@@ -341,7 +313,7 @@ int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct b
struct stat statbuf;
if (fstat(at_fd, &statbuf) == 0) {
- bfs_stat_convert(&statbuf, buf);
+ bfs_stat_convert(buf, &statbuf);
return 0;
} else {
return -1;
@@ -364,7 +336,7 @@ const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_f
case BFS_STAT_MTIME:
return &buf->mtime;
default:
- assert(!"Invalid stat field for time");
+ bfs_bug("Invalid stat field for time");
errno = EINVAL;
return NULL;
}
diff --git a/src/stat.h b/src/stat.h
index 55c75e9..8d7144d 100644
--- a/src/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,49 @@
#ifndef BFS_STAT_H
#define BFS_STAT_H
-#include "util.h"
+#include "prelude.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 BFS_USE_SYS_PARAM_H
+# include <sys/param.h>
+#endif
+
+#ifdef DEV_BSIZE
+# define BFS_STAT_BLKSIZE DEV_BSIZE
+#elif defined(S_BLKSIZE)
+# define BFS_STAT_BLKSIZE S_BLKSIZE
+#else
+# define BFS_STAT_BLKSIZE 512
#endif
/**
* bfs_stat field bitmask.
*/
enum bfs_stat_field {
- BFS_STAT_DEV = 1 << 0,
- BFS_STAT_INO = 1 << 1,
- BFS_STAT_TYPE = 1 << 2,
- BFS_STAT_MODE = 1 << 3,
- BFS_STAT_NLINK = 1 << 4,
- BFS_STAT_GID = 1 << 5,
- BFS_STAT_UID = 1 << 6,
- BFS_STAT_SIZE = 1 << 7,
- BFS_STAT_BLOCKS = 1 << 8,
- BFS_STAT_RDEV = 1 << 9,
- BFS_STAT_ATTRS = 1 << 10,
- BFS_STAT_ATIME = 1 << 11,
- BFS_STAT_BTIME = 1 << 12,
- BFS_STAT_CTIME = 1 << 13,
- BFS_STAT_MTIME = 1 << 14,
+ BFS_STAT_MODE = 1 << 0,
+ BFS_STAT_DEV = 1 << 1,
+ BFS_STAT_INO = 1 << 2,
+ BFS_STAT_NLINK = 1 << 3,
+ BFS_STAT_GID = 1 << 4,
+ BFS_STAT_UID = 1 << 5,
+ BFS_STAT_SIZE = 1 << 6,
+ BFS_STAT_BLOCKS = 1 << 7,
+ BFS_STAT_RDEV = 1 << 8,
+ BFS_STAT_ATTRS = 1 << 9,
+ BFS_STAT_ATIME = 1 << 10,
+ BFS_STAT_BTIME = 1 << 11,
+ BFS_STAT_CTIME = 1 << 12,
+ BFS_STAT_MTIME = 1 << 13,
};
/**
@@ -73,14 +76,6 @@ enum bfs_stat_flags {
BFS_STAT_NOSYNC = 1 << 2,
};
-#ifdef DEV_BSIZE
-# define BFS_STAT_BLKSIZE DEV_BSIZE
-#elif defined(S_BLKSIZE)
-# define BFS_STAT_BLKSIZE S_BLKSIZE
-#else
-# define BFS_STAT_BLKSIZE 512
-#endif
-
/**
* Facade over struct stat.
*/
@@ -88,12 +83,12 @@ struct bfs_stat {
/** Bitmask indicating filled fields. */
enum bfs_stat_field mask;
+ /** File type and access mode. */
+ mode_t mode;
/** Device ID containing the file. */
dev_t dev;
/** Inode number. */
ino_t ino;
- /** File type and access mode. */
- mode_t mode;
/** Number of hard links. */
nlink_t nlink;
/** Owner group ID. */
@@ -138,6 +133,28 @@ struct bfs_stat {
int bfs_stat(int at_fd, const char *at_path, enum bfs_stat_flags flags, struct bfs_stat *buf);
/**
+ * Convert bfs_stat_flags to fstatat() flags.
+ */
+int bfs_fstatat_flags(enum bfs_stat_flags flags);
+
+/**
+ * Convert struct stat to struct bfs_stat.
+ */
+void bfs_stat_convert(struct bfs_stat *dest, const struct stat *src);
+
+#if BFS_USE_STATX
+/**
+ * Convert bfs_stat_flags to statx() flags.
+ */
+int bfs_statx_flags(enum bfs_stat_flags flags);
+
+/**
+ * Convert struct statx to struct bfs_stat.
+ */
+int bfs_statx_convert(struct bfs_stat *dest, const struct statx *src);
+#endif
+
+/**
* Get a particular time field from a bfs_stat() buffer.
*/
const struct timespec *bfs_stat_time(const struct bfs_stat *buf, enum bfs_stat_field field);
diff --git a/src/thread.c b/src/thread.c
new file mode 100644
index 0000000..3793896
--- /dev/null
+++ b/src/thread.c
@@ -0,0 +1,81 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "thread.h"
+#include "bfstd.h"
+#include "diag.h"
+#include <errno.h>
+#include <pthread.h>
+
+#define THREAD_FALLIBLE(expr) \
+ do { \
+ int err = expr; \
+ if (err == 0) { \
+ return 0; \
+ } else { \
+ errno = err; \
+ return -1; \
+ } \
+ } while (0)
+
+#define THREAD_INFALLIBLE(...) \
+ THREAD_INFALLIBLE_(__VA_ARGS__, 0, )
+
+#define THREAD_INFALLIBLE_(expr, allowed, ...) \
+ int err = expr; \
+ bfs_verify(err == 0 || err == allowed, "%s: %s", #expr, xstrerror(err)); \
+ (void)0
+
+int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg) {
+ THREAD_FALLIBLE(pthread_create(thread, attr, fn, arg));
+}
+
+void thread_join(pthread_t thread, void **ret) {
+ THREAD_INFALLIBLE(pthread_join(thread, ret));
+}
+
+int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) {
+ THREAD_FALLIBLE(pthread_mutex_init(mutex, attr));
+}
+
+void mutex_lock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_lock(mutex));
+}
+
+bool mutex_trylock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_trylock(mutex), EBUSY);
+ return err == 0;
+}
+
+void mutex_unlock(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_unlock(mutex));
+}
+
+void mutex_destroy(pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_mutex_destroy(mutex));
+}
+
+int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) {
+ THREAD_FALLIBLE(pthread_cond_init(cond, attr));
+}
+
+void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
+ THREAD_INFALLIBLE(pthread_cond_wait(cond, mutex));
+}
+
+void cond_signal(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_signal(cond));
+}
+
+void cond_broadcast(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_broadcast(cond));
+}
+
+void cond_destroy(pthread_cond_t *cond) {
+ THREAD_INFALLIBLE(pthread_cond_destroy(cond));
+}
+
+void invoke_once(pthread_once_t *once, once_fn *fn) {
+ THREAD_INFALLIBLE(pthread_once(once, fn));
+}
diff --git a/src/thread.h b/src/thread.h
new file mode 100644
index 0000000..db11bd8
--- /dev/null
+++ b/src/thread.h
@@ -0,0 +1,99 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Wrappers for POSIX threading APIs.
+ */
+
+#ifndef BFS_THREAD_H
+#define BFS_THREAD_H
+
+#include "prelude.h"
+#include <pthread.h>
+
+#if __STDC_VERSION__ < C23 && !defined(thread_local)
+# if BFS_USE_THREADS_H
+# include <threads.h>
+# else
+# define thread_local _Thread_local
+# endif
+#endif
+
+/** Thread entry point type. */
+typedef void *thread_fn(void *arg);
+
+/**
+ * Wrapper for pthread_create().
+ *
+ * @return
+ * 0 on success, -1 on error.
+ */
+int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg);
+
+/**
+ * Wrapper for pthread_join().
+ */
+void thread_join(pthread_t thread, void **ret);
+
+/**
+ * Wrapper for pthread_mutex_init().
+ */
+int mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
+
+/**
+ * Wrapper for pthread_mutex_lock().
+ */
+void mutex_lock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_trylock().
+ *
+ * @return
+ * Whether the mutex was locked.
+ */
+bool mutex_trylock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_unlock().
+ */
+void mutex_unlock(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_mutex_destroy().
+ */
+void mutex_destroy(pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_cond_init().
+ */
+int cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
+
+/**
+ * Wrapper for pthread_cond_wait().
+ */
+void cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
+
+/**
+ * Wrapper for pthread_cond_signal().
+ */
+void cond_signal(pthread_cond_t *cond);
+
+/**
+ * Wrapper for pthread_cond_broadcast().
+ */
+void cond_broadcast(pthread_cond_t *cond);
+
+/**
+ * Wrapper for pthread_cond_destroy().
+ */
+void cond_destroy(pthread_cond_t *cond);
+
+/** pthread_once() callback type. */
+typedef void once_fn(void);
+
+/**
+ * Wrapper for pthread_once().
+ */
+void invoke_once(pthread_once_t *once, once_fn *fn);
+
+#endif // BFS_THREAD_H
diff --git a/src/trie.c b/src/trie.c
index bae9acb..808953e 100644
--- a/src/trie.c
+++ b/src/trie.c
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* This is an implementation of a "qp trie," as documented at
@@ -22,29 +9,29 @@
* look like
*
* A A A A
- * *--->*--->*--->*--->$
- * | | | D D
- * | | +--->*--->$
- * | | B C D
- * | +--->*--->*--->$
- * | D D A A
- * +--->*--->*--->*--->$
- * | D D
- * +--->*--->$
+ * ●───→●───→●───→●───→○
+ * │ │ │ D D
+ * │ │ └───→●───→○
+ * │ │ B C D
+ * │ └───→●───→●───→○
+ * │ D D A A
+ * └───→●───→●───→●───→○
+ * │ D D
+ * └───→●───→○
*
* A compressed (PATRICIA) trie collapses internal nodes that have only a single
* child, like this:
*
* A A AA
- * *--->*--->*---->$
- * | | | DD
- * | | +---->$
- * | | BCD
- * | +----->$
- * | DD AA
- * +---->*---->$
- * | DD
- * +---->$
+ * ●───→●───→●────→○
+ * │ │ │ DD
+ * │ │ └────→○
+ * │ │ BCD
+ * │ └─────→○
+ * │ DD AA
+ * └────→●────→○
+ * │ DD
+ * └────→○
*
* The nodes can be compressed further by dropping the actual compressed
* sequences from the nodes, storing it only in the leaves. This is the
@@ -53,21 +40,39 @@
* branch on, need to be stored in each node.
*
* A A A
- * 0--->1--->2--->AAAA
- * | | | D
- * | | +--->AADD
- * | | B
- * | +--->ABCD
- * | D A
- * +--->2--->DDAA
- * | D
- * +--->DDDD
+ * 0───→1───→2───→AAAA
+ * │ │ │ D
+ * │ │ └───→AADD
+ * │ │ B
+ * │ └───→ABCD
+ * │ D A
+ * └───→2───→DDAA
+ * │ D
+ * └───→DDDD
*
* Nodes are represented very compactly. Rather than a dense array of children,
* a sparse array of only the non-NULL children directly follows the node in
- * memory. A bitmap is used to track which children exist; the index of a child
- * i is found by counting the number of bits below bit i that are set. A tag
- * bit is used to tell pointers to internal nodes apart from pointers to leaves.
+ * memory. A bitmap is used to track which children exist.
+ *
+ * ┌────────────┐
+ * │ [4] [3] [2][1][0] ←─ children
+ * │ ↓ ↓ ↓ ↓ ↓
+ * │ 14 10 6 3 0 ←─ sparse index
+ * │ ↓ ↓ ↓ ↓ ↓
+ * │ 0100010001001001 ←─ bitmap
+ * │
+ * │ To convert a sparse index to a dense index, mask off the bits above it, and
+ * │ count the remaining bits.
+ * │
+ * │ 10 ←─ sparse index
+ * │ ↓
+ * │ 0000001111111111 ←─ mask
+ * │ & 0100010001001001 ←─ bitmap
+ * │ ────────────────
+ * │ = 0000000001001001
+ * │ └──┼──┘
+ * │ [3] ←─ dense index
+ * └───────────────────┘
*
* This implementation tests a whole nibble (half byte/hex digit) at every
* branch, so the bitmap takes up 16 bits. The remainder of a machine word is
@@ -76,25 +81,29 @@
* and insert intermediate singleton "jump" nodes when necessary.
*/
+#include "prelude.h"
#include "trie.h"
-#include "util.h"
-#include <assert.h>
-#include <limits.h>
-#include <stdbool.h>
+#include "alloc.h"
+#include "bit.h"
+#include "diag.h"
+#include "list.h"
#include <stdint.h>
-#include <stdlib.h>
#include <string.h>
-#if CHAR_BIT != 8
-# error "This trie implementation assumes 8-bit bytes."
+bfs_static_assert(CHAR_WIDTH == 8);
+
+#if __i386__ || __x86_64__
+# define trie_clones attr(target_clones("popcnt", "default"))
+#else
+# define trie_clones
#endif
/** Number of bits for the sparse array bitmap, aka the range of a nibble. */
-#define BITMAP_BITS 16
+#define BITMAP_WIDTH 16
/** The number of remaining bits in a word, to hold the offset. */
-#define OFFSET_BITS (sizeof(size_t)*CHAR_BIT - BITMAP_BITS)
+#define OFFSET_WIDTH (SIZE_WIDTH - BITMAP_WIDTH)
/** The highest representable offset (only 64k on a 32-bit architecture). */
-#define OFFSET_MAX (((size_t)1 << OFFSET_BITS) - 1)
+#define OFFSET_MAX (((size_t)1 << OFFSET_WIDTH) - 1)
/**
* An internal node of the trie.
@@ -105,13 +114,13 @@ struct trie_node {
* Bit i will be set if a child exists at logical index i, and its index
* into the array will be popcount(bitmap & ((1 << i) - 1)).
*/
- size_t bitmap : BITMAP_BITS;
+ size_t bitmap : BITMAP_WIDTH;
/**
* The offset into the key in nibbles. This is relative to the parent
* node, to support offsets larger than OFFSET_MAX.
*/
- size_t offset : OFFSET_BITS;
+ size_t offset : OFFSET_WIDTH;
/**
* Flexible array of children. Each pointer uses the lowest bit as a
@@ -128,48 +137,35 @@ static bool trie_is_leaf(uintptr_t ptr) {
/** Decode a pointer to a leaf. */
static struct trie_leaf *trie_decode_leaf(uintptr_t ptr) {
- assert(trie_is_leaf(ptr));
+ bfs_assert(trie_is_leaf(ptr));
return (struct trie_leaf *)(ptr ^ 1);
}
/** Encode a pointer to a leaf. */
static uintptr_t trie_encode_leaf(const struct trie_leaf *leaf) {
uintptr_t ptr = (uintptr_t)leaf ^ 1;
- assert(trie_is_leaf(ptr));
+ bfs_assert(trie_is_leaf(ptr));
return ptr;
}
/** Decode a pointer to an internal node. */
static struct trie_node *trie_decode_node(uintptr_t ptr) {
- assert(!trie_is_leaf(ptr));
+ bfs_assert(!trie_is_leaf(ptr));
return (struct trie_node *)ptr;
}
/** Encode a pointer to an internal node. */
static uintptr_t trie_encode_node(const struct trie_node *node) {
uintptr_t ptr = (uintptr_t)node;
- assert(!trie_is_leaf(ptr));
+ bfs_assert(!trie_is_leaf(ptr));
return ptr;
}
void trie_init(struct trie *trie) {
trie->root = 0;
-}
-
-/** Compute the popcount (Hamming weight) of a bitmap. */
-static unsigned int trie_popcount(unsigned int n) {
-#if __POPCNT__
- // Use the x86 instruction if we have it. Otherwise, GCC generates a
- // library call, so use the below implementation instead.
- return __builtin_popcount(n);
-#else
- // See https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation
- n -= (n >> 1) & 0x5555;
- n = (n & 0x3333) + ((n >> 2) & 0x3333);
- n = (n + (n >> 4)) & 0x0F0F;
- n = (n + (n >> 8)) & 0xFF;
- return n;
-#endif
+ LIST_INIT(trie);
+ VARENA_INIT(&trie->nodes, struct trie_node, children);
+ VARENA_INIT(&trie->leaves, struct trie_leaf, key);
}
/** Extract the nibble at a certain offset from a byte sequence. */
@@ -194,6 +190,7 @@ static unsigned char trie_key_nibble(const void *key, size_t offset) {
* that case, the first mismatch between the key and the representative will be
* the depth at which to make a new branch to insert the key.
*/
+trie_clones
static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) {
uintptr_t ptr = trie->root;
if (!ptr) {
@@ -209,9 +206,10 @@ static struct trie_leaf *trie_representative(const struct trie *trie, const void
if ((offset >> 1) < length) {
unsigned char nibble = trie_key_nibble(key, offset);
unsigned int bit = 1U << nibble;
- if (node->bitmap & bit) {
- index = trie_popcount(node->bitmap & (bit - 1));
- }
+ // bits = bitmap & bit ? bitmap & (bit - 1) : 0
+ unsigned int mask = -!!(node->bitmap & bit);
+ unsigned int bits = node->bitmap & (bit - 1) & mask;
+ index = count_ones(bits);
}
ptr = node->children[index];
}
@@ -219,10 +217,6 @@ static struct trie_leaf *trie_representative(const struct trie *trie, const void
return trie_decode_leaf(ptr);
}
-struct trie_leaf *trie_first_leaf(const struct trie *trie) {
- return trie_representative(trie, NULL, 0);
-}
-
struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) {
return trie_find_mem(trie, key, strlen(key) + 1);
}
@@ -276,7 +270,8 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k
}
}
-struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
+trie_clones
+static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) {
uintptr_t ptr = trie->root;
if (!ptr) {
return NULL;
@@ -303,7 +298,7 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
unsigned char nibble = trie_key_nibble(key, offset);
unsigned int bit = 1U << nibble;
if (node->bitmap & bit) {
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
+ unsigned int index = count_ones(node->bitmap & (bit - 1));
ptr = node->children[index];
} else {
return best;
@@ -318,55 +313,101 @@ struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
return best;
}
+struct trie_leaf *trie_find_prefix(const struct trie *trie, const char *key) {
+ return trie_find_prefix_impl(trie, key);
+}
+
/** Create a new leaf, holding a copy of the given key. */
-static struct trie_leaf *new_trie_leaf(const void *key, size_t length) {
- struct trie_leaf *leaf = malloc(BFS_FLEX_SIZEOF(struct trie_leaf, key, length));
- if (leaf) {
- leaf->value = NULL;
- leaf->length = length;
- memcpy(leaf->key, key, length);
+static struct trie_leaf *trie_leaf_alloc(struct trie *trie, const void *key, size_t length) {
+ struct trie_leaf *leaf = varena_alloc(&trie->leaves, length);
+ if (!leaf) {
+ return NULL;
}
+
+ LIST_ITEM_INIT(leaf);
+ LIST_APPEND(trie, leaf);
+
+ leaf->value = NULL;
+ leaf->length = length;
+ memcpy(leaf->key, key, length);
+
return leaf;
}
-/** Compute the size of a trie node with a certain number of children. */
-static size_t trie_node_size(unsigned int size) {
- // Empty nodes aren't supported
- assert(size > 0);
- // Node size must be a power of two
- assert((size & (size - 1)) == 0);
+/** Free a leaf. */
+static void trie_leaf_free(struct trie *trie, struct trie_leaf *leaf) {
+ LIST_REMOVE(trie, leaf);
+ varena_free(&trie->leaves, leaf, leaf->length);
+}
+
+/** Create a new node. */
+static struct trie_node *trie_node_alloc(struct trie *trie, size_t size) {
+ bfs_assert(has_single_bit(size));
+ return varena_alloc(&trie->nodes, size);
+}
+
+/** Reallocate a trie node. */
+static struct trie_node *trie_node_realloc(struct trie *trie, struct trie_node *node, size_t old_size, size_t new_size) {
+ bfs_assert(has_single_bit(old_size));
+ bfs_assert(has_single_bit(new_size));
+ return varena_realloc(&trie->nodes, node, old_size, new_size);
+}
- return BFS_FLEX_SIZEOF(struct trie_node, children, size);
+/** Free a node. */
+static void trie_node_free(struct trie *trie, struct trie_node *node, size_t size) {
+ bfs_assert(size == (size_t)count_ones(node->bitmap));
+ varena_free(&trie->nodes, node, size);
}
+#if ENDIAN_NATIVE == ENDIAN_LITTLE
+# define TRIE_BSWAP(n) (n)
+#elif ENDIAN_NATIVE == ENDIAN_BIG
+# define TRIE_BSWAP(n) bswap(n)
+#endif
+
/** Find the offset of the first nibble that differs between two keys. */
-static size_t trie_key_mismatch(const void *key1, const void *key2, size_t length) {
- const unsigned char *bytes1 = key1;
- const unsigned char *bytes2 = key2;
- size_t i = 0;
- size_t offset = 0;
- const size_t chunk = sizeof(size_t);
+static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t length) {
+ if (!rep) {
+ return 0;
+ }
+
+ if (rep->length < length) {
+ length = rep->length;
+ }
- for (; i + chunk <= length; i += chunk) {
- if (memcmp(bytes1 + i, bytes2 + i, chunk) != 0) {
+ const char *rep_bytes = rep->key;
+ const char *key_bytes = key;
+
+ size_t i = 0;
+ for (size_t chunk = sizeof(chunk); i + chunk <= length; i += chunk) {
+ size_t rep_chunk, key_chunk;
+ memcpy(&rep_chunk, rep_bytes + i, sizeof(rep_chunk));
+ memcpy(&key_chunk, key_bytes + i, sizeof(key_chunk));
+
+ if (rep_chunk != key_chunk) {
+#ifdef TRIE_BSWAP
+ size_t diff = TRIE_BSWAP(rep_chunk ^ key_chunk);
+ i *= 2;
+ i += trailing_zeros(diff) / 4;
+ return i;
+#else
break;
+#endif
}
}
for (; i < length; ++i) {
- unsigned char b1 = bytes1[i], b2 = bytes2[i];
- if (b1 != b2) {
- offset = (b1 & 0xF) == (b2 & 0xF);
- break;
+ unsigned char diff = rep_bytes[i] ^ key_bytes[i];
+ if (diff) {
+ return 2 * i + !(diff & 0xF);
}
}
- offset |= i << 1;
- return offset;
+ return 2 * i;
}
/**
- * Insert a key into a node. The node must not have a child in that position
+ * Insert a leaf into a node. The node must not have a child in that position
* already. Effectively takes a subtrie like this:
*
* ptr
@@ -383,41 +424,36 @@ static size_t trie_key_mismatch(const void *key1, const void *key2, size_t lengt
* v X
* *--->...
* | Y
- * +--->key
+ * +--->leaf
* | Z
* +--->...
*/
-static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_t length, size_t offset) {
+trie_clones
+static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, unsigned char nibble) {
struct trie_node *node = trie_decode_node(*ptr);
- unsigned int size = trie_popcount(node->bitmap);
+ unsigned int size = count_ones(node->bitmap);
// Double the capacity every power of two
- if ((size & (size - 1)) == 0) {
- node = realloc(node, trie_node_size(2*size));
+ if (has_single_bit(size)) {
+ node = trie_node_realloc(trie, node, size, 2 * size);
if (!node) {
+ trie_leaf_free(trie, leaf);
return NULL;
}
*ptr = trie_encode_node(node);
}
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (!leaf) {
- return NULL;
- }
-
- unsigned char nibble = trie_key_nibble(key, offset);
unsigned int bit = 1U << nibble;
// The child must not already be present
- assert(!(node->bitmap & bit));
+ bfs_assert(!(node->bitmap & bit));
node->bitmap |= bit;
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
- uintptr_t *child = &node->children[index];
- if (index < size) {
- memmove(child + 1, child, (size - index)*sizeof(*child));
+ unsigned int target = count_ones(node->bitmap & (bit - 1));
+ for (size_t i = size; i > target; --i) {
+ node->children[i] = node->children[i - 1];
}
- *child = trie_encode_leaf(leaf);
+ node->children[target] = trie_encode_leaf(leaf);
return leaf;
}
@@ -446,12 +482,12 @@ static struct trie_leaf *trie_node_insert(uintptr_t *ptr, const void *key, size_
* | Y
* +--->key
*/
-static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) {
+static uintptr_t *trie_jump(struct trie *trie, uintptr_t *ptr, const char *key, size_t *offset) {
// We only ever need to jump to leaf nodes, since internal nodes are
// guaranteed to be within OFFSET_MAX anyway
- assert(trie_is_leaf(*ptr));
+ bfs_assert(trie_is_leaf(*ptr));
- struct trie_node *node = malloc(trie_node_size(1));
+ struct trie_node *node = trie_node_alloc(trie, 1);
if (!node) {
return NULL;
}
@@ -482,21 +518,16 @@ static uintptr_t *trie_jump(uintptr_t *ptr, const char *key, size_t *offset) {
* v X
* *--->*...>--->rep
* | Y
- * +--->key
+ * +--->leaf
*/
-static struct trie_leaf *trie_split(uintptr_t *ptr, const void *key, size_t length, struct trie_leaf *rep, size_t offset, size_t mismatch) {
- unsigned char key_nibble = trie_key_nibble(key, mismatch);
+static struct trie_leaf *trie_split(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, struct trie_leaf *rep, size_t offset, size_t mismatch) {
+ unsigned char key_nibble = trie_key_nibble(leaf->key, mismatch);
unsigned char rep_nibble = trie_key_nibble(rep->key, mismatch);
- assert(key_nibble != rep_nibble);
+ bfs_assert(key_nibble != rep_nibble);
- struct trie_node *node = malloc(trie_node_size(2));
+ struct trie_node *node = trie_node_alloc(trie, 2);
if (!node) {
- return NULL;
- }
-
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (!leaf) {
- free(node);
+ trie_leaf_free(trie, leaf);
return NULL;
}
@@ -520,20 +551,22 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) {
return trie_insert_mem(trie, key, strlen(key) + 1);
}
-struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) {
+trie_clones
+static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) {
struct trie_leaf *rep = trie_representative(trie, key, length);
- if (!rep) {
- struct trie_leaf *leaf = new_trie_leaf(key, length);
- if (leaf) {
- trie->root = trie_encode_leaf(leaf);
- }
- return leaf;
+ size_t mismatch = trie_mismatch(rep, key, length);
+ if (mismatch >= (length << 1)) {
+ return rep;
}
- size_t limit = length < rep->length ? length : rep->length;
- size_t mismatch = trie_key_mismatch(key, rep->key, limit);
- if ((mismatch >> 1) >= length) {
- return rep;
+ struct trie_leaf *leaf = trie_leaf_alloc(trie, key, length);
+ if (!leaf) {
+ return NULL;
+ }
+
+ if (!rep) {
+ trie->root = trie_encode_leaf(leaf);
+ return leaf;
}
size_t offset = 0;
@@ -548,38 +581,43 @@ struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t len
unsigned char nibble = trie_key_nibble(key, offset);
unsigned int bit = 1U << nibble;
if (node->bitmap & bit) {
- assert(offset < mismatch);
- unsigned int index = trie_popcount(node->bitmap & (bit - 1));
+ bfs_assert(offset < mismatch);
+ unsigned int index = count_ones(node->bitmap & (bit - 1));
ptr = &node->children[index];
} else {
- assert(offset == mismatch);
- return trie_node_insert(ptr, key, length, offset);
+ bfs_assert(offset == mismatch);
+ return trie_node_insert(trie, ptr, leaf, nibble);
}
}
while (mismatch - offset > OFFSET_MAX) {
- ptr = trie_jump(ptr, key, &offset);
+ ptr = trie_jump(trie, ptr, key, &offset);
if (!ptr) {
+ trie_leaf_free(trie, leaf);
return NULL;
}
}
- return trie_split(ptr, key, length, rep, offset, mismatch);
+ return trie_split(trie, ptr, leaf, rep, offset, mismatch);
+}
+
+struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t length) {
+ return trie_insert_mem_impl(trie, key, length);
}
/** Free a chain of singleton nodes. */
-static void trie_free_singletons(uintptr_t ptr) {
+static void trie_free_singletons(struct trie *trie, 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);
+ bfs_assert(has_single_bit(node->bitmap));
ptr = node->children[0];
- free(node);
+ trie_node_free(trie, node, 1);
}
- free(trie_decode_leaf(ptr));
+ trie_leaf_free(trie, trie_decode_leaf(ptr));
}
/**
@@ -599,7 +637,7 @@ static void trie_free_singletons(uintptr_t ptr) {
* v
* other
*/
-static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) {
+static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_node *parent_node, unsigned int child_index) {
uintptr_t other = parent_node->children[child_index ^ 1];
if (!trie_is_leaf(other)) {
struct trie_node *other_node = trie_decode_node(other);
@@ -611,11 +649,12 @@ static int trie_collapse_node(uintptr_t *parent, struct trie_node *parent_node,
}
*parent = other;
- free(parent_node);
+ trie_node_free(trie, parent_node, 1);
return 0;
}
-void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
+trie_clones
+static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) {
uintptr_t *child = &trie->root;
uintptr_t *parent = NULL;
unsigned int child_bit = 0, child_index = 0;
@@ -623,16 +662,16 @@ void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
while (!trie_is_leaf(*child)) {
struct trie_node *node = trie_decode_node(*child);
offset += node->offset;
- assert((offset >> 1) < leaf->length);
+ bfs_assert((offset >> 1) < leaf->length);
unsigned char nibble = trie_key_nibble(leaf->key, offset);
unsigned int bit = 1U << nibble;
unsigned int bitmap = node->bitmap;
- assert(bitmap & bit);
- unsigned int index = trie_popcount(bitmap & (bit - 1));
+ bfs_assert(bitmap & bit);
+ unsigned int index = count_ones(bitmap & (bit - 1));
// Advance the parent pointer, unless this node had only one child
- if (bitmap & (bitmap - 1)) {
+ if (!has_single_bit(bitmap)) {
parent = child;
child_bit = bit;
child_index = index;
@@ -641,53 +680,50 @@ void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
child = &node->children[index];
}
- assert(trie_decode_leaf(*child) == leaf);
+ bfs_assert(trie_decode_leaf(*child) == leaf);
if (!parent) {
- trie_free_singletons(trie->root);
+ trie_free_singletons(trie, trie->root);
trie->root = 0;
return;
}
struct trie_node *node = trie_decode_node(*parent);
child = node->children + child_index;
- trie_free_singletons(*child);
+ trie_free_singletons(trie, *child);
node->bitmap ^= child_bit;
- unsigned int parent_size = trie_popcount(node->bitmap);
- assert(parent_size > 0);
- if (parent_size == 1 && trie_collapse_node(parent, node, child_index) == 0) {
+ unsigned int parent_size = count_ones(node->bitmap);
+ bfs_assert(parent_size > 0);
+ if (parent_size == 1 && trie_collapse_node(trie, parent, node, child_index) == 0) {
return;
}
if (child_index < parent_size) {
- memmove(child, child + 1, (parent_size - child_index)*sizeof(*child));
+ memmove(child, child + 1, (parent_size - child_index) * sizeof(*child));
}
- if ((parent_size & (parent_size - 1)) == 0) {
- node = realloc(node, trie_node_size(parent_size));
+ if (has_single_bit(parent_size)) {
+ node = trie_node_realloc(trie, node, 2 * parent_size, parent_size);
if (node) {
*parent = trie_encode_node(node);
}
}
}
-/** Free an encoded pointer to a node. */
-static void free_trie_ptr(uintptr_t ptr) {
- if (trie_is_leaf(ptr)) {
- free(trie_decode_leaf(ptr));
- } else {
- struct trie_node *node = trie_decode_node(ptr);
- size_t size = trie_popcount(node->bitmap);
- for (size_t i = 0; i < size; ++i) {
- free_trie_ptr(node->children[i]);
- }
- free(node);
- }
+void trie_remove(struct trie *trie, struct trie_leaf *leaf) {
+ trie_remove_impl(trie, leaf);
+}
+
+void trie_clear(struct trie *trie) {
+ trie->root = 0;
+ LIST_INIT(trie);
+
+ varena_clear(&trie->leaves);
+ varena_clear(&trie->nodes);
}
void trie_destroy(struct trie *trie) {
- if (trie->root) {
- free_trie_ptr(trie->root);
- }
+ varena_destroy(&trie->leaves);
+ varena_destroy(&trie->nodes);
}
diff --git a/src/trie.h b/src/trie.h
index 2d29ac7..4288d76 100644
--- a/src/trie.h
+++ b/src/trie.h
@@ -1,66 +1,46 @@
-/****************************************************************************
- * 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.
- */
+ /** The key itself, stored inline. */
char key[];
};
/**
- * Initialize an empty trie.
+ * A trie that holds a set of fixed- or variable-length strings.
*/
-void trie_init(struct trie *trie);
+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;
+};
/**
- * Get the first (lexicographically earliest) leaf in the trie.
- *
- * @param trie
- * The trie to search.
- * @return
- * The first leaf, or NULL if the trie is empty.
+ * Initialize an empty trie.
*/
-struct trie_leaf *trie_first_leaf(const struct trie *trie);
+void trie_init(struct trie *trie);
/**
* Find the leaf for a string key.
@@ -149,8 +129,19 @@ struct trie_leaf *trie_insert_mem(struct trie *trie, const void *key, size_t len
void trie_remove(struct trie *trie, struct trie_leaf *leaf);
/**
+ * Remove all leaves from a trie.
+ */
+void trie_clear(struct trie *trie);
+
+/**
* Destroy a trie and its contents.
*/
void trie_destroy(struct trie *trie);
+/**
+ * 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/src/typo.c b/src/typo.c
index 4012730..b1c5c44 100644
--- a/src/typo.c
+++ b/src/typo.c
@@ -1,26 +1,14 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#include "typo.h"
#include <limits.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Assume QWERTY layout for now
-static const int key_coords[UCHAR_MAX][3] = {
+static const int8_t key_coords[UCHAR_MAX + 1][3] = {
['`'] = { 0, 0, 0},
['~'] = { 0, 0, 1},
['1'] = { 3, 0, 0},
@@ -125,7 +113,7 @@ static const int key_coords[UCHAR_MAX][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
index 0347aae..13eaa67 100644
--- a/src/typo.h
+++ b/src/typo.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
#ifndef BFS_TYPO_H
#define BFS_TYPO_H
diff --git a/src/util.c b/src/util.c
deleted file mode 100644
index a62e66c..0000000
--- a/src/util.c
+++ /dev/null
@@ -1,510 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "util.h"
-#include "dstring.h"
-#include "xregex.h"
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <langinfo.h>
-#include <nl_types.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-#include <wchar.h>
-
-#if BFS_HAS_SYS_PARAM
-# include <sys/param.h>
-#endif
-
-#if BFS_HAS_SYS_SYSMACROS
-# include <sys/sysmacros.h>
-#elif BFS_HAS_SYS_MKDEV
-# include <sys/mkdev.h>
-#endif
-
-#if BFS_HAS_UTIL
-# include <util.h>
-#endif
-
-char *xreadlinkat(int fd, const char *path, size_t size) {
- ssize_t len;
- char *name = NULL;
-
- if (size == 0) {
- size = 64;
- } else {
- ++size; // NUL terminator
- }
-
- while (true) {
- char *new_name = realloc(name, size);
- if (!new_name) {
- goto error;
- }
- name = new_name;
-
- len = readlinkat(fd, path, name, size);
- if (len < 0) {
- goto error;
- } else if ((size_t)len >= size) {
- size *= 2;
- } else {
- break;
- }
- }
-
- name[len] = '\0';
- return name;
-
-error:
- free(name);
- return NULL;
-}
-
-int dup_cloexec(int fd) {
-#ifdef F_DUPFD_CLOEXEC
- return fcntl(fd, F_DUPFD_CLOEXEC, 0);
-#else
- int ret = dup(fd);
- if (ret < 0) {
- return -1;
- }
-
- if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) {
- close_quietly(ret);
- return -1;
- }
-
- return ret;
-#endif
-}
-
-int pipe_cloexec(int pipefd[2]) {
-#if __linux__ || (BSD && !__APPLE__)
- return pipe2(pipefd, O_CLOEXEC);
-#else
- if (pipe(pipefd) != 0) {
- return -1;
- }
-
- if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) {
- close_quietly(pipefd[1]);
- close_quietly(pipefd[0]);
- return -1;
- }
-
- return 0;
-#endif
-}
-
-/** Get the single character describing the given file type. */
-static char type_char(mode_t mode) {
- switch (mode & S_IFMT) {
- case S_IFREG:
- return '-';
- case S_IFBLK:
- return 'b';
- case S_IFCHR:
- return 'c';
- case S_IFDIR:
- return 'd';
- case S_IFLNK:
- return 'l';
- case S_IFIFO:
- return 'p';
- case S_IFSOCK:
- return 's';
-#ifdef S_IFDOOR
- case S_IFDOOR:
- return 'D';
-#endif
-#ifdef S_IFPORT
- case S_IFPORT:
- return 'P';
-#endif
-#ifdef S_IFWHT
- case S_IFWHT:
- return 'w';
-#endif
- }
-
- return '?';
-}
-
-void xstrmode(mode_t mode, char str[11]) {
- strcpy(str, "----------");
-
- str[0] = type_char(mode);
-
- if (mode & 00400) {
- str[1] = 'r';
- }
- if (mode & 00200) {
- str[2] = 'w';
- }
- if ((mode & 04100) == 04000) {
- str[3] = 'S';
- } else if (mode & 04000) {
- str[3] = 's';
- } else if (mode & 00100) {
- str[3] = 'x';
- }
-
- if (mode & 00040) {
- str[4] = 'r';
- }
- if (mode & 00020) {
- str[5] = 'w';
- }
- if ((mode & 02010) == 02000) {
- str[6] = 'S';
- } else if (mode & 02000) {
- str[6] = 's';
- } else if (mode & 00010) {
- str[6] = 'x';
- }
-
- if (mode & 00004) {
- str[7] = 'r';
- }
- if (mode & 00002) {
- str[8] = 'w';
- }
- if ((mode & 01001) == 01000) {
- str[9] = 'T';
- } else if (mode & 01000) {
- str[9] = 't';
- } else if (mode & 00001) {
- str[9] = 'x';
- }
-}
-
-const char *xbasename(const char *path) {
- const char *i;
-
- // Skip trailing slashes
- for (i = path + strlen(path); i > path && i[-1] == '/'; --i);
-
- // Find the beginning of the name
- for (; i > path && i[-1] != '/'; --i);
-
- // Skip leading slashes
- for (; i[0] == '/' && i[1]; ++i);
-
- return i;
-}
-
-int xfaccessat(int fd, const char *path, int amode) {
- int ret = faccessat(fd, path, amode, 0);
-
-#ifdef AT_EACCESS
- // Some platforms, like Hurd, only support AT_EACCESS. Other platforms,
- // like Android, don't support AT_EACCESS at all.
- if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) {
- ret = faccessat(fd, path, amode, AT_EACCESS);
- }
-#endif
-
- return ret;
-}
-
-int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {
-#if BSD && !__GNU__
- char *str_arg = (char *)*str;
- unsigned long set_arg = 0;
- unsigned long clear_arg = 0;
-
-#if __NetBSD__
- int ret = string_to_flags(&str_arg, &set_arg, &clear_arg);
-#else
- int ret = strtofflags(&str_arg, &set_arg, &clear_arg);
-#endif
-
- *str = str_arg;
- *set = set_arg;
- *clear = clear_arg;
-
- if (ret != 0) {
- errno = EINVAL;
- }
- return ret;
-#else // !BSD
- errno = ENOTSUP;
- return -1;
-#endif
-}
-
-size_t xstrwidth(const char *str) {
- size_t len = strlen(str);
- size_t ret = 0;
-
- mbstate_t mb;
- memset(&mb, 0, sizeof(mb));
-
- while (len > 0) {
- wchar_t wc;
- size_t mblen = mbrtowc(&wc, str, len, &mb);
- int cwidth;
- if (mblen == (size_t)-1) {
- // Invalid byte sequence, assume a single-width '?'
- mblen = 1;
- cwidth = 1;
- memset(&mb, 0, sizeof(mb));
- } else if (mblen == (size_t)-2) {
- // Incomplete byte sequence, assume a single-width '?'
- mblen = len;
- cwidth = 1;
- } else {
- cwidth = wcwidth(wc);
- if (cwidth < 0) {
- cwidth = 0;
- }
- }
-
- str += mblen;
- len -= mblen;
- ret += cwidth;
- }
-
- return ret;
-}
-
-bool is_nonexistence_error(int error) {
- return error == ENOENT || errno == ENOTDIR;
-}
-
-/** Compile and execute a regular expression for xrpmatch(). */
-static int xrpregex(nl_item item, const char *response) {
- const char *pattern = nl_langinfo(item);
- if (!pattern) {
- return -1;
- }
-
- struct bfs_regex *regex;
- int ret = bfs_regcomp(&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;
-}
-
-dev_t bfs_makedev(int ma, int mi) {
-#ifdef makedev
- return makedev(ma, mi);
-#else
- return (ma << 8) | mi;
-#endif
-}
-
-int bfs_major(dev_t dev) {
-#ifdef major
- return major(dev);
-#else
- return dev >> 8;
-#endif
-}
-
-int bfs_minor(dev_t dev) {
-#ifdef minor
- return minor(dev);
-#else
- return dev & 0xFF;
-#endif
-}
-
-size_t xread(int fd, void *buf, size_t nbytes) {
- size_t count = 0;
-
- while (count < nbytes) {
- ssize_t ret = read(fd, (char *)buf + count, nbytes - count);
- if (ret < 0) {
- if (errno == EINTR) {
- continue;
- } else {
- break;
- }
- } else if (ret == 0) {
- // EOF
- errno = 0;
- break;
- } else {
- count += ret;
- }
- }
-
- return count;
-}
-
-size_t xwrite(int fd, const void *buf, size_t nbytes) {
- size_t count = 0;
-
- while (count < nbytes) {
- ssize_t ret = write(fd, (const char *)buf + count, nbytes - count);
- if (ret < 0) {
- if (errno == EINTR) {
- continue;
- } else {
- break;
- }
- } else if (ret == 0) {
- // EOF?
- errno = 0;
- break;
- } else {
- count += ret;
- }
- }
-
- return count;
-}
-
-char *xconfstr(int name) {
- size_t len = confstr(name, NULL, 0);
- if (len == 0) {
- return NULL;
- }
-
- char *str = malloc(len);
- if (!str) {
- return NULL;
- }
-
- if (confstr(name, str, len) != len) {
- free(str);
- return NULL;
- }
-
- return str;
-}
-
-char *xgetdelim(FILE *file, char delim) {
- char *chunk = NULL;
- size_t n = 0;
- ssize_t len = getdelim(&chunk, &n, delim, file);
- if (len >= 0) {
- if (chunk[len] == delim) {
- chunk[len] = '\0';
- }
- return chunk;
- } else {
- free(chunk);
- if (!ferror(file)) {
- errno = 0;
- }
- return NULL;
- }
-}
-
-FILE *xfopen(const char *path, int flags) {
- char mode[4];
-
- switch (flags & O_ACCMODE) {
- case O_RDONLY:
- strcpy(mode, "rb");
- break;
- case O_WRONLY:
- strcpy(mode, "wb");
- break;
- case O_RDWR:
- strcpy(mode, "r+b");
- break;
- default:
- assert(!"Invalid access mode");
- errno = EINVAL;
- return NULL;
- }
-
- if (flags & O_APPEND) {
- mode[0] = 'a';
- }
-
- int fd;
- if (flags & O_CREAT) {
- fd = open(path, flags, 0666);
- } else {
- fd = open(path, flags);
- }
-
- if (fd < 0) {
- return NULL;
- }
-
- FILE *ret = fdopen(fd, mode);
- if (!ret) {
- close_quietly(fd);
- return NULL;
- }
-
- return ret;
-}
-
-int xclose(int fd) {
- int ret = close(fd);
- if (ret != 0) {
- assert(errno != EBADF);
- }
- return ret;
-}
-
-void close_quietly(int fd) {
- int error = errno;
- xclose(fd);
- errno = error;
-}
diff --git a/src/util.h b/src/util.h
deleted file mode 100644
index 2e89af6..0000000
--- a/src/util.h
+++ /dev/null
@@ -1,323 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2016-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-/**
- * Assorted utilities that don't belong anywhere else.
- */
-
-#ifndef BFS_UTIL_H
-#define BFS_UTIL_H
-
-#include <fcntl.h>
-#include <fnmatch.h>
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <sys/types.h>
-
-// Some portability concerns
-
-#ifdef __has_feature
-# define BFS_HAS_FEATURE(feature, fallback) __has_feature(feature)
-#else
-# define BFS_HAS_FEATURE(feature, fallback) fallback
-#endif
-
-#ifdef __has_include
-# define BFS_HAS_INCLUDE(header, fallback) __has_include(header)
-#else
-# define BFS_HAS_INCLUDE(header, fallback) fallback
-#endif
-
-#ifdef __has_c_attribute
-# define BFS_HAS_C_ATTRIBUTE(attr) __has_c_attribute(attr)
-#else
-# define BFS_HAS_C_ATTRIBUTE(attr) false
-#endif
-
-#if __GNUC__ && defined(__has_attribute)
-# define BFS_HAS_GNU_ATTRIBUTE(attr) __has_attribute(attr)
-#else
-# define BFS_HAS_GNU_ATTRIBUTE(attr) false
-#endif
-
-#ifndef BFS_HAS_MNTENT
-# define BFS_HAS_MNTENT BFS_HAS_INCLUDE(<mntent.h>, __GLIBC__)
-#endif
-
-#ifndef BFS_HAS_SYS_ACL
-# define BFS_HAS_SYS_ACL BFS_HAS_INCLUDE(<sys/acl.h>, true)
-#endif
-
-#ifndef BFS_HAS_SYS_CAPABILITY
-# define BFS_HAS_SYS_CAPABILITY BFS_HAS_INCLUDE(<sys/capability.h>, __linux__)
-#endif
-
-#ifndef BFS_HAS_SYS_EXTATTR
-# define BFS_HAS_SYS_EXTATTR BFS_HAS_INCLUDE(<sys/extattr.h>, __FreeBSD__)
-#endif
-
-#ifndef BFS_HAS_SYS_MKDEV
-# define BFS_HAS_SYS_MKDEV BFS_HAS_INCLUDE(<sys/mkdev.h>, false)
-#endif
-
-#ifndef BFS_HAS_SYS_PARAM
-# define BFS_HAS_SYS_PARAM BFS_HAS_INCLUDE(<sys/param.h>, true)
-#endif
-
-#ifndef BFS_HAS_SYS_SYSMACROS
-# define BFS_HAS_SYS_SYSMACROS BFS_HAS_INCLUDE(<sys/sysmacros.h>, __GLIBC__)
-#endif
-
-#ifndef BFS_HAS_SYS_XATTR
-# define BFS_HAS_SYS_XATTR BFS_HAS_INCLUDE(<sys/xattr.h>, __linux__)
-#endif
-
-#ifndef BFS_HAS_UTIL
-# define BFS_HAS_UTIL BFS_HAS_INCLUDE(<util.h>, __NetBSD__)
-#endif
-
-#ifdef __GLIBC_PREREQ
-# define BFS_GLIBC_PREREQ(maj, min) __GLIBC_PREREQ(maj, min)
-#else
-# define BFS_GLIBC_PREREQ(maj, min) false
-#endif
-
-#if !defined(FNM_CASEFOLD) && defined(FNM_IGNORECASE)
-# define FNM_CASEFOLD FNM_IGNORECASE
-#endif
-
-#ifndef O_DIRECTORY
-# define O_DIRECTORY 0
-#endif
-
-#if BFS_HAS_C_ATTRIBUTE(fallthrough)
-# define BFS_FALLTHROUGH [[fallthrough]]
-#elif BFS_HAS_GNU_ATTRIBUTE(fallthrough)
-# define BFS_FALLTHROUGH __attribute__((fallthrough))
-#else
-# define BFS_FALLTHROUGH ((void)0)
-#endif
-
-/**
- * Adds compiler warnings for bad printf()-style function calls, if supported.
- */
-#if BFS_HAS_GNU_ATTRIBUTE(format)
-# define BFS_FORMATTER(fmt, args) __attribute__((format(printf, fmt, args)))
-#else
-# define BFS_FORMATTER(fmt, args)
-#endif
-
-// Lower bound on BFS_FLEX_SIZEOF()
-#define BFS_FLEX_LB(type, member, length) (offsetof(type, member) + sizeof(((type *)NULL)->member[0]) * (length))
-
-// Maximum macro for BFS_FLEX_SIZE()
-#define BFS_FLEX_MAX(a, b) ((a) > (b) ? (a) : (b))
-
-/**
- * Computes the size of a struct containing a flexible array member of the given
- * length.
- *
- * @param type
- * The type of the struct containing the flexible array.
- * @param member
- * The name of the flexible array member.
- * @param length
- * The length of the flexible array.
- */
-#define BFS_FLEX_SIZEOF(type, member, length) \
- (sizeof(type) <= BFS_FLEX_LB(type, member, 0) \
- ? BFS_FLEX_LB(type, member, length) \
- : BFS_FLEX_MAX(sizeof(type), BFS_FLEX_LB(type, member, length)))
-
-/**
- * readlinkat() wrapper that dynamically allocates the result.
- *
- * @param fd
- * The base directory descriptor.
- * @param path
- * The path to the link, relative to fd.
- * @param size
- * An estimate for the size of the link name (pass 0 if unknown).
- * @return The target of the link, allocated with malloc(), or NULL on failure.
- */
-char *xreadlinkat(int fd, const char *path, size_t size);
-
-/**
- * Like dup(), but set the FD_CLOEXEC flag.
- *
- * @param fd
- * The file descriptor to duplicate.
- * @return A duplicated file descriptor, or -1 on failure.
- */
-int dup_cloexec(int fd);
-
-/**
- * Like pipe(), but set the FD_CLOEXEC flag.
- *
- * @param pipefd
- * The array to hold the two file descriptors.
- * @return 0 on success, -1 on failure.
- */
-int pipe_cloexec(int pipefd[2]);
-
-/**
- * Format a mode like ls -l (e.g. -rw-r--r--).
- *
- * @param mode
- * The mode to format.
- * @param str
- * The string to hold the formatted mode.
- */
-void xstrmode(mode_t mode, char str[11]);
-
-/**
- * basename() variant that doesn't modify the input.
- *
- * @param path
- * The path in question.
- * @return A pointer into path at the base name offset.
- */
-const char *xbasename(const char *path);
-
-/**
- * Wrapper for faccessat() that handles some portability issues.
- */
-int xfaccessat(int fd, const char *path, int amode);
-
-/**
- * Portability wrapper for strtofflags().
- *
- * @param str
- * The string to parse. The pointee will be advanced to the first
- * invalid position on error.
- * @param set
- * The flags that are set in the string.
- * @param clear
- * The flags that are cleared in the string.
- * @return
- * 0 on success, -1 on failure.
- */
-int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear);
-
-/**
- * wcswidth() variant that works on narrow strings.
- *
- * @param str
- * The string to measure.
- * @return
- * The likely width of that string in a terminal.
- */
-size_t xstrwidth(const char *str);
-
-/**
- * Return whether an error code is due to a path not existing.
- */
-bool is_nonexistence_error(int error);
-
-/**
- * Process a yes/no prompt.
- *
- * @return 1 for yes, 0 for no, and -1 for unknown.
- */
-int ynprompt(void);
-
-/**
- * Portable version of makedev().
- */
-dev_t bfs_makedev(int ma, int mi);
-
-/**
- * Portable version of major().
- */
-int bfs_major(dev_t dev);
-
-/**
- * Portable version of minor().
- */
-int bfs_minor(dev_t dev);
-
-/**
- * A safe version of read() that handles interrupted system calls and partial
- * reads.
- *
- * @return
- * The number of bytes read. A value != nbytes indicates an error
- * (errno != 0) or end of file (errno == 0).
- */
-size_t xread(int fd, void *buf, size_t nbytes);
-
-/**
- * A safe version of write() that handles interrupted system calls and partial
- * writes.
- *
- * @return
- The number of bytes written. A value != nbytes indicates an error.
- */
-size_t xwrite(int fd, const void *buf, size_t nbytes);
-
-/**
- * Wrapper for confstr() that allocates with malloc().
- *
- * @param name
- * The ID of the confstr to look up.
- * @return
- * The value of the confstr, or NULL on failure.
- */
-char *xconfstr(int name);
-
-/**
- * Convenience wrapper for getdelim().
- *
- * @param file
- * The file to read.
- * @param delim
- * The delimiter character to split on.
- * @return
- * The read chunk (without the delimiter), allocated with malloc().
- * NULL is returned on error (errno != 0) or end of file (errno == 0).
- */
-char *xgetdelim(FILE *file, char delim);
-
-/**
- * fopen() variant that takes open() style flags.
- *
- * @param path
- * The path to open.
- * @param flags
- * Flags to pass to open().
- */
-FILE *xfopen(const char *path, int flags);
-
-/**
- * close() wrapper that asserts the file descriptor is valid.
- *
- * @param fd
- * The file descriptor to close.
- * @return
- * 0 on success, or -1 on error.
- */
-int xclose(int fd);
-
-/**
- * close() variant that preserves errno.
- *
- * @param fd
- * The file descriptor to close.
- */
-void close_quietly(int fd);
-
-#endif // BFS_UTIL_H
diff --git a/src/xregex.c b/src/xregex.c
index 3c3cf35..c2711bc 100644
--- a/src/xregex.c
+++ b/src/xregex.c
@@ -1,35 +1,27 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xregex.h"
-#include "util.h"
-#include <assert.h>
+#include "alloc.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "sanity.h"
+#include "thread.h"
#include <errno.h>
+#include <pthread.h>
#include <stdlib.h>
#include <string.h>
-#if BFS_WITH_ONIGURUMA
-# include <langinfo.h>
-# include <oniguruma.h>
+#if BFS_USE_ONIGURUMA
+# include <langinfo.h>
+# include <oniguruma.h>
#else
-# include <regex.h>
+# include <regex.h>
#endif
struct bfs_regex {
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
unsigned char *pattern;
OnigRegex impl;
int err;
@@ -40,36 +32,34 @@ struct bfs_regex {
#endif
};
-#if BFS_WITH_ONIGURUMA
-/** Get (and initialize) the appropriate encoding for the current locale. */
-static int bfs_onig_encoding(OnigEncoding *penc) {
- static OnigEncoding enc = NULL;
- if (enc) {
- *penc = enc;
- return ONIG_NORMAL;
- }
+#if BFS_USE_ONIGURUMA
+
+static int bfs_onig_status;
+static OnigEncoding bfs_onig_enc;
+/** pthread_once() callback. */
+static void bfs_onig_once(void) {
// Fall back to ASCII by default
- enc = ONIG_ENCODING_ASCII;
+ bfs_onig_enc = ONIG_ENCODING_ASCII;
// Oniguruma has no locale support, so try to guess the right encoding
// from the current locale.
const char *charmap = nl_langinfo(CODESET);
if (charmap) {
-#define BFS_MAP_ENCODING(name, value) \
- do { \
- if (strcmp(charmap, name) == 0) { \
- enc = value; \
- } \
+#define BFS_MAP_ENCODING(name, value) \
+ do { \
+ if (strcmp(charmap, name) == 0) { \
+ bfs_onig_enc = value; \
+ } \
} while (0)
-#define BFS_MAP_ENCODING2(name1, name2, value) \
- do { \
- BFS_MAP_ENCODING(name1, value); \
- BFS_MAP_ENCODING(name2, value); \
+#define BFS_MAP_ENCODING2(name1, name2, value) \
+ do { \
+ BFS_MAP_ENCODING(name1, value); \
+ BFS_MAP_ENCODING(name2, value); \
} while (0)
// These names were found with locale -m on Linux and FreeBSD
-#define BFS_MAP_ISO_8859(n) \
+#define BFS_MAP_ISO_8859(n) \
BFS_MAP_ENCODING2("ISO-8859-" #n, "ISO8859-" #n, ONIG_ENCODING_ISO_8859_ ## n)
BFS_MAP_ISO_8859(1);
@@ -91,7 +81,7 @@ static int bfs_onig_encoding(OnigEncoding *penc) {
BFS_MAP_ENCODING("UTF-8", ONIG_ENCODING_UTF8);
-#define BFS_MAP_EUC(name) \
+#define BFS_MAP_EUC(name) \
BFS_MAP_ENCODING2("EUC-" #name, "euc" #name, ONIG_ENCODING_EUC_ ## name)
BFS_MAP_EUC(JP);
@@ -109,22 +99,29 @@ static int bfs_onig_encoding(OnigEncoding *penc) {
BFS_MAP_ENCODING("GB18030", ONIG_ENCODING_BIG5);
}
- int ret = onig_initialize(&enc, 1);
- if (ret != ONIG_NORMAL) {
- enc = NULL;
+ bfs_onig_status = onig_initialize(&bfs_onig_enc, 1);
+ if (bfs_onig_status != ONIG_NORMAL) {
+ bfs_onig_enc = NULL;
}
- *penc = enc;
- return ret;
+}
+
+/** Initialize Oniguruma. */
+static int bfs_onig_initialize(OnigEncoding *enc) {
+ static pthread_once_t once = PTHREAD_ONCE_INIT;
+ invoke_once(&once, bfs_onig_once);
+
+ *enc = bfs_onig_enc;
+ return bfs_onig_status;
}
#endif
int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_type type, enum bfs_regcomp_flags flags) {
- struct bfs_regex *regex = *preg = malloc(sizeof(*regex));
+ struct bfs_regex *regex = *preg = ALLOC(struct bfs_regex);
if (!regex) {
return -1;
}
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
// onig_error_code_to_str() says
//
// don't call this after the pattern argument of onig_new() is freed
@@ -153,7 +150,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ
syntax = ONIG_SYNTAX_GREP;
break;
}
- assert(syntax);
+ bfs_assert(syntax, "Invalid regex type");
OnigOptionType options = syntax->options;
if (flags & BFS_REGEX_ICASE) {
@@ -161,7 +158,7 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ
}
OnigEncoding enc;
- regex->err = bfs_onig_encoding(&enc);
+ regex->err = bfs_onig_initialize(&enc);
if (regex->err != ONIG_NORMAL) {
return -1;
}
@@ -188,13 +185,10 @@ int bfs_regcomp(struct bfs_regex **preg, const char *pattern, enum bfs_regex_typ
cflags |= REG_ICASE;
}
-#if BFS_HAS_FEATURE(memory_sanitizer, false)
- // https://github.com/google/sanitizers/issues/1496
- memset(&regex->impl, 0, sizeof(regex->impl));
-#endif
-
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
@@ -210,7 +204,7 @@ fail:
int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags flags) {
size_t len = strlen(str);
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
const unsigned char *ustr = (const unsigned char *)str;
const unsigned char *end = ustr + len;
@@ -269,7 +263,7 @@ int bfs_regexec(struct bfs_regex *regex, const char *str, enum bfs_regexec_flags
void bfs_regfree(struct bfs_regex *regex) {
if (regex) {
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
onig_free(regex->impl);
free(regex->pattern);
#else
@@ -281,10 +275,10 @@ void bfs_regfree(struct bfs_regex *regex) {
char *bfs_regerror(const struct bfs_regex *regex) {
if (!regex) {
- return strdup(strerror(ENOMEM));
+ return strdup(xstrerror(ENOMEM));
}
-#if BFS_WITH_ONIGURUMA
+#if BFS_USE_ONIGURUMA
unsigned char *str = malloc(ONIG_MAX_ERROR_MESSAGE_LEN);
if (str) {
onig_error_code_to_str(str, regex->err, &regex->einfo);
diff --git a/src/xregex.h b/src/xregex.h
index b2f56a5..998a2b0 100644
--- a/src/xregex.h
+++ b/src/xregex.h
@@ -1,19 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2022 Tavian Barnes <tavianator@tavianator.com> and bfs *
- * contributors *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com> and the bfs contributors
+// SPDX-License-Identifier: 0BSD
#ifndef BFS_XREGEX_H
#define BFS_XREGEX_H
diff --git a/src/xspawn.c b/src/xspawn.c
index 93c270a..0b0cea4 100644
--- a/src/xspawn.c
+++ b/src/xspawn.c
@@ -1,33 +1,32 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xspawn.h"
-#include "util.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "list.h"
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/resource.h>
#include <sys/types.h>
-#include <sys/wait.h>
#include <unistd.h>
+#if BFS_USE_PATHS_H
+# include <paths.h>
+#endif
+
+#if _POSIX_SPAWN > 0
+# include <spawn.h>
+#endif
+
/**
* Types of spawn actions.
*/
enum bfs_spawn_op {
+ BFS_SPAWN_OPEN,
BFS_SPAWN_CLOSE,
BFS_SPAWN_DUP2,
BFS_SPAWN_FCHDIR,
@@ -38,120 +37,486 @@ enum bfs_spawn_op {
* A spawn action.
*/
struct bfs_spawn_action {
+ /** The next action in the list. */
struct bfs_spawn_action *next;
+ /** This action's operation. */
enum bfs_spawn_op op;
+ /** The input fd (or -1). */
int in_fd;
+ /** The output fd (or -1). */
int out_fd;
- int resource;
- struct rlimit rlimit;
+
+ /** Operation-specific args. */
+ union {
+ /** BFS_SPAWN_OPEN args. */
+ struct {
+ const char *path;
+ int flags;
+ mode_t mode;
+ };
+
+ /** BFS_SPAWN_SETRLIMIT args. */
+ struct {
+ int resource;
+ struct rlimit rlimit;
+ };
+ };
};
int bfs_spawn_init(struct bfs_spawn *ctx) {
ctx->flags = 0;
- ctx->actions = NULL;
- ctx->tail = &ctx->actions;
+ SLIST_INIT(ctx);
+
+#if _POSIX_SPAWN > 0
+ ctx->flags |= BFS_SPAWN_USE_POSIX;
+
+ errno = posix_spawn_file_actions_init(&ctx->actions);
+ if (errno != 0) {
+ return -1;
+ }
+
+ errno = posix_spawnattr_init(&ctx->attr);
+ if (errno != 0) {
+ posix_spawn_file_actions_destroy(&ctx->actions);
+ return -1;
+ }
+#endif
+
return 0;
}
int bfs_spawn_destroy(struct bfs_spawn *ctx) {
- struct bfs_spawn_action *action = ctx->actions;
- while (action) {
- struct bfs_spawn_action *next = action->next;
+#if _POSIX_SPAWN > 0
+ posix_spawnattr_destroy(&ctx->attr);
+ posix_spawn_file_actions_destroy(&ctx->actions);
+#endif
+
+ for_slist (struct bfs_spawn_action, action, ctx) {
free(action);
- action = next;
}
+
return 0;
}
-int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) {
- ctx->flags = flags;
+#if _POSIX_SPAWN > 0
+/** Set some posix_spawnattr flags. */
+attr(maybe_unused)
+static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) {
+ short prev;
+ errno = posix_spawnattr_getflags(&ctx->attr, &prev);
+ if (errno != 0) {
+ return -1;
+ }
+
+ short next = prev | flags;
+ if (next != prev) {
+ errno = posix_spawnattr_setflags(&ctx->attr, next);
+ if (errno != 0) {
+ return -1;
+ }
+ }
+
return 0;
}
+#endif // _POSIX_SPAWN > 0
-/** Add a spawn action to the chain. */
-static struct bfs_spawn_action *bfs_spawn_add(struct bfs_spawn *ctx, enum bfs_spawn_op op) {
- struct bfs_spawn_action *action = malloc(sizeof(*action));
- if (action) {
- action->next = NULL;
- action->op = op;
- action->in_fd = -1;
- action->out_fd = -1;
-
- *ctx->tail = action;
- ctx->tail = &action->next;
+/** Allocate a spawn action. */
+static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) {
+ struct bfs_spawn_action *action = ALLOC(struct bfs_spawn_action);
+ if (!action) {
+ return NULL;
}
+
+ SLIST_ITEM_INIT(action);
+ action->op = op;
+ action->in_fd = -1;
+ action->out_fd = -1;
return action;
}
-int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) {
- if (fd < 0) {
- errno = EBADF;
+int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_OPEN);
+ if (!action) {
return -1;
}
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE);
- if (action) {
- action->out_fd = fd;
- return 0;
- } else {
+#if _POSIX_SPAWN > 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_addopen(&ctx->actions, fd, path, flags, mode);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#endif
+
+ action->out_fd = fd;
+ action->path = path;
+ action->flags = flags;
+ action->mode = mode;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_CLOSE);
+ if (!action) {
return -1;
}
+
+#if _POSIX_SPAWN > 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_addclose(&ctx->actions, fd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#endif
+
+ action->out_fd = fd;
+ SLIST_APPEND(ctx, action);
+ return 0;
}
int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
- if (oldfd < 0 || newfd < 0) {
- errno = EBADF;
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_DUP2);
+ if (!action) {
return -1;
}
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_DUP2);
- if (action) {
- action->in_fd = oldfd;
- action->out_fd = newfd;
- return 0;
- } else {
- return -1;
+#if _POSIX_SPAWN > 0
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = posix_spawn_file_actions_adddup2(&ctx->actions, oldfd, newfd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
}
+#endif
+
+ action->in_fd = oldfd;
+ action->out_fd = newfd;
+ SLIST_APPEND(ctx, action);
+ return 0;
}
+/**
+ * https://www.austingroupbugs.net/view.php?id=1208#c4830 says:
+ *
+ * ... a search of the directories passed as the environment variable
+ * PATH ..., using the working directory of the child process after all
+ * file_actions have been performed.
+ *
+ * but macOS and NetBSD resolve the PATH *before* file_actions (because there
+ * posix_spawn() is its own syscall).
+ */
+#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__)
+
int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
- if (fd < 0) {
- errno = EBADF;
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR);
+ if (!action) {
return -1;
}
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR);
- if (action) {
- action->in_fd = fd;
- return 0;
+#if BFS_HAS_POSIX_SPAWN_ADDFCHDIR
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir
+#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP
+# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np
+#endif
+
+#if _POSIX_SPAWN > 0 && defined(BFS_POSIX_SPAWN_FCHDIR)
+ if (ctx->flags & BFS_SPAWN_USE_POSIX) {
+ errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd);
+ if (errno != 0) {
+ free(action);
+ return -1;
+ }
+ }
+#else
+ ctx->flags &= ~BFS_SPAWN_USE_POSIX;
+#endif
+
+ action->in_fd = fd;
+ SLIST_APPEND(ctx, action);
+ return 0;
+}
+
+int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) {
+ struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_SETRLIMIT);
+ if (!action) {
+ goto fail;
+ }
+
+#ifdef POSIX_SPAWN_SETRLIMIT
+ if (bfs_spawn_addflags(ctx, POSIX_SPAWN_SETRLIMIT) != 0) {
+ goto fail;
+ }
+
+ errno = posix_spawnattr_setrlimit(&ctx->attr, resource, rl);
+ if (errno != 0) {
+ goto fail;
+ }
+#else
+ ctx->flags &= ~BFS_SPAWN_USE_POSIX;
+#endif
+
+ action->resource = resource;
+ action->rlimit = *rl;
+ SLIST_APPEND(ctx, action);
+ return 0;
+
+fail:
+ free(action);
+ return -1;
+}
+
+/**
+ * Context for resolving executables in the $PATH.
+ */
+struct bfs_resolver {
+ /** The executable to spawn. */
+ const char *exe;
+ /** The $PATH to resolve in. */
+ char *path;
+ /** A buffer to hold the resolved path. */
+ char *buf;
+ /** The size of the buffer. */
+ size_t len;
+ /** Whether the executable is already resolved. */
+ bool done;
+ /** Whether to free(path). */
+ bool free;
+};
+
+/** Free a $PATH resolution context. */
+static void bfs_resolve_free(struct bfs_resolver *res) {
+ if (res->free) {
+ free(res->path);
+ }
+ free(res->buf);
+}
+
+/** Get the next component in the $PATH. */
+static bool bfs_resolve_next(const char **path, const char **next, size_t *len) {
+ *path = *next;
+ if (!*path) {
+ return false;
+ }
+
+ *next = strchr(*path, ':');
+ if (*next) {
+ *len = *next - *path;
+ ++*next;
} else {
- return -1;
+ *len = strlen(*path);
}
+
+ if (*len == 0) {
+ // POSIX 8.3: "A zero-length prefix is a legacy feature that
+ // indicates the current working directory."
+ *path = ".";
+ *len = 1;
+ }
+
+ return true;
}
-int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) {
- struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_SETRLIMIT);
- if (action) {
- action->resource = resource;
- action->rlimit = *rl;
+/** Finish resolving an executable, potentially from the child process. */
+static int bfs_resolve_late(struct bfs_resolver *res) {
+ if (res->done) {
return 0;
+ }
+
+ char *buf = res->buf;
+ char *end = buf + res->len;
+
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ char *cur = xstpencpy(buf, end, path, len);
+ cur = xstpecpy(cur, end, "/");
+ cur = xstpecpy(cur, end, res->exe);
+ if (cur == end) {
+ bfs_bug("PATH resolution buffer too small");
+ errno = ENOMEM;
+ return -1;
+ }
+
+ if (xfaccessat(AT_FDCWD, buf, X_OK) == 0) {
+ res->exe = buf;
+ res->done = true;
+ return 0;
+ }
+ }
+
+ errno = ENOENT;
+ return -1;
+}
+
+/** Check if we can skip path resolution entirely. */
+static bool bfs_can_skip_resolve(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (ctx && !(ctx->flags & BFS_SPAWN_USE_PATH)) {
+ return true;
+ }
+
+ if (strchr(res->exe, '/')) {
+ return true;
+ }
+
+ return false;
+}
+
+/** Check if any $PATH components are relative. */
+static bool bfs_resolve_relative(const struct bfs_resolver *res) {
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ if (path[0] != '/') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** Check if we can resolve the executable before file actions. */
+static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (!bfs_resolve_relative(res)) {
+ return true;
+ }
+
+ if (ctx) {
+ for_slist (const struct bfs_spawn_action, action, ctx) {
+ if (action->op == BFS_SPAWN_FCHDIR) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+/** Get the required path resolution buffer size. */
+static size_t bfs_resolve_capacity(const struct bfs_resolver *res) {
+ size_t max = 0;
+
+ const char *path;
+ const char *next = res->path;
+ size_t len;
+ while (bfs_resolve_next(&path, &next, &len)) {
+ if (len > max) {
+ max = len;
+ }
+ }
+
+ // path + "/" + exe + '\0'
+ return max + 1 + strlen(res->exe) + 1;
+}
+
+/** Begin resolving an executable, from the parent process. */
+static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const struct bfs_spawn *ctx) {
+ *res = (struct bfs_resolver) {
+ .exe = exe,
+ };
+
+ if (bfs_can_skip_resolve(res, ctx)) {
+ res->done = true;
+ return 0;
+ }
+
+ res->path = getenv("PATH");
+ if (!res->path) {
+#if defined(_CS_PATH)
+ res->path = xconfstr(_CS_PATH);
+ res->free = true;
+#elif defined(_PATH_DEFPATH)
+ res->path = _PATH_DEFPATH;
+#else
+ errno = ENOENT;
+#endif
+ }
+ if (!res->path) {
+ goto fail;
+ }
+
+ bool can_finish = bfs_can_resolve_early(res, ctx);
+
+#if BFS_POSIX_SPAWNP_AFTER_FCHDIR
+ bool use_posix = ctx && (ctx->flags & BFS_SPAWN_USE_POSIX);
+ if (!can_finish && use_posix) {
+ // posix_spawnp() will do the resolution, so don't bother
+ // allocating a buffer
+ return 0;
+ }
+#endif
+
+ res->len = bfs_resolve_capacity(res);
+ res->buf = malloc(res->len);
+ if (!res->buf) {
+ goto fail;
+ }
+
+ if (can_finish && bfs_resolve_late(res) != 0) {
+ goto fail;
+ }
+
+ return 0;
+
+fail:
+ bfs_resolve_free(res);
+ return -1;
+}
+
+#if _POSIX_SPAWN > 0
+
+/** bfs_spawn() implementation using posix_spawn(). */
+static pid_t bfs_posix_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ pid_t ret;
+
+ if (res->done) {
+ errno = posix_spawn(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp);
} else {
+ errno = posix_spawnp(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp);
+ }
+
+ if (errno != 0) {
return -1;
}
+
+ return ret;
}
-/** Actually exec() the new process. */
-static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) {
- int error;
- const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL;
+/** Check if we can use posix_spawn(). */
+static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (!(ctx->flags & BFS_SPAWN_USE_POSIX)) {
+ return false;
+ }
+#if !BFS_POSIX_SPAWNP_AFTER_FCHDIR
+ if (!res->done) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+#endif // _POSIX_SPAWN > 0
+
+/** Actually exec() the new process. */
+static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) {
xclose(pipefd[0]);
- for (const struct bfs_spawn_action *action = actions; action; action = action->next) {
+ for_slist (const struct bfs_spawn_action, action, ctx) {
+ int fd;
+
// Move the error-reporting pipe out of the way if necessary...
if (action->out_fd == pipefd[1]) {
- int fd = dup_cloexec(pipefd[1]);
+ fd = dup_cloexec(pipefd[1]);
if (fd < 0) {
goto fail;
}
@@ -166,6 +531,17 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **
}
switch (action->op) {
+ case BFS_SPAWN_OPEN:
+ fd = open(action->path, action->flags, action->mode);
+ if (fd < 0) {
+ goto fail;
+ }
+ if (fd != action->out_fd) {
+ if (dup2(fd, action->out_fd) < 0) {
+ goto fail;
+ }
+ }
+ break;
case BFS_SPAWN_CLOSE:
if (close(action->out_fd) != 0) {
goto fail;
@@ -189,38 +565,28 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **
}
}
- execve(exe, argv, envp);
+ if (bfs_resolve_late(res) != 0) {
+ goto fail;
+ }
-fail:
- error = errno;
+ execve(res->exe, argv, envp);
+
+fail:;
+ int error = errno;
// In case of a write error, the parent will still see that we exited
// unsuccessfully, but won't know why
- (void) xwrite(pipefd[1], &error, sizeof(error));
+ (void)xwrite(pipefd[1], &error, sizeof(error));
xclose(pipefd[1]);
_Exit(127);
}
-pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
- extern char **environ;
- if (!envp) {
- envp = environ;
- }
-
- enum bfs_spawn_flags flags = ctx ? ctx->flags : 0;
- char *resolved = NULL;
- if (flags & BFS_SPAWN_USEPATH) {
- exe = resolved = bfs_spawn_resolve(exe);
- if (!resolved) {
- return -1;
- }
- }
-
+/** bfs_spawn() implementation using fork()/exec(). */
+static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
// Use a pipe to report errors from the child
int pipefd[2];
if (pipe_cloexec(pipefd) != 0) {
- free(resolved);
return -1;
}
@@ -228,23 +594,21 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char
if (pid < 0) {
close_quietly(pipefd[1]);
close_quietly(pipefd[0]);
- free(resolved);
return -1;
} else if (pid == 0) {
// Child
- bfs_spawn_exec(exe, ctx, argv, envp, pipefd);
+ bfs_spawn_exec(res, ctx, argv, envp, pipefd);
}
// Parent
xclose(pipefd[1]);
- free(resolved);
int error;
ssize_t nbytes = xread(pipefd[0], &error, sizeof(error));
xclose(pipefd[0]);
if (nbytes == sizeof(error)) {
int wstatus;
- waitpid(pid, &wstatus, 0);
+ xwaitpid(pid, &wstatus, 0);
errno = error;
return -1;
}
@@ -252,67 +616,54 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char
return pid;
}
-char *bfs_spawn_resolve(const char *exe) {
- if (strchr(exe, '/')) {
- return strdup(exe);
+/** Call the right bfs_spawn() implementation. */
+static pid_t bfs_spawn_impl(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) {
+#if _POSIX_SPAWN > 0
+ if (bfs_use_posix_spawn(res, ctx)) {
+ return bfs_posix_spawn(res, ctx, argv, envp);
}
+#endif
- const char *path = getenv("PATH");
+ return bfs_fork_spawn(res, ctx, argv, envp);
+}
- char *confpath = NULL;
- if (!path) {
- path = confpath = xconfstr(_CS_PATH);
- if (!path) {
- return NULL;
- }
+pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ // execvp()/posix_spawnp() are typically implemented with repeated
+ // execv() calls for each $PATH component until one succeeds. It's
+ // faster to resolve the full path ahead of time.
+ struct bfs_resolver res;
+ if (bfs_resolve_early(&res, exe, ctx) != 0) {
+ return -1;
}
- size_t cap = 0;
- char *ret = NULL;
- while (true) {
- const char *end = strchr(path, ':');
- size_t len = end ? (size_t)(end - path) : strlen(path);
-
- // POSIX 8.3: "A zero-length prefix is a legacy feature that
- // indicates the current working directory."
- if (len == 0) {
- path = ".";
- len = 1;
- }
-
- size_t total = len + 1 + strlen(exe) + 1;
- if (cap < total) {
- char *grown = realloc(ret, total);
- if (!grown) {
- goto fail;
- }
- ret = grown;
- cap = total;
- }
-
- memcpy(ret, path, len);
- if (ret[len - 1] != '/') {
- ret[len++] = '/';
- }
- strcpy(ret + len, exe);
+ extern char **environ;
+ if (!envp) {
+ envp = environ;
+ }
- if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) {
- break;
- }
+ pid_t ret = bfs_spawn_impl(&res, ctx, argv, envp);
+ bfs_resolve_free(&res);
+ return ret;
+}
- if (!end) {
- errno = ENOENT;
- goto fail;
- }
+char *bfs_spawn_resolve(const char *exe) {
+ struct bfs_resolver res;
+ if (bfs_resolve_early(&res, exe, NULL) != 0) {
+ return NULL;
+ }
+ if (bfs_resolve_late(&res) != 0) {
+ bfs_resolve_free(&res);
+ return NULL;
+ }
- path = end + 1;
+ char *ret;
+ if (res.exe == res.buf) {
+ ret = res.buf;
+ res.buf = NULL;
+ } else {
+ ret = strdup(res.exe);
}
- free(confpath);
+ bfs_resolve_free(&res);
return ret;
-
-fail:
- free(confpath);
- free(ret);
- return NULL;
}
diff --git a/src/xspawn.h b/src/xspawn.h
index cd6a42e..6a8f54a 100644
--- a/src/xspawn.h
+++ b/src/xspawn.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* A process-spawning library inspired by posix_spawn().
@@ -21,74 +8,98 @@
#ifndef BFS_XSPAWN_H
#define BFS_XSPAWN_H
+#include "prelude.h"
#include <sys/resource.h>
#include <sys/types.h>
+#include <unistd.h>
+
+#if _POSIX_SPAWN > 0
+# include <spawn.h>
+#endif
/**
* bfs_spawn() flags.
*/
enum bfs_spawn_flags {
/** Use the PATH variable to resolve the executable (like execvp()). */
- BFS_SPAWN_USEPATH = 1 << 0,
+ BFS_SPAWN_USE_PATH = 1 << 0,
+ /** Whether posix_spawn() can be used. */
+ BFS_SPAWN_USE_POSIX = 1 << 1,
};
/**
* bfs_spawn() attributes, controlling the context of the new process.
*/
struct bfs_spawn {
+ /** Spawn flags. */
enum bfs_spawn_flags flags;
- struct bfs_spawn_action *actions;
+
+ /** Linked list of actions. */
+ struct bfs_spawn_action *head;
struct bfs_spawn_action **tail;
+
+#if _POSIX_SPAWN > 0
+ /** posix_spawn() context, for when we can use it. */
+ posix_spawn_file_actions_t actions;
+ posix_spawnattr_t attr;
+#endif
};
/**
* Create a new bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
int bfs_spawn_init(struct bfs_spawn *ctx);
/**
* Destroy a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
int bfs_spawn_destroy(struct bfs_spawn *ctx);
/**
- * Set the flags for a bfs_spawn() context.
+ * Add an open() action to a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
-int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags);
+int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode);
/**
* Add a close() action to a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd);
/**
* Add a dup2() action to a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd);
/**
* Add an fchdir() action to a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd);
/**
- * Add a setrlimit() action to a bfs_spawn() context.
+ * Apply setrlimit() to a bfs_spawn() context.
*
- * @return 0 on success, -1 on failure.
+ * @return
+ * 0 on success, -1 on failure.
*/
-int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl);
+int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl);
/**
* Spawn a new process.
@@ -108,7 +119,7 @@ int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rli
pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp);
/**
- * Look up an executable in the current PATH, as BFS_SPAWN_USEPATH or execvp()
+ * Look up an executable in the current PATH, as BFS_SPAWN_USE_PATH or execvp()
* would do.
*
* @param exe
diff --git a/src/xtime.c b/src/xtime.c
index 8ca963b..91ed915 100644
--- a/src/xtime.c
+++ b/src/xtime.c
@@ -1,56 +1,15 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+#include "prelude.h"
#include "xtime.h"
+#include "bfstd.h"
+#include "diag.h"
#include <errno.h>
#include <limits.h>
-#include <stdbool.h>
-#include <stdlib.h>
+#include <sys/time.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;
- }
-}
+#include <unistd.h>
int xmktime(struct tm *tm, time_t *timep) {
*timep = mktime(tm);
@@ -59,7 +18,8 @@ int xmktime(struct tm *tm, time_t *timep) {
int error = errno;
struct tm tmp;
- if (xlocaltime(timep, &tmp) != 0) {
+ if (!localtime_r(timep, &tmp)) {
+ bfs_bug("localtime_r(-1): %s", xstrerror(errno));
return -1;
}
@@ -90,7 +50,7 @@ static int safe_add(int *value, int delta) {
static int floor_div(int n, int d) {
int a = n < 0;
- return (n + a)/d - a;
+ return (n + a) / d - a;
}
static int wrap(int *value, int max, int *next) {
@@ -102,80 +62,84 @@ static int wrap(int *value, int max, int *next) {
static int month_length(int year, int month) {
static const int month_lengths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int ret = month_lengths[month];
- if (month == 1 && year%4 == 0 && (year%100 != 0 || (year + 300)%400 == 0)) {
+ if (month == 1 && year % 4 == 0 && (year % 100 != 0 || (year + 300) % 400 == 0)) {
++ret;
}
return ret;
}
int xtimegm(struct tm *tm, time_t *timep) {
- tm->tm_isdst = 0;
+ struct tm copy = *tm;
+ copy.tm_isdst = 0;
- if (wrap(&tm->tm_sec, 60, &tm->tm_min) != 0) {
+ if (wrap(&copy.tm_sec, 60, &copy.tm_min) != 0) {
goto overflow;
}
- if (wrap(&tm->tm_min, 60, &tm->tm_hour) != 0) {
+ if (wrap(&copy.tm_min, 60, &copy.tm_hour) != 0) {
goto overflow;
}
- if (wrap(&tm->tm_hour, 24, &tm->tm_mday) != 0) {
+ 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(&tm->tm_mon, 12, &tm->tm_year) != 0) {
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
goto overflow;
}
- if (tm->tm_mday < 1) {
+ if (copy.tm_mday < 1) {
do {
- --tm->tm_mon;
- if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) {
+ --copy.tm_mon;
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
goto overflow;
}
- tm->tm_mday += month_length(tm->tm_year, tm->tm_mon);
- } while (tm->tm_mday < 1);
+ copy.tm_mday += month_length(copy.tm_year, copy.tm_mon);
+ } while (copy.tm_mday < 1);
} else {
while (true) {
- int days = month_length(tm->tm_year, tm->tm_mon);
- if (tm->tm_mday <= days) {
+ int days = month_length(copy.tm_year, copy.tm_mon);
+ if (copy.tm_mday <= days) {
break;
}
- tm->tm_mday -= days;
- ++tm->tm_mon;
- if (wrap(&tm->tm_mon, 12, &tm->tm_year) != 0) {
+ copy.tm_mday -= days;
+ ++copy.tm_mon;
+ if (wrap(&copy.tm_mon, 12, &copy.tm_year) != 0) {
goto overflow;
}
}
}
- tm->tm_yday = 0;
- for (int i = 0; i < tm->tm_mon; ++i) {
- tm->tm_yday += month_length(tm->tm_year, i);
+ copy.tm_yday = 0;
+ for (int i = 0; i < copy.tm_mon; ++i) {
+ copy.tm_yday += month_length(copy.tm_year, i);
}
- tm->tm_yday += tm->tm_mday - 1;
+ copy.tm_yday += copy.tm_mday - 1;
int leap_days;
// Compute floor((year - 69)/4) - floor((year - 1)/100) + floor((year + 299)/400) without overflows
- if (tm->tm_year >= 0) {
- leap_days = floor_div(tm->tm_year - 69, 4) - floor_div(tm->tm_year - 1, 100) + floor_div(tm->tm_year - 101, 400) + 1;
+ if (copy.tm_year >= 0) {
+ leap_days = floor_div(copy.tm_year - 69, 4) - floor_div(copy.tm_year - 1, 100) + floor_div(copy.tm_year - 101, 400) + 1;
} else {
- leap_days = floor_div(tm->tm_year + 3, 4) - floor_div(tm->tm_year + 99, 100) + floor_div(tm->tm_year + 299, 400) - 17;
+ leap_days = floor_div(copy.tm_year + 3, 4) - floor_div(copy.tm_year + 99, 100) + floor_div(copy.tm_year + 299, 400) - 17;
}
- long long epoch_days = 365LL*(tm->tm_year - 70) + leap_days + tm->tm_yday;
- tm->tm_wday = (epoch_days + 4)%7;
- if (tm->tm_wday < 0) {
- tm->tm_wday += 7;
+ long long epoch_days = 365LL * (copy.tm_year - 70) + leap_days + copy.tm_yday;
+ copy.tm_wday = (epoch_days + 4) % 7;
+ if (copy.tm_wday < 0) {
+ copy.tm_wday += 7;
}
- long long epoch_time = tm->tm_sec + 60*(tm->tm_min + 60*(tm->tm_hour + 24*epoch_days));
- *timep = (time_t)epoch_time;
- if ((long long)*timep != epoch_time) {
+ long long epoch_time = copy.tm_sec + 60 * (copy.tm_min + 60 * (copy.tm_hour + 24 * epoch_days));
+ time_t time = (time_t)epoch_time;
+ if ((long long)time != epoch_time) {
goto overflow;
}
+
+ *tm = copy;
+ *timep = time;
return 0;
overflow:
@@ -183,23 +147,33 @@ overflow:
return -1;
}
+/** Parse a decimal digit. */
+static int xgetdigit(char c) {
+ int ret = c - '0';
+ if (ret < 0 || ret > 9) {
+ return -1;
+ } else {
+ return ret;
+ }
+}
+
/** Parse some digits from a timestamp. */
-static int parse_timestamp_part(const char **str, size_t n, int *result) {
- char buf[n + 1];
+static int xgetpart(const char **str, size_t n, int *result) {
+ *result = 0;
+
for (size_t i = 0; i < n; ++i, ++*str) {
- char c = **str;
- if (c < '0' || c > '9') {
+ int dig = xgetdigit(**str);
+ if (dig < 0) {
return -1;
}
- buf[i] = c;
+ *result *= 10;
+ *result += dig;
}
- buf[n] = '\0';
- *result = atoi(buf);
return 0;
}
-int parse_timestamp(const char *str, struct timespec *result) {
+int xgetdate(const char *str, struct timespec *result) {
struct tm tm = {
.tm_isdst = -1,
};
@@ -210,7 +184,7 @@ int parse_timestamp(const char *str, struct timespec *result) {
bool local = true;
// YYYY
- if (parse_timestamp_part(&str, 4, &tm.tm_year) != 0) {
+ if (xgetpart(&str, 4, &tm.tm_year) != 0) {
goto invalid;
}
tm.tm_year -= 1900;
@@ -219,7 +193,7 @@ int parse_timestamp(const char *str, struct timespec *result) {
if (*str == '-') {
++str;
}
- if (parse_timestamp_part(&str, 2, &tm.tm_mon) != 0) {
+ if (xgetpart(&str, 2, &tm.tm_mon) != 0) {
goto invalid;
}
tm.tm_mon -= 1;
@@ -228,18 +202,18 @@ int parse_timestamp(const char *str, struct timespec *result) {
if (*str == '-') {
++str;
}
- if (parse_timestamp_part(&str, 2, &tm.tm_mday) != 0) {
+ if (xgetpart(&str, 2, &tm.tm_mday) != 0) {
goto invalid;
}
if (!*str) {
goto end;
- } else if (*str == 'T') {
+ } else if (*str == 'T' || *str == ' ') {
++str;
}
// hh
- if (parse_timestamp_part(&str, 2, &tm.tm_hour) != 0) {
+ if (xgetpart(&str, 2, &tm.tm_hour) != 0) {
goto invalid;
}
@@ -248,8 +222,10 @@ int parse_timestamp(const char *str, struct timespec *result) {
goto end;
} else if (*str == ':') {
++str;
+ } else if (xgetdigit(*str) < 0) {
+ goto zone;
}
- if (parse_timestamp_part(&str, 2, &tm.tm_min) != 0) {
+ if (xgetpart(&str, 2, &tm.tm_min) != 0) {
goto invalid;
}
@@ -258,11 +234,14 @@ int parse_timestamp(const char *str, struct timespec *result) {
goto end;
} else if (*str == ':') {
++str;
+ } else if (xgetdigit(*str) < 0) {
+ goto zone;
}
- if (parse_timestamp_part(&str, 2, &tm.tm_sec) != 0) {
+ if (xgetpart(&str, 2, &tm.tm_sec) != 0) {
goto invalid;
}
+zone:
if (!*str) {
goto end;
} else if (*str == 'Z') {
@@ -274,7 +253,7 @@ int parse_timestamp(const char *str, struct timespec *result) {
++str;
// hh
- if (parse_timestamp_part(&str, 2, &tz_hour) != 0) {
+ if (xgetpart(&str, 2, &tz_hour) != 0) {
goto invalid;
}
@@ -284,7 +263,7 @@ int parse_timestamp(const char *str, struct timespec *result) {
} else if (*str == ':') {
++str;
}
- if (parse_timestamp_part(&str, 2, &tz_min) != 0) {
+ if (xgetpart(&str, 2, &tz_min) != 0) {
goto invalid;
}
} else {
@@ -305,11 +284,11 @@ end:
goto error;
}
- int offset = 60*tz_hour + tz_min;
+ int offset = (tz_hour * 60 + tz_min) * 60;
if (tz_negative) {
- result->tv_sec -= offset;
- } else {
result->tv_sec += offset;
+ } else {
+ result->tv_sec -= offset;
}
}
@@ -321,3 +300,17 @@ invalid:
error:
return -1;
}
+
+int xgettime(struct timespec *result) {
+#if _POSIX_TIMERS > 0
+ return clock_gettime(CLOCK_REALTIME, result);
+#else
+ struct timeval tv;
+ int ret = gettimeofday(&tv, NULL);
+ if (ret == 0) {
+ result->tv_sec = tv.tv_sec;
+ result->tv_nsec = tv.tv_usec * 1000L;
+ }
+ return ret;
+#endif
+}
diff --git a/src/xtime.h b/src/xtime.h
index ceff48f..fb60ae4 100644
--- a/src/xtime.h
+++ b/src/xtime.h
@@ -1,18 +1,5 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* Date/time handling.
@@ -24,30 +11,6 @@
#include <time.h>
/**
- * localtime_r() wrapper that calls tzset() first.
- *
- * @param[in] timep
- * The time_t to convert.
- * @param[out] result
- * Buffer to hold the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xlocaltime(const time_t *timep, struct tm *result);
-
-/**
- * gmtime_r() wrapper that calls tzset() first.
- *
- * @param[in] timep
- * The time_t to convert.
- * @param[out] result
- * Buffer to hold the result.
- * @return
- * 0 on success, -1 on failure.
- */
-int xgmtime(const time_t *timep, struct tm *result);
-
-/**
* mktime() wrapper that reports errors more reliably.
*
* @param[in,out] tm
@@ -81,6 +44,16 @@ int xtimegm(struct tm *tm, time_t *timep);
* @return
* 0 on success, -1 on failure.
*/
-int parse_timestamp(const char *str, struct timespec *result);
+int xgetdate(const char *str, struct timespec *result);
+
+/**
+ * Get the current time.
+ *
+ * @param[out] result
+ * A pointer to the result.
+ * @return
+ * 0 on success, -1 on failure.
+ */
+int xgettime(struct timespec *result);
#endif // BFS_XTIME_H
diff --git a/tests/alloc.c b/tests/alloc.c
new file mode 100644
index 0000000..6c0defd
--- /dev/null
+++ b/tests/alloc.c
@@ -0,0 +1,52 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "alloc.h"
+#include "diag.h"
+#include <errno.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+bool check_alloc(void) {
+ bool ret = true;
+
+ // Check sizeof_flex()
+ struct flexible {
+ alignas(64) int foo[8];
+ int bar[];
+ };
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible));
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0);
+
+ size_t too_many = SIZE_MAX / sizeof(int) + 1;
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX));
+
+ // Corner case: sizeof(type) > align_ceil(alignof(type), offsetof(type, member))
+ // Doesn't happen in typical ABIs
+ ret &= bfs_check(flex_size(8, 16, 4, 4, 1) == 16);
+
+ // Make sure we detect allocation size overflows
+#if __GNUC__ && !__clang__
+# pragma GCC diagnostic ignored "-Walloc-size-larger-than="
+#endif
+
+ ret &= bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+
+ // varena tests
+ struct varena varena;
+ VARENA_INIT(&varena, struct flexible, bar);
+
+ for (size_t i = 0; i < 256; ++i) {
+ bfs_verify(varena_alloc(&varena, i));
+ struct arena *arena = &varena.arenas[varena.narenas - 1];
+ ret &= bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i));
+ }
+
+ varena_destroy(&varena);
+ return ret;
+}
diff --git a/tests/test_D_all.out b/tests/bfs/D_all.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_D_all.out
+++ b/tests/bfs/D_all.out
diff --git a/tests/bfs/D_all.sh b/tests/bfs/D_all.sh
new file mode 100644
index 0000000..170698a
--- /dev/null
+++ b/tests/bfs/D_all.sh
@@ -0,0 +1 @@
+bfs_diff -D all basic
diff --git a/tests/bfs/D_incomplete.sh b/tests/bfs/D_incomplete.sh
new file mode 100644
index 0000000..30c522a
--- /dev/null
+++ b/tests/bfs/D_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -D
diff --git a/tests/test_D_multi.out b/tests/bfs/D_multi.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_D_multi.out
+++ b/tests/bfs/D_multi.out
diff --git a/tests/bfs/D_multi.sh b/tests/bfs/D_multi.sh
new file mode 100644
index 0000000..08a8ca6
--- /dev/null
+++ b/tests/bfs/D_multi.sh
@@ -0,0 +1 @@
+bfs_diff -D opt,tree,unknown basic
diff --git a/tests/test_type_f.out b/tests/bfs/D_opt.out
index 6218a0c..6218a0c 100644
--- a/tests/test_type_f.out
+++ b/tests/bfs/D_opt.out
diff --git a/tests/bfs/D_opt.sh b/tests/bfs/D_opt.sh
new file mode 100644
index 0000000..c14fe70
--- /dev/null
+++ b/tests/bfs/D_opt.sh
@@ -0,0 +1 @@
+bfs_diff -D opt -nohidden -not \( -type c -o -type d \) -links -5 -links -10 -not -hidden basic
diff --git a/tests/test_O0.out b/tests/bfs/D_unknown.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_O0.out
+++ b/tests/bfs/D_unknown.out
diff --git a/tests/bfs/D_unknown.sh b/tests/bfs/D_unknown.sh
new file mode 100644
index 0000000..cac9bd9
--- /dev/null
+++ b/tests/bfs/D_unknown.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -D unknown basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/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_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_O1.out b/tests/bfs/O0.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_O1.out
+++ b/tests/bfs/O0.out
diff --git a/tests/bfs/O0.sh b/tests/bfs/O0.sh
new file mode 100644
index 0000000..0f92d71
--- /dev/null
+++ b/tests/bfs/O0.sh
@@ -0,0 +1 @@
+bfs_diff -O0 basic -not \( -type f -not -type f \)
diff --git a/tests/test_O2.out b/tests/bfs/O1.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_O2.out
+++ b/tests/bfs/O1.out
diff --git a/tests/bfs/O1.sh b/tests/bfs/O1.sh
new file mode 100644
index 0000000..924b410
--- /dev/null
+++ b/tests/bfs/O1.sh
@@ -0,0 +1 @@
+bfs_diff -O1 basic -not \( -type f -not -type f \)
diff --git a/tests/test_O3.out b/tests/bfs/O2.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_O3.out
+++ b/tests/bfs/O2.out
diff --git a/tests/bfs/O2.sh b/tests/bfs/O2.sh
new file mode 100644
index 0000000..9382456
--- /dev/null
+++ b/tests/bfs/O2.sh
@@ -0,0 +1 @@
+bfs_diff -O2 basic -not \( -type f -not -type f \)
diff --git a/tests/test_Ofast.out b/tests/bfs/O3.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_Ofast.out
+++ b/tests/bfs/O3.out
diff --git a/tests/bfs/O3.sh b/tests/bfs/O3.sh
new file mode 100644
index 0000000..5bdf2bc
--- /dev/null
+++ b/tests/bfs/O3.sh
@@ -0,0 +1 @@
+bfs_diff -O3 basic -not \( -type f -not -type f \)
diff --git a/tests/test_S_dfs.out b/tests/bfs/O9.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_S_dfs.out
+++ b/tests/bfs/O9.out
diff --git a/tests/bfs/O9.sh b/tests/bfs/O9.sh
new file mode 100644
index 0000000..c12a7a3
--- /dev/null
+++ b/tests/bfs/O9.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -O9 basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/test_basic.out b/tests/bfs/Ofast.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_basic.out
+++ b/tests/bfs/Ofast.out
diff --git a/tests/bfs/Ofast.sh b/tests/bfs/Ofast.sh
new file mode 100644
index 0000000..87c1d8d
--- /dev/null
+++ b/tests/bfs/Ofast.sh
@@ -0,0 +1 @@
+bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \)
diff --git a/tests/test_S_bfs.out b/tests/bfs/S_bfs.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_S_bfs.out
+++ b/tests/bfs/S_bfs.out
diff --git a/tests/bfs/S_bfs.sh b/tests/bfs/S_bfs.sh
new file mode 100644
index 0000000..76976de
--- /dev/null
+++ b/tests/bfs/S_bfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S bfs -s basic >"$OUT"
+diff_output
diff --git a/tests/test_closed_stdin.out b/tests/bfs/S_dfs.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_closed_stdin.out
+++ b/tests/bfs/S_dfs.out
diff --git a/tests/bfs/S_dfs.sh b/tests/bfs/S_dfs.sh
new file mode 100644
index 0000000..7dd7a46
--- /dev/null
+++ b/tests/bfs/S_dfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S dfs -s basic >"$OUT"
+diff_output
diff --git a/tests/test_S_ids.out b/tests/bfs/S_ids.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_S_ids.out
+++ b/tests/bfs/S_ids.out
diff --git a/tests/bfs/S_ids.sh b/tests/bfs/S_ids.sh
new file mode 100644
index 0000000..3995cf0
--- /dev/null
+++ b/tests/bfs/S_ids.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S ids -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/and_incomplete.sh b/tests/bfs/and_incomplete.sh
new file mode 100644
index 0000000..05abc2d
--- /dev/null
+++ b/tests/bfs/and_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -a
diff --git a/tests/bfs/capable.out b/tests/bfs/capable.out
new file mode 100644
index 0000000..ac7b5ce
--- /dev/null
+++ b/tests/bfs/capable.out
@@ -0,0 +1 @@
+./capable
diff --git a/tests/bfs/capable.sh b/tests/bfs/capable.sh
new file mode 100644
index 0000000..35bb0b4
--- /dev/null
+++ b/tests/bfs/capable.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+bfs_diff . -capable
diff --git a/tests/bfs/closed_stderr.sh b/tests/bfs/closed_stderr.sh
new file mode 100644
index 0000000..26abd85
--- /dev/null
+++ b/tests/bfs/closed_stderr.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stderr before we can
+(bash -c 'echo >&2' 2>&-) && skip
+
+! invoke_bfs basic >&- 2>&-
diff --git a/tests/test_d_path.out b/tests/bfs/closed_stdin.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_d_path.out
+++ b/tests/bfs/closed_stdin.out
diff --git a/tests/bfs/closed_stdin.sh b/tests/bfs/closed_stdin.sh
new file mode 100644
index 0000000..6932be8
--- /dev/null
+++ b/tests/bfs/closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic <&-
diff --git a/tests/bfs/closed_stdout.sh b/tests/bfs/closed_stdout.sh
new file mode 100644
index 0000000..5b6f7c3
--- /dev/null
+++ b/tests/bfs/closed_stdout.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stdout before we can
+(bash -c echo >&-) && skip
+
+! invoke_bfs basic >&-
diff --git a/tests/test_color.out b/tests/bfs/color.out
index 77fc8a8..a439814 100644
--- a/tests/test_color.out
+++ b/tests/bfs/color.out
@@ -1,3 +1,5 @@
+$'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_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..4adba69
--- /dev/null
+++ b/tests/bfs/color_ext_case.sh
@@ -0,0 +1,6 @@
+# *.gz=01;31:*.GZ=01;32 -- case sensitive
+# *.tAr=01;33:*.TaR=01;33 -- case-insensitive
+# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive
+# *.txt=35:*TXT=36 -- case-insensitive
+export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36"
+bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_override.out b/tests/bfs/color_ext_override.out
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..ac4c7fb
--- /dev/null
+++ b/tests/bfs/color_ext_override.sh
@@ -0,0 +1 @@
+LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.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_ext_underride.out b/tests/bfs/color_fi0_no.out
index 787248a..a439814 100644
--- a/tests/test_color_ext_underride.out
+++ b/tests/bfs/color_fi0_no.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_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..f69eb9c
--- /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/test_color_mh0.out b/tests/bfs/color_mh0.out
index 77fc8a8..a439814 100644
--- a/tests/test_color_mh0.out
+++ b/tests/bfs/color_mh0.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_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/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 d39bbe7..077ef8d 100644
--- a/tests/test_color_rs_lc_rc_ec.out
+++ b/tests/bfs/color_rs_lc_rc_ec.out
@@ -1,3 +1,5 @@
+LC01;34RC$'rainbow/\e[1m'EC
+LC01;34RC$'rainbow/\e[1m/'EC$'\e[0m'
LC01;34RCrainbow/ECLC01;32RCexec.shEC
LC01;34RCrainbow/ECLC01;35RCsocketEC
LC01;34RCrainbow/ECLC01;36RCbrokenEC
@@ -12,9 +14,14 @@ LC01;34RCrainbow/ECLC37;41RCsuidEC
LC01;34RCrainbow/ECLC37;44RCstickyEC
LC01;34RCrainbow/ECfile.dat
LC01;34RCrainbow/ECfile.txt
+LC01;34RCrainbow/EClower.gz
+LC01;34RCrainbow/EClower.tar
+LC01;34RCrainbow/EClower.tar.gz
+LC01;34RCrainbow/EClu.tar.GZ
LC01;34RCrainbow/ECmh1
LC01;34RCrainbow/ECmh2
-LC01;34RCrainbow/ECstar.gz
-LC01;34RCrainbow/ECstar.tar
-LC01;34RCrainbow/ECstar.tar.gz
+LC01;34RCrainbow/ECul.TAR.gz
+LC01;34RCrainbow/ECupper.GZ
+LC01;34RCrainbow/ECupper.TAR
+LC01;34RCrainbow/ECupper.TAR.GZ
LC01;34RCrainbowEC
diff --git a/tests/bfs/color_rs_lc_rc_ec.sh b/tests/bfs/color_rs_lc_rc_ec.sh
new file mode 100644
index 0000000..467b2da
--- /dev/null
+++ b/tests/bfs/color_rs_lc_rc_ec.sh
@@ -0,0 +1 @@
+LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw0_ow.out b/tests/bfs/color_st0_tw0_ow.out
index 9a47ef2..a82762b 100644
--- a/tests/test_color_st0_tw0_ow.out
+++ b/tests/bfs/color_st0_tw0_ow.out
@@ -1,6 +1,7 @@
+$'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/test_data_flow_group.out b/tests/bfs/data_flow_hidden.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_data_flow_group.out
+++ b/tests/bfs/data_flow_hidden.out
diff --git a/tests/bfs/data_flow_hidden.sh b/tests/bfs/data_flow_hidden.sh
new file mode 100644
index 0000000..6afaab2
--- /dev/null
+++ b/tests/bfs/data_flow_hidden.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \)
diff --git a/tests/test_deep.out b/tests/bfs/deep_strict.out
index c385fce..c385fce 100644
--- a/tests/test_deep.out
+++ b/tests/bfs/deep_strict.out
diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh
new file mode 100644
index 0000000..22453c0
--- /dev/null
+++ b/tests/bfs/deep_strict.sh
@@ -0,0 +1,3 @@
+# Not even enough fds to keep the root open
+ulimit -n $((NOPENFD + 4))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/test_exclude_depth.out b/tests/bfs/exclude_depth.out
index 59e3c42..59e3c42 100644
--- a/tests/test_exclude_depth.out
+++ b/tests/bfs/exclude_depth.out
diff --git a/tests/bfs/exclude_depth.sh b/tests/bfs/exclude_depth.sh
new file mode 100644
index 0000000..437b4dd
--- /dev/null
+++ b/tests/bfs/exclude_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -exclude -name foo
diff --git a/tests/bfs/exclude_exclude.sh b/tests/bfs/exclude_exclude.sh
new file mode 100644
index 0000000..739342f
--- /dev/null
+++ b/tests/bfs/exclude_exclude.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -exclude -name foo
diff --git a/tests/test_L_ilname.out b/tests/bfs/exclude_mindepth.out
index e69de29..e69de29 100644
--- a/tests/test_L_ilname.out
+++ b/tests/bfs/exclude_mindepth.out
diff --git a/tests/bfs/exclude_mindepth.sh b/tests/bfs/exclude_mindepth.sh
new file mode 100644
index 0000000..c8f70f9
--- /dev/null
+++ b/tests/bfs/exclude_mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 3 -exclude -name foo
diff --git a/tests/test_exclude_name.out b/tests/bfs/exclude_name.out
index 59e3c42..59e3c42 100644
--- a/tests/test_exclude_name.out
+++ b/tests/bfs/exclude_name.out
diff --git a/tests/bfs/exclude_name.sh b/tests/bfs/exclude_name.sh
new file mode 100644
index 0000000..7cf9f33
--- /dev/null
+++ b/tests/bfs/exclude_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -exclude -name foo
diff --git a/tests/bfs/exclude_print.sh b/tests/bfs/exclude_print.sh
new file mode 100644
index 0000000..dc89e1d
--- /dev/null
+++ b/tests/bfs/exclude_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -print
diff --git a/tests/test_exec_flush_fprint.out b/tests/bfs/exec_flush_fprint.out
index 511198f..511198f 100644
--- a/tests/test_exec_flush_fprint.out
+++ b/tests/bfs/exec_flush_fprint.out
diff --git a/tests/bfs/exec_flush_fprint.sh b/tests/bfs/exec_flush_fprint.sh
new file mode 100644
index 0000000..a862773
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint.sh
@@ -0,0 +1,2 @@
+# Even non-stdstreams should be flushed
+bfs_diff basic/a -fprint "$OUT.f" -exec cat "$OUT.f" \;
diff --git a/tests/bfs/exec_flush_fprint_fail.sh b/tests/bfs/exec_flush_fprint_fail.sh
new file mode 100644
index 0000000..cd38e41
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint_fail.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic/a -fprint /dev/full -exec true \;
diff --git a/tests/test_execdir_plus.out b/tests/bfs/execdir_plus.out
index 8866a8f..8866a8f 100644
--- a/tests/test_execdir_plus.out
+++ b/tests/bfs/execdir_plus.out
diff --git a/tests/bfs/execdir_plus.sh b/tests/bfs/execdir_plus.sh
new file mode 100644
index 0000000..6f24bdc
--- /dev/null
+++ b/tests/bfs/execdir_plus.sh
@@ -0,0 +1,4 @@
+tree=$(invoke_bfs -D tree 2>&1 -quit)
+[[ "$tree" == *"-S dfs"* ]] && skip
+
+bfs_diff -j1 basic -execdir "$TESTS/sort-args.sh" {} +
diff --git a/tests/test_data_flow_hidden.out b/tests/bfs/execdir_plus_nonexistent.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_data_flow_hidden.out
+++ b/tests/bfs/execdir_plus_nonexistent.out
diff --git a/tests/bfs/execdir_plus_nonexistent.sh b/tests/bfs/execdir_plus_nonexistent.sh
new file mode 100644
index 0000000..ed7ed56
--- /dev/null
+++ b/tests/bfs/execdir_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_H_type_l.out b/tests/bfs/expr_flag_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_H_type_l.out
+++ b/tests/bfs/expr_flag_path.out
diff --git a/tests/bfs/expr_flag_path.sh b/tests/bfs/expr_flag_path.sh
new file mode 100644
index 0000000..bb89d92
--- /dev/null
+++ b/tests/bfs/expr_flag_path.sh
@@ -0,0 +1 @@
+bfs_diff -type l -H links/skip
diff --git a/tests/test_expr_flag_path.out b/tests/bfs/expr_path_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_flag_path.out
+++ b/tests/bfs/expr_path_flag.out
diff --git a/tests/bfs/expr_path_flag.sh b/tests/bfs/expr_path_flag.sh
new file mode 100644
index 0000000..818e5d1
--- /dev/null
+++ b/tests/bfs/expr_path_flag.sh
@@ -0,0 +1 @@
+bfs_diff -type l links/skip -H
diff --git a/tests/test_expr_path_flag.out b/tests/bfs/flag_expr_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_path_flag.out
+++ b/tests/bfs/flag_expr_path.out
diff --git a/tests/bfs/flag_expr_path.sh b/tests/bfs/flag_expr_path.sh
new file mode 100644
index 0000000..a414e10
--- /dev/null
+++ b/tests/bfs/flag_expr_path.sh
@@ -0,0 +1 @@
+bfs_diff -H -type l links/skip
diff --git a/tests/test_fprint_append.out b/tests/bfs/fprint_duplicate_stdout.out
index 6c21751..6c21751 100644
--- a/tests/test_fprint_append.out
+++ b/tests/bfs/fprint_duplicate_stdout.out
diff --git a/tests/bfs/fprint_duplicate_stdout.sh b/tests/bfs/fprint_duplicate_stdout.sh
new file mode 100644
index 0000000..4e95e30
--- /dev/null
+++ b/tests/bfs/fprint_duplicate_stdout.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT" -print >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/fprint_error_stderr.sh b/tests/bfs/fprint_error_stderr.sh
new file mode 100644
index 0000000..2cc4037
--- /dev/null
+++ b/tests/bfs/fprint_error_stderr.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full
diff --git a/tests/bfs/fprint_error_stdout.sh b/tests/bfs/fprint_error_stdout.sh
new file mode 100644
index 0000000..42a7b36
--- /dev/null
+++ b/tests/bfs/fprint_error_stdout.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full
diff --git a/tests/bfs/help.sh b/tests/bfs/help.sh
new file mode 100644
index 0000000..5029c7e
--- /dev/null
+++ b/tests/bfs/help.sh
@@ -0,0 +1,4 @@
+! invoke_bfs -help | grep -E '\{...?\}' || fail
+! invoke_bfs -D help | grep -E '\{...?\}' || fail
+! invoke_bfs -S help | grep -E '\{...?\}' || fail
+! invoke_bfs -regextype help | grep -E '\{...?\}' || fail
diff --git a/tests/test_hidden.out b/tests/bfs/hidden.out
index e65ede9..e65ede9 100644
--- a/tests/test_hidden.out
+++ b/tests/bfs/hidden.out
diff --git a/tests/bfs/hidden.sh b/tests/bfs/hidden.sh
new file mode 100644
index 0000000..b0413c5
--- /dev/null
+++ b/tests/bfs/hidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -hidden
diff --git a/tests/test_hidden_root.out b/tests/bfs/hidden_root.out
index 8c1371b..8c1371b 100644
--- a/tests/test_hidden_root.out
+++ b/tests/bfs/hidden_root.out
diff --git a/tests/bfs/hidden_root.sh b/tests/bfs/hidden_root.sh
new file mode 100644
index 0000000..905c5b5
--- /dev/null
+++ b/tests/bfs/hidden_root.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . ./. ... ./... .../.. -hidden
diff --git a/tests/bfs/high_byte.sh b/tests/bfs/high_byte.sh
new file mode 100644
index 0000000..c76199f
--- /dev/null
+++ b/tests/bfs/high_byte.sh
@@ -0,0 +1 @@
+! invoke_bfs -$'\xFF'
diff --git a/tests/bfs/j0.sh b/tests/bfs/j0.sh
new file mode 100644
index 0000000..97a7c5c
--- /dev/null
+++ b/tests/bfs/j0.sh
@@ -0,0 +1 @@
+! invoke_bfs -j0 basic
diff --git a/tests/test_data_flow_sparse.out b/tests/bfs/j1.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_data_flow_sparse.out
+++ b/tests/bfs/j1.out
diff --git a/tests/bfs/j1.sh b/tests/bfs/j1.sh
new file mode 100644
index 0000000..972ac1b
--- /dev/null
+++ b/tests/bfs/j1.sh
@@ -0,0 +1 @@
+bfs_diff -j1 basic
diff --git a/tests/test_data_flow_user.out b/tests/bfs/j64.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_data_flow_user.out
+++ b/tests/bfs/j64.out
diff --git a/tests/bfs/j64.sh b/tests/bfs/j64.sh
new file mode 100644
index 0000000..c56788f
--- /dev/null
+++ b/tests/bfs/j64.sh
@@ -0,0 +1 @@
+bfs_diff -j64 basic
diff --git a/tests/bfs/j_negative.sh b/tests/bfs/j_negative.sh
new file mode 100644
index 0000000..809c98c
--- /dev/null
+++ b/tests/bfs/j_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs -j-1 basic
diff --git a/tests/bfs/limit.out b/tests/bfs/limit.out
new file mode 100644
index 0000000..ea94276
--- /dev/null
+++ b/tests/bfs/limit.out
@@ -0,0 +1,4 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
diff --git a/tests/bfs/limit.sh b/tests/bfs/limit.sh
new file mode 100644
index 0000000..84b605f
--- /dev/null
+++ b/tests/bfs/limit.sh
@@ -0,0 +1 @@
+bfs_diff -s basic -type f -print -limit 4
diff --git a/tests/bfs/limit_0.sh b/tests/bfs/limit_0.sh
new file mode 100644
index 0000000..3ce26de
--- /dev/null
+++ b/tests/bfs/limit_0.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit 0
diff --git a/tests/bfs/limit_implicit_print.sh b/tests/bfs/limit_implicit_print.sh
new file mode 100644
index 0000000..cdb059d
--- /dev/null
+++ b/tests/bfs/limit_implicit_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -type f -limit 1
diff --git a/tests/bfs/limit_incomplete.sh b/tests/bfs/limit_incomplete.sh
new file mode 100644
index 0000000..2d1e842
--- /dev/null
+++ b/tests/bfs/limit_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit
diff --git a/tests/bfs/limit_one.sh b/tests/bfs/limit_one.sh
new file mode 100644
index 0000000..3f8181c
--- /dev/null
+++ b/tests/bfs/limit_one.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit one
diff --git a/tests/bfs/links_empty.sh b/tests/bfs/links_empty.sh
new file mode 100644
index 0000000..42cf6e5
--- /dev/null
+++ b/tests/bfs/links_empty.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ''
diff --git a/tests/bfs/links_invalid.sh b/tests/bfs/links_invalid.sh
new file mode 100644
index 0000000..4d139c9
--- /dev/null
+++ b/tests/bfs/links_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ASDF
diff --git a/tests/bfs/links_leading_space.sh b/tests/bfs/links_leading_space.sh
new file mode 100644
index 0000000..15957af
--- /dev/null
+++ b/tests/bfs/links_leading_space.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ' 1'
diff --git a/tests/bfs/links_negative.sh b/tests/bfs/links_negative.sh
new file mode 100644
index 0000000..e664b99
--- /dev/null
+++ b/tests/bfs/links_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links +-1
diff --git a/tests/bfs/links_noarg.sh b/tests/bfs/links_noarg.sh
new file mode 100644
index 0000000..5c948dc
--- /dev/null
+++ b/tests/bfs/links_noarg.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links
diff --git a/tests/bfs/newerma_nonexistent.sh b/tests/bfs/newerma_nonexistent.sh
new file mode 100644
index 0000000..cdedb4a
--- /dev/null
+++ b/tests/bfs/newerma_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerma basic/nonexistent
diff --git a/tests/bfs/newermq.sh b/tests/bfs/newermq.sh
new file mode 100644
index 0000000..2f705dc
--- /dev/null
+++ b/tests/bfs/newermq.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermq times/a
diff --git a/tests/bfs/newermt_invalid.sh b/tests/bfs/newermt_invalid.sh
new file mode 100644
index 0000000..98efece
--- /dev/null
+++ b/tests/bfs/newermt_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermt not_a_date_time
diff --git a/tests/bfs/newerqm.sh b/tests/bfs/newerqm.sh
new file mode 100644
index 0000000..c0cff98
--- /dev/null
+++ b/tests/bfs/newerqm.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerqm times/a
diff --git a/tests/bfs/nocolor.out b/tests/bfs/nocolor.out
new file mode 100644
index 0000000..d51d24d
--- /dev/null
+++ b/tests/bfs/nocolor.out
@@ -0,0 +1,27 @@
+rainbow
+rainbow/
+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/test_nohidden.out b/tests/bfs/nohidden.out
index d3ec901..d3ec901 100644
--- a/tests/test_nohidden.out
+++ b/tests/bfs/nohidden.out
diff --git a/tests/bfs/nohidden.sh b/tests/bfs/nohidden.sh
new file mode 100644
index 0000000..e3a3e4a
--- /dev/null
+++ b/tests/bfs/nohidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -nohidden
diff --git a/tests/test_nohidden_depth.out b/tests/bfs/nohidden_depth.out
index d3ec901..d3ec901 100644
--- a/tests/test_nohidden_depth.out
+++ b/tests/bfs/nohidden_depth.out
diff --git a/tests/bfs/nohidden_depth.sh b/tests/bfs/nohidden_depth.sh
new file mode 100644
index 0000000..9fd7017
--- /dev/null
+++ b/tests/bfs/nohidden_depth.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -depth -nohidden
diff --git a/tests/bfs/nowarn.sh b/tests/bfs/nowarn.sh
new file mode 100644
index 0000000..d9f9ab3
--- /dev/null
+++ b/tests/bfs/nowarn.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null)
+[ -z "$stderr" ]
diff --git a/tests/test_ok_plus_semicolon.out b/tests/bfs/ok_plus_semicolon.out
index 2a3e14f..2a3e14f 100644
--- a/tests/test_ok_plus_semicolon.out
+++ b/tests/bfs/ok_plus_semicolon.out
diff --git a/tests/bfs/ok_plus_semicolon.sh b/tests/bfs/ok_plus_semicolon.sh
new file mode 100644
index 0000000..57d6103
--- /dev/null
+++ b/tests/bfs/ok_plus_semicolon.sh
@@ -0,0 +1,8 @@
+# The -ok primary shall be equivalent to -exec, except that the use of a
+# <plus-sign> to punctuate the end of the primary expression need not be
+# supported, ...
+#
+# bfs chooses not to support it, for compatibility with most other find
+# implementations.
+
+yes | bfs_diff basic -ok echo {} + \;
diff --git a/tests/test_okdir_plus_semicolon.out b/tests/bfs/okdir_plus_semicolon.out
index 1909d27..1909d27 100644
--- a/tests/test_okdir_plus_semicolon.out
+++ b/tests/bfs/okdir_plus_semicolon.out
diff --git a/tests/bfs/okdir_plus_semicolon.sh b/tests/bfs/okdir_plus_semicolon.sh
new file mode 100644
index 0000000..d316bd7
--- /dev/null
+++ b/tests/bfs/okdir_plus_semicolon.sh
@@ -0,0 +1 @@
+yes | bfs_diff basic -okdir echo {} + \;
diff --git a/tests/bfs/or_incomplete.sh b/tests/bfs/or_incomplete.sh
new file mode 100644
index 0000000..4af31b6
--- /dev/null
+++ b/tests/bfs/or_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -o
diff --git a/tests/test_flag_expr_path.out b/tests/bfs/path_expr_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_flag_expr_path.out
+++ b/tests/bfs/path_expr_flag.out
diff --git a/tests/bfs/path_expr_flag.sh b/tests/bfs/path_expr_flag.sh
new file mode 100644
index 0000000..7cfa1cd
--- /dev/null
+++ b/tests/bfs/path_expr_flag.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l -H
diff --git a/tests/test_path_expr_flag.out b/tests/bfs/path_flag_expr.out
index e67f10b..e67f10b 100644
--- a/tests/test_path_expr_flag.out
+++ b/tests/bfs/path_flag_expr.out
diff --git a/tests/bfs/path_flag_expr.sh b/tests/bfs/path_flag_expr.sh
new file mode 100644
index 0000000..ca00c8c
--- /dev/null
+++ b/tests/bfs/path_flag_expr.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -H -type l
diff --git a/tests/test_L_lname.out b/tests/bfs/perm_leading_plus_symbolic.out
index e69de29..e69de29 100644
--- a/tests/test_L_lname.out
+++ b/tests/bfs/perm_leading_plus_symbolic.out
diff --git a/tests/bfs/perm_leading_plus_symbolic.sh b/tests/bfs/perm_leading_plus_symbolic.sh
new file mode 100644
index 0000000..4202ac1
--- /dev/null
+++ b/tests/bfs/perm_leading_plus_symbolic.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +rwx
diff --git a/tests/bfs/perm_symbolic_double_comma.sh b/tests/bfs/perm_symbolic_double_comma.sh
new file mode 100644
index 0000000..48f9d4b
--- /dev/null
+++ b/tests/bfs/perm_symbolic_double_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,,u+w
diff --git a/tests/bfs/perm_symbolic_missing_action.sh b/tests/bfs/perm_symbolic_missing_action.sh
new file mode 100644
index 0000000..28446ab
--- /dev/null
+++ b/tests/bfs/perm_symbolic_missing_action.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a
diff --git a/tests/bfs/perm_symbolic_trailing_comma.sh b/tests/bfs/perm_symbolic_trailing_comma.sh
new file mode 100644
index 0000000..01bbc16
--- /dev/null
+++ b/tests/bfs/perm_symbolic_trailing_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,
diff --git a/tests/test_printf_color.out b/tests/bfs/printf_color.out
index d9cd1a4..77d21c3 100644
--- a/tests/test_printf_color.out
+++ b/tests/bfs/printf_color.out
@@ -1,5 +1,8 @@
-. . rainbow ./rainbow rainbow
+. $'./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
@@ -13,8 +16,13 @@
. ./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 star.gz ./rainbow/star.gz rainbow/star.gz
-. ./rainbow star.tar ./rainbow/star.tar rainbow/star.tar
-. ./rainbow star.tar.gz ./rainbow/star.tar.gz rainbow/star.tar.gz
+. ./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_format.sh b/tests/bfs/printf_invalid_format.sh
new file mode 100644
index 0000000..59d63a7
--- /dev/null
+++ b/tests/bfs/printf_invalid_format.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%!'
diff --git a/tests/bfs/printf_must_be_numeric.sh b/tests/bfs/printf_must_be_numeric.sh
new file mode 100644
index 0000000..7c7c3fa
--- /dev/null
+++ b/tests/bfs/printf_must_be_numeric.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%+p'
diff --git a/tests/test_and_purity.out b/tests/bfs/printf_w.out
index e69de29..e69de29 100644
--- a/tests/test_and_purity.out
+++ b/tests/bfs/printf_w.out
diff --git a/tests/bfs/printf_w.sh b/tests/bfs/printf_w.sh
new file mode 100644
index 0000000..3b27ee7
--- /dev/null
+++ b/tests/bfs/printf_w.sh
@@ -0,0 +1,2 @@
+# Birth times may not be supported, so just check that %w/%W/%B can be parsed
+bfs_diff times -false -printf '%w %WY %BY\n'
diff --git a/tests/bfs/status.sh b/tests/bfs/status.sh
new file mode 100644
index 0000000..83e12d3
--- /dev/null
+++ b/tests/bfs/status.sh
@@ -0,0 +1 @@
+bfs_pty basic -status -print -depth 0 -exec stty cols 123 rows 14 \; >"$OUT"
diff --git a/tests/bfs/stderr_fails_loudly.sh b/tests/bfs/stderr_fails_loudly.sh
new file mode 100644
index 0000000..8572d5a
--- /dev/null
+++ b/tests/bfs/stderr_fails_loudly.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full
diff --git a/tests/test_daystart.out b/tests/bfs/stderr_fails_silently.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_daystart.out
+++ b/tests/bfs/stderr_fails_silently.out
diff --git a/tests/bfs/stderr_fails_silently.sh b/tests/bfs/stderr_fails_silently.sh
new file mode 100644
index 0000000..a37393d
--- /dev/null
+++ b/tests/bfs/stderr_fails_silently.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+bfs_diff -D all basic 2>/dev/full
diff --git a/tests/test_type_multi.out b/tests/bfs/type_multi.out
index 3cae08a..3cae08a 100644
--- a/tests/test_type_multi.out
+++ b/tests/bfs/type_multi.out
diff --git a/tests/bfs/type_multi.sh b/tests/bfs/type_multi.sh
new file mode 100644
index 0000000..59992c7
--- /dev/null
+++ b/tests/bfs/type_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -type f,d,c
diff --git a/tests/bfs/typo.sh b/tests/bfs/typo.sh
new file mode 100644
index 0000000..459e9fe
--- /dev/null
+++ b/tests/bfs/typo.sh
@@ -0,0 +1 @@
+invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null
diff --git a/tests/bfs/unexpected_operator.sh b/tests/bfs/unexpected_operator.sh
new file mode 100644
index 0000000..2eb0e71
--- /dev/null
+++ b/tests/bfs/unexpected_operator.sh
@@ -0,0 +1 @@
+! invoke_bfs \! -o -print
diff --git a/tests/test_unique.out b/tests/bfs/unique.out
index 289cbde..289cbde 100644
--- a/tests/test_unique.out
+++ b/tests/bfs/unique.out
diff --git a/tests/bfs/unique.sh b/tests/bfs/unique.sh
new file mode 100644
index 0000000..ea8adfd
--- /dev/null
+++ b/tests/bfs/unique.sh
@@ -0,0 +1 @@
+bfs_diff links/{file,symlink,hardlink} -unique
diff --git a/tests/test_daystart_twice.out b/tests/bfs/unique_depth.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_daystart_twice.out
+++ b/tests/bfs/unique_depth.out
diff --git a/tests/bfs/unique_depth.sh b/tests/bfs/unique_depth.sh
new file mode 100644
index 0000000..c1d9716
--- /dev/null
+++ b/tests/bfs/unique_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -unique -depth
diff --git a/tests/bfs/version.sh b/tests/bfs/version.sh
new file mode 100644
index 0000000..e0417ca
--- /dev/null
+++ b/tests/bfs/version.sh
@@ -0,0 +1 @@
+invoke_bfs -version >/dev/null
diff --git a/tests/bfs/warn_O9.out b/tests/bfs/warn_O9.out
new file mode 100644
index 0000000..336a6e8
--- /dev/null
+++ b/tests/bfs/warn_O9.out
@@ -0,0 +1,19 @@
+.
+./a
+./b
+./c
+./c/d
+./e
+./e/f
+./g
+./g/h
+./i
+./j
+./j/foo
+./k
+./k/foo
+./k/foo/bar
+./l
+./l/foo
+./l/foo/bar
+./l/foo/bar/baz
diff --git a/tests/bfs/warn_O9.sh b/tests/bfs/warn_O9.sh
new file mode 100644
index 0000000..821789f
--- /dev/null
+++ b/tests/bfs/warn_O9.sh
@@ -0,0 +1,3 @@
+# Regression test: don't crash when warning if -O9 is the last argument
+cd basic
+bfs_diff -warn -O9
diff --git a/tests/bfs/warn_depth_prune.sh b/tests/bfs/warn_depth_prune.sh
new file mode 100644
index 0000000..0f613c8
--- /dev/null
+++ b/tests/bfs/warn_depth_prune.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/bfs/warn_exclude_path.sh b/tests/bfs/warn_exclude_path.sh
new file mode 100644
index 0000000..988544e
--- /dev/null
+++ b/tests/bfs/warn_exclude_path.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs -warn -exclude basic -name '*f*' 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/test_depth.out b/tests/bfs/warn_xdev_mount.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_depth.out
+++ b/tests/bfs/warn_xdev_mount.out
diff --git a/tests/bfs/warn_xdev_mount.sh b/tests/bfs/warn_xdev_mount.sh
new file mode 100644
index 0000000..5d395f6
--- /dev/null
+++ b/tests/bfs/warn_xdev_mount.sh
@@ -0,0 +1,2 @@
+# Regression test: don't crash if -mount is the last option
+bfs_diff basic -warn -xdev -mount
diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh
new file mode 100644
index 0000000..02c8173
--- /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 loops -xtype l -depth 100
diff --git a/tests/test_xtype_multi.out b/tests/bfs/xtype_multi.out
index 558e89c..558e89c 100644
--- a/tests/test_xtype_multi.out
+++ b/tests/bfs/xtype_multi.out
diff --git a/tests/bfs/xtype_multi.sh b/tests/bfs/xtype_multi.sh
new file mode 100644
index 0000000..ed20955
--- /dev/null
+++ b/tests/bfs/xtype_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f,d,c
diff --git a/tests/test_data_flow_type.out b/tests/bfs/xtype_reorder.out
index e69de29..e69de29 100644
--- a/tests/test_data_flow_type.out
+++ b/tests/bfs/xtype_reorder.out
diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh
new file mode 100644
index 0000000..8d75d7d
--- /dev/null
+++ b/tests/bfs/xtype_reorder.sh
@@ -0,0 +1,4 @@
+# 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
diff --git a/tests/bfstd.c b/tests/bfstd.c
new file mode 100644
index 0000000..07b68b0
--- /dev/null
+++ b/tests/bfstd.c
@@ -0,0 +1,93 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "bfstd.h"
+#include "diag.h"
+#include <errno.h>
+#include <langinfo.h>
+#include <stdlib.h>
+#include <string.h>
+
+/** Check the result of xdirname()/xbasename(). */
+static bool check_base_dir(const char *path, const char *dir, const char *base) {
+ bool ret = true;
+
+ char *xdir = xdirname(path);
+ bfs_verify(xdir, "xdirname(): %s", xstrerror(errno));
+ ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
+ free(xdir);
+
+ char *xbase = xbasename(path);
+ bfs_verify(xbase, "xbasename(): %s", xstrerror(errno));
+ ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
+ free(xbase);
+
+ return ret;
+}
+
+/** Check the result of wordesc(). */
+static bool check_wordesc(const char *str, const char *exp, enum wesc_flags flags) {
+ char buf[256];
+ char *end = buf + sizeof(buf);
+ char *esc = wordesc(buf, end, str, flags);
+
+ return bfs_check(esc != end)
+ && bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp);
+}
+
+bool check_bfstd(void) {
+ bool ret = true;
+
+ ret &= bfs_check(asciilen("") == 0);
+ ret &= bfs_check(asciilen("@") == 1);
+ ret &= bfs_check(asciilen("@@") == 2);
+ ret &= bfs_check(asciilen("\xFF@") == 0);
+ ret &= bfs_check(asciilen("@\xFF") == 1);
+ ret &= bfs_check(asciilen("@@@@@@@@") == 8);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17);
+ ret &= bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7);
+ ret &= bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8);
+ ret &= bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9);
+
+ // From man 3p basename
+ ret &= check_base_dir("usr", ".", "usr");
+ ret &= check_base_dir("usr/", ".", "usr");
+ ret &= check_base_dir("", ".", ".");
+ ret &= check_base_dir("/", "/", "/");
+ // check_base_dir("//", "/" or "//", "/" or "//");
+ ret &= check_base_dir("///", "/", "/");
+ ret &= check_base_dir("/usr/", "/", "usr");
+ ret &= check_base_dir("/usr/lib", "/usr", "lib");
+ ret &= check_base_dir("//usr//lib//", "//usr", "lib");
+ ret &= check_base_dir("/home//dwc//test", "/home//dwc", "test");
+
+ ret &= check_wordesc("", "\"\"", WESC_SHELL);
+ ret &= check_wordesc("word", "word", WESC_SHELL);
+ ret &= check_wordesc("two words", "\"two words\"", WESC_SHELL);
+ ret &= check_wordesc("word's", "\"word's\"", WESC_SHELL);
+ ret &= check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL);
+ ret &= check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL);
+ ret &= check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("~user", "\"~user\"", WESC_SHELL);
+
+ const char *charmap = nl_langinfo(CODESET);
+ if (strcmp(charmap, "UTF-8") == 0) {
+ ret &= check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL);
+ }
+
+ ret &= bfs_check(xstrwidth("Hello world") == 11);
+ ret &= bfs_check(xstrwidth("Hello\1world") == 10);
+
+ return ret;
+}
diff --git a/tests/bit.c b/tests/bit.c
new file mode 100644
index 0000000..674d1b2
--- /dev/null
+++ b/tests/bit.c
@@ -0,0 +1,141 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "bit.h"
+#include "diag.h"
+#include <limits.h>
+#include <stdint.h>
+#include <string.h>
+
+bfs_static_assert(UMAX_WIDTH(0x1) == 1);
+bfs_static_assert(UMAX_WIDTH(0x3) == 2);
+bfs_static_assert(UMAX_WIDTH(0x7) == 3);
+bfs_static_assert(UMAX_WIDTH(0xF) == 4);
+bfs_static_assert(UMAX_WIDTH(0xFF) == 8);
+bfs_static_assert(UMAX_WIDTH(0xFFF) == 12);
+bfs_static_assert(UMAX_WIDTH(0xFFFF) == 16);
+
+#define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1)
+#define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1)
+#define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1)
+
+bfs_static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH));
+bfs_static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH));
+bfs_static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH));
+
+bfs_static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH));
+bfs_static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH));
+bfs_static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH));
+
+bfs_static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH));
+bfs_static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH));
+bfs_static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH));
+
+bfs_static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH));
+bfs_static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH));
+bfs_static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH));
+
+bfs_static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH));
+bfs_static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH));
+bfs_static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH));
+
+bfs_static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH));
+bfs_static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH));
+bfs_static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH));
+
+bfs_static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH));
+bfs_static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH));
+bfs_static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH));
+
+bfs_static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH));
+bfs_static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH));
+bfs_static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH));
+
+#define check_eq(a, b) \
+ bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b))
+
+bool check_bit(void) {
+ bool ret = true;
+
+ const char *str = "\x1\x2\x3\x4";
+ uint32_t word;
+ memcpy(&word, str, sizeof(word));
+
+#if ENDIAN_NATIVE == ENDIAN_LITTLE
+ ret &= check_eq(word, 0x04030201);
+#elif ENDIAN_NATIVE == ENDIAN_BIG
+ ret &= check_eq(word, 0x01020304);
+#else
+# warning "Skipping byte order tests on mixed/unknown-endian machine"
+#endif
+
+ ret &= check_eq(bswap((uint8_t)0x12), 0x12);
+ ret &= check_eq(bswap((uint16_t)0x1234), 0x3412);
+ ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412);
+ ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412);
+
+ ret &= check_eq(count_ones(0x0), 0);
+ ret &= check_eq(count_ones(0x1), 1);
+ ret &= check_eq(count_ones(0x2), 1);
+ ret &= check_eq(count_ones(0x3), 2);
+ ret &= check_eq(count_ones(0x137F), 10);
+
+ ret &= check_eq(count_zeros(0), INT_WIDTH);
+ ret &= check_eq(count_zeros(0L), LONG_WIDTH);
+ ret &= check_eq(count_zeros(0LL), LLONG_WIDTH);
+ ret &= check_eq(count_zeros((uint8_t)0), 8);
+ ret &= check_eq(count_zeros((uint16_t)0), 16);
+ ret &= check_eq(count_zeros((uint32_t)0), 32);
+ ret &= check_eq(count_zeros((uint64_t)0), 64);
+
+ ret &= check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A);
+ ret &= check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123);
+ ret &= check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345);
+ ret &= check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678);
+
+ ret &= check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A);
+ ret &= check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341);
+ ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123);
+ ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678);
+
+ for (int i = 0; i < 16; ++i) {
+ uint16_t n = (uint16_t)1 << i;
+ for (int j = i; j < 16; ++j) {
+ uint16_t m = (uint16_t)1 << j;
+ uint16_t nm = n | m;
+ ret &= check_eq(count_ones(nm), 1 + (n != m));
+ ret &= check_eq(count_zeros(nm), 15 - (n != m));
+ ret &= check_eq(leading_zeros(nm), 15 - j);
+ ret &= check_eq(trailing_zeros(nm), i);
+ ret &= check_eq(first_leading_one(nm), j + 1);
+ ret &= check_eq(first_trailing_one(nm), i + 1);
+ ret &= check_eq(bit_width(nm), j + 1);
+ ret &= check_eq(bit_floor(nm), m);
+ if (n == m) {
+ ret &= check_eq(bit_ceil(nm), m);
+ ret &= bfs_check(has_single_bit(nm));
+ } else {
+ if (j < 15) {
+ ret &= check_eq(bit_ceil(nm), (m << 1));
+ }
+ ret &= bfs_check(!has_single_bit(nm));
+ }
+ }
+ }
+
+ ret &= check_eq(leading_zeros((uint16_t)0), 16);
+ ret &= check_eq(trailing_zeros((uint16_t)0), 16);
+ ret &= check_eq(first_leading_one(0), 0);
+ ret &= check_eq(first_trailing_one(0), 0);
+ ret &= check_eq(bit_width(0), 0);
+ ret &= check_eq(bit_floor(0), 0);
+ ret &= check_eq(bit_ceil(0), 1);
+
+ ret &= bfs_check(!has_single_bit(0));
+ ret &= bfs_check(!has_single_bit(UINT32_MAX));
+ ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1)));
+
+ return ret;
+}
diff --git a/tests/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/bsd/L_acl.out b/tests/bsd/L_acl.out
new file mode 100644
index 0000000..dd89800
--- /dev/null
+++ b/tests/bsd/L_acl.out
@@ -0,0 +1,2 @@
+./acl
+./link
diff --git a/tests/bsd/L_acl.sh b/tests/bsd/L_acl.sh
new file mode 100644
index 0000000..a3fcbc8
--- /dev/null
+++ b/tests/bsd/L_acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff -L . -acl
diff --git a/tests/bsd/L_xattr.out b/tests/bsd/L_xattr.out
new file mode 100644
index 0000000..21eb50f
--- /dev/null
+++ b/tests/bsd/L_xattr.out
@@ -0,0 +1,3 @@
+./link
+./xattr
+./xattr_2
diff --git a/tests/bsd/L_xattr.sh b/tests/bsd/L_xattr.sh
new file mode 100644
index 0000000..f8b56d8
--- /dev/null
+++ b/tests/bsd/L_xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff -L . -xattr
diff --git a/tests/bsd/L_xattrname.out b/tests/bsd/L_xattrname.out
new file mode 100644
index 0000000..9e4c172
--- /dev/null
+++ b/tests/bsd/L_xattrname.out
@@ -0,0 +1,2 @@
+./link
+./xattr
diff --git a/tests/bsd/L_xattrname.sh b/tests/bsd/L_xattrname.sh
new file mode 100644
index 0000000..8108d57
--- /dev/null
+++ b/tests/bsd/L_xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff -L . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff -L . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/test_X.out b/tests/bsd/X.out
index afa84f7..afa84f7 100644
--- a/tests/test_X.out
+++ b/tests/bsd/X.out
diff --git a/tests/bsd/X.sh b/tests/bsd/X.sh
new file mode 100644
index 0000000..54000cf
--- /dev/null
+++ b/tests/bsd/X.sh
@@ -0,0 +1 @@
+! bfs_diff -X weirdnames
diff --git a/tests/bsd/acl.out b/tests/bsd/acl.out
new file mode 100644
index 0000000..92e2f67
--- /dev/null
+++ b/tests/bsd/acl.out
@@ -0,0 +1 @@
+./acl
diff --git a/tests/bsd/acl.sh b/tests/bsd/acl.sh
new file mode 100644
index 0000000..a13c75f
--- /dev/null
+++ b/tests/bsd/acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff . -acl
diff --git a/tests/test_asince.out b/tests/bsd/asince.out
index 650e550..650e550 100644
--- a/tests/test_asince.out
+++ b/tests/bsd/asince.out
diff --git a/tests/bsd/asince.sh b/tests/bsd/asince.sh
new file mode 100644
index 0000000..32d5228
--- /dev/null
+++ b/tests/bsd/asince.sh
@@ -0,0 +1 @@
+bfs_diff times -asince 1991-12-14T00:01
diff --git a/tests/test_depth_overflow.out b/tests/bsd/d_path.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_depth_overflow.out
+++ b/tests/bsd/d_path.out
diff --git a/tests/bsd/d_path.sh b/tests/bsd/d_path.sh
new file mode 100644
index 0000000..010f76f
--- /dev/null
+++ b/tests/bsd/d_path.sh
@@ -0,0 +1 @@
+bfs_diff -d basic
diff --git a/tests/test_data_flow_depth.out b/tests/bsd/data_flow_depth.out
index ab127ec..ab127ec 100644
--- a/tests/test_data_flow_depth.out
+++ b/tests/bsd/data_flow_depth.out
diff --git a/tests/bsd/data_flow_depth.sh b/tests/bsd/data_flow_depth.sh
new file mode 100644
index 0000000..cd5d6b2
--- /dev/null
+++ b/tests/bsd/data_flow_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +1 -depth -4
diff --git a/tests/test_exec.out b/tests/bsd/data_flow_sparse.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_exec.out
+++ b/tests/bsd/data_flow_sparse.out
diff --git a/tests/bsd/data_flow_sparse.sh b/tests/bsd/data_flow_sparse.sh
new file mode 100644
index 0000000..bd7e1f7
--- /dev/null
+++ b/tests/bsd/data_flow_sparse.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \)
diff --git a/tests/test_depth_depth_n.out b/tests/bsd/depth_depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_depth_n.out
+++ b/tests/bsd/depth_depth_n.out
diff --git a/tests/bsd/depth_depth_n.sh b/tests/bsd/depth_depth_n.sh
new file mode 100644
index 0000000..5989b3c
--- /dev/null
+++ b/tests/bsd/depth_depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth 2
diff --git a/tests/test_depth_depth_n_minus.out b/tests/bsd/depth_depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_depth_n_minus.out
+++ b/tests/bsd/depth_depth_n_minus.out
diff --git a/tests/bsd/depth_depth_n_minus.sh b/tests/bsd/depth_depth_n_minus.sh
new file mode 100644
index 0000000..1d8ac79
--- /dev/null
+++ b/tests/bsd/depth_depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth -2
diff --git a/tests/test_depth_depth_n_plus.out b/tests/bsd/depth_depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_depth_n_plus.out
+++ b/tests/bsd/depth_depth_n_plus.out
diff --git a/tests/bsd/depth_depth_n_plus.sh b/tests/bsd/depth_depth_n_plus.sh
new file mode 100644
index 0000000..64e392b
--- /dev/null
+++ b/tests/bsd/depth_depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth +2
diff --git a/tests/test_depth_n.out b/tests/bsd/depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_n.out
+++ b/tests/bsd/depth_n.out
diff --git a/tests/bsd/depth_n.sh b/tests/bsd/depth_n.sh
new file mode 100644
index 0000000..4852952
--- /dev/null
+++ b/tests/bsd/depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth 2
diff --git a/tests/test_depth_maxdepth_1.out b/tests/bsd/depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_maxdepth_1.out
+++ b/tests/bsd/depth_n_minus.out
diff --git a/tests/bsd/depth_n_minus.sh b/tests/bsd/depth_n_minus.sh
new file mode 100644
index 0000000..192bf8a
--- /dev/null
+++ b/tests/bsd/depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -2
diff --git a/tests/test_depth_n_plus.out b/tests/bsd/depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_n_plus.out
+++ b/tests/bsd/depth_n_plus.out
diff --git a/tests/bsd/depth_n_plus.sh b/tests/bsd/depth_n_plus.sh
new file mode 100644
index 0000000..858e1c4
--- /dev/null
+++ b/tests/bsd/depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +2
diff --git a/tests/test_exec_nopath.out b/tests/bsd/depth_overflow.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_exec_nopath.out
+++ b/tests/bsd/depth_overflow.out
diff --git a/tests/bsd/depth_overflow.sh b/tests/bsd/depth_overflow.sh
new file mode 100644
index 0000000..4685d03
--- /dev/null
+++ b/tests/bsd/depth_overflow.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -4294967296
diff --git a/tests/test_exit.out b/tests/bsd/exit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_exit.out
+++ b/tests/bsd/exit.out
diff --git a/tests/bsd/exit.sh b/tests/bsd/exit.sh
new file mode 100644
index 0000000..248349c
--- /dev/null
+++ b/tests/bsd/exit.sh
@@ -0,0 +1,5 @@
+check_exit 42 invoke_bfs basic -name foo -exit 42
+
+check_exit 0 invoke_bfs basic -name qux -exit 42
+
+bfs_diff basic/g -print -name g -exit
diff --git a/tests/test_exclude_mindepth.out b/tests/bsd/exit_no_implicit_print.out
index e69de29..e69de29 100644
--- a/tests/test_exclude_mindepth.out
+++ b/tests/bsd/exit_no_implicit_print.out
diff --git a/tests/bsd/exit_no_implicit_print.sh b/tests/bsd/exit_no_implicit_print.sh
new file mode 100644
index 0000000..c48b43c
--- /dev/null
+++ b/tests/bsd/exit_no_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo -o -exit
diff --git a/tests/test_f.out b/tests/bsd/f.out
index 77eac77..77eac77 100644
--- a/tests/test_f.out
+++ b/tests/bsd/f.out
diff --git a/tests/bsd/f.sh b/tests/bsd/f.sh
new file mode 100644
index 0000000..42d2dfd
--- /dev/null
+++ b/tests/bsd/f.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -f '-' -f '('
diff --git a/tests/bsd/f_incomplete.sh b/tests/bsd/f_incomplete.sh
new file mode 100644
index 0000000..0dfb19f
--- /dev/null
+++ b/tests/bsd/f_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -f
diff --git a/tests/bsd/flags.out b/tests/bsd/flags.out
new file mode 100644
index 0000000..3216ff5
--- /dev/null
+++ b/tests/bsd/flags.out
@@ -0,0 +1 @@
+./bar
diff --git a/tests/bsd/flags.sh b/tests/bsd/flags.sh
new file mode 100644
index 0000000..eb9bc22
--- /dev/null
+++ b/tests/bsd/flags.sh
@@ -0,0 +1,8 @@
+invoke_bfs . -quit -flags offline || skip
+
+cd "$TEST"
+
+"$XTOUCH" foo bar
+chflags offline bar || skip
+
+bfs_diff . -flags -offline,nohidden
diff --git a/tests/test_exec_plus_status.out b/tests/bsd/gid_name.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_exec_plus_status.out
+++ b/tests/bsd/gid_name.out
diff --git a/tests/bsd/gid_name.sh b/tests/bsd/gid_name.sh
new file mode 100644
index 0000000..c7e0e71
--- /dev/null
+++ b/tests/bsd/gid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -gn)"
diff --git a/tests/test_H_newer.out b/tests/bsd/mnewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_H_newer.out
+++ b/tests/bsd/mnewer.out
diff --git a/tests/bsd/mnewer.sh b/tests/bsd/mnewer.sh
new file mode 100644
index 0000000..5d9f1a7
--- /dev/null
+++ b/tests/bsd/mnewer.sh
@@ -0,0 +1 @@
+bfs_diff times -mnewer times/a
diff --git a/tests/test_msince.out b/tests/bsd/msince.out
index 650e550..650e550 100644
--- a/tests/test_msince.out
+++ b/tests/bsd/msince.out
diff --git a/tests/bsd/msince.sh b/tests/bsd/msince.sh
new file mode 100644
index 0000000..ec22f02
--- /dev/null
+++ b/tests/bsd/msince.sh
@@ -0,0 +1 @@
+bfs_diff times -msince 1991-12-14T00:01
diff --git a/tests/bsd/mtime_bad_unit.sh b/tests/bsd/mtime_bad_unit.sh
new file mode 100644
index 0000000..6e2caf1
--- /dev/null
+++ b/tests/bsd/mtime_bad_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1q
diff --git a/tests/bsd/mtime_missing_unit.sh b/tests/bsd/mtime_missing_unit.sh
new file mode 100644
index 0000000..f6b1f93
--- /dev/null
+++ b/tests/bsd/mtime_missing_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1w2
diff --git a/tests/test_mtime_units.out b/tests/bsd/mtime_units.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_mtime_units.out
+++ b/tests/bsd/mtime_units.out
diff --git a/tests/bsd/mtime_units.sh b/tests/bsd/mtime_units.sh
new file mode 100644
index 0000000..a1e587e
--- /dev/null
+++ b/tests/bsd/mtime_units.sh
@@ -0,0 +1 @@
+bfs_diff times -mtime +500w400d300h200m100s
diff --git a/tests/test_okdir_stdin.out b/tests/bsd/okdir_stdin.out
index ef2a68b..ef2a68b 100644
--- a/tests/test_okdir_stdin.out
+++ b/tests/bsd/okdir_stdin.out
diff --git a/tests/bsd/okdir_stdin.sh b/tests/bsd/okdir_stdin.sh
new file mode 100644
index 0000000..7908ac0
--- /dev/null
+++ b/tests/bsd/okdir_stdin.sh
@@ -0,0 +1,2 @@
+# -okdir should *not* close stdin
+yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/test_perm_000_minus.out b/tests/bsd/perm_000_plus.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_minus.out
+++ b/tests/bsd/perm_000_plus.out
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/test_perm_222_plus.out b/tests/bsd/perm_222_plus.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_perm_222_plus.out
+++ b/tests/bsd/perm_222_plus.out
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/test_perm_644_plus.out b/tests/bsd/perm_644_plus.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_644_plus.out
+++ b/tests/bsd/perm_644_plus.out
diff --git a/tests/bsd/perm_644_plus.sh b/tests/bsd/perm_644_plus.sh
new file mode 100644
index 0000000..b3f5bc6
--- /dev/null
+++ b/tests/bsd/perm_644_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +644
diff --git a/tests/test_printx.out b/tests/bsd/printx.out
index 04bf9a9..04bf9a9 100644
--- a/tests/test_printx.out
+++ b/tests/bsd/printx.out
diff --git a/tests/bsd/printx.sh b/tests/bsd/printx.sh
new file mode 100644
index 0000000..cb24aab
--- /dev/null
+++ b/tests/bsd/printx.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -printx
diff --git a/tests/test_and_false_or_true.out b/tests/bsd/quit_implicit_print.out
index 15a13db..15a13db 100644
--- a/tests/test_and_false_or_true.out
+++ b/tests/bsd/quit_implicit_print.out
diff --git a/tests/bsd/quit_implicit_print.sh b/tests/bsd/quit_implicit_print.sh
new file mode 100644
index 0000000..ea8fd5d
--- /dev/null
+++ b/tests/bsd/quit_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name basic -o -quit
diff --git a/tests/bsd/rm.out b/tests/bsd/rm.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/bsd/rm.out
@@ -0,0 +1 @@
+.
diff --git a/tests/bsd/rm.sh b/tests/bsd/rm.sh
new file mode 100644
index 0000000..595d514
--- /dev/null
+++ b/tests/bsd/rm.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -rm
+bfs_diff .
diff --git a/tests/test_s.out b/tests/bsd/s.out
index 6b790c2..6b790c2 100644
--- a/tests/test_s.out
+++ b/tests/bsd/s.out
diff --git a/tests/bsd/s.sh b/tests/bsd/s.sh
new file mode 100644
index 0000000..52f8eb3
--- /dev/null
+++ b/tests/bsd/s.sh
@@ -0,0 +1,2 @@
+invoke_bfs -s weirdnames -maxdepth 1 >"$OUT"
+diff_output
diff --git a/tests/bsd/s_quit.out b/tests/bsd/s_quit.out
new file mode 100644
index 0000000..5ea492b
--- /dev/null
+++ b/tests/bsd/s_quit.out
@@ -0,0 +1 @@
+basic/j/foo
diff --git a/tests/bsd/s_quit.sh b/tests/bsd/s_quit.sh
new file mode 100644
index 0000000..6bd55ab
--- /dev/null
+++ b/tests/bsd/s_quit.sh
@@ -0,0 +1,4 @@
+# Regression test: bfs -S ids -s -name foo -quit would not actually quit,
+# ending up in a confused state and erroring/crashing
+
+bfs_diff -s basic -name foo -print -quit
diff --git a/tests/test_size_T.out b/tests/bsd/size_T.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_T.out
+++ b/tests/bsd/size_T.out
diff --git a/tests/bsd/size_T.sh b/tests/bsd/size_T.sh
new file mode 100644
index 0000000..1023a10
--- /dev/null
+++ b/tests/bsd/size_T.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 1T
diff --git a/tests/bsd/sparse.out b/tests/bsd/sparse.out
new file mode 100644
index 0000000..52dcf49
--- /dev/null
+++ b/tests/bsd/sparse.out
@@ -0,0 +1 @@
+mnt/sparse
diff --git a/tests/bsd/sparse.sh b/tests/bsd/sparse.sh
new file mode 100644
index 0000000..7fcdeed
--- /dev/null
+++ b/tests/bsd/sparse.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+truncate -s 1M mnt/sparse
+dd if=/dev/zero of=mnt/dense bs=1M count=1
+
+bfs_diff mnt -type f -sparse
diff --git a/tests/bsd/type_w.out b/tests/bsd/type_w.out
new file mode 100644
index 0000000..a20a4f3
--- /dev/null
+++ b/tests/bsd/type_w.out
@@ -0,0 +1,34 @@
+1: -rw-r--r-- mnt/lower/bar
+1: -rw-r--r-- mnt/lower/baz
+1: -rw-r--r-- mnt/lower/foo
+1: -rw-r--r-- mnt/upper/baz/qux
+1: -rw-r--r-- mnt/upper/foo
+1: drwxr-xr-x mnt/lower
+1: drwxr-xr-x mnt/upper
+1: drwxr-xr-x mnt/upper/baz
+2: w--------- mnt/upper/bar
+3: -rw-r--r-- mnt/lower/bar
+3: -rw-r--r-- mnt/lower/baz
+3: -rw-r--r-- mnt/lower/foo
+3: -rw-r--r-- mnt/upper/baz/qux
+3: -rw-r--r-- mnt/upper/foo
+3: drwxr-xr-x mnt/lower
+3: drwxr-xr-x mnt/upper
+3: drwxr-xr-x mnt/upper/baz
+3: w--------- mnt/upper/bar
+4: -rw-r--r-- mnt/lower/bar
+4: -rw-r--r-- mnt/lower/baz
+4: -rw-r--r-- mnt/lower/foo
+4: -rw-r--r-- mnt/upper/baz/qux
+4: drwxr-xr-x mnt/lower
+4: drwxr-xr-x mnt/upper
+4: drwxr-xr-x mnt/upper/baz
+5: w--------- mnt/upper/bar
+6: -rw-r--r-- mnt/lower/bar
+6: -rw-r--r-- mnt/lower/baz
+6: -rw-r--r-- mnt/lower/foo
+6: -rw-r--r-- mnt/upper/baz/qux
+6: drwxr-xr-x mnt/lower
+6: drwxr-xr-x mnt/upper
+6: drwxr-xr-x mnt/upper/baz
+6: w--------- mnt/upper/bar
diff --git a/tests/bsd/type_w.sh b/tests/bsd/type_w.sh
new file mode 100644
index 0000000..3aa50d5
--- /dev/null
+++ b/tests/bsd/type_w.sh
@@ -0,0 +1,56 @@
+# Only ffs supports whiteouts on FreeBSD
+command -v mdconfig &>/dev/null || skip
+command -v newfs &>/dev/null || skip
+
+cd "$TEST"
+
+# Create a ramdisk
+if command -v truncate &>/dev/null; then
+ truncate -s1M img
+else
+ dd if=/dev/zero of=img bs=1k count=1k
+fi
+md=$(bfs_sudo mdconfig img) || skip
+defer bfs_sudo mdconfig -du "$md"
+
+# Make an ffs filesystem
+bfs_sudo newfs -n "/dev/$md" >&2 || skip
+mkdir mnt
+
+# Mount it
+bfs_sudo mount "/dev/$md" mnt || skip
+defer bfs_sudo umount mnt
+
+# Make it owned by us
+bfs_sudo chown "$(id -u):$(id -g)" mnt
+"$XTOUCH" -p mnt/{lower/{foo,bar,baz},upper/{bar,baz/qux}}
+
+# Mount a union filesystem within it
+bfs_sudo mount -t unionfs -o below mnt/{lower,upper}
+defer bfs_sudo umount mnt/upper
+
+# Create a whiteout
+rm mnt/upper/bar
+
+# FreeBSD find doesn't have -printf, so munge -ls output
+munge_ls() {
+ sed -En 's|.*([-drwx]{10}).*(mnt/.*)|'"$1"': \1 \2|p'
+}
+
+# Do a few tests in one
+{
+ # Normally, we shouldn't see the whiteouts
+ invoke_bfs mnt -ls | munge_ls 1
+ # -type w adds whiteouts to the output
+ invoke_bfs mnt -type w -ls | munge_ls 2
+ # So this is not the same as test 1
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 3
+ # Unmount the unionfs
+ pop_defer
+ # Now repeat the same tests
+ invoke_bfs mnt -ls | munge_ls 4
+ invoke_bfs mnt -type w -ls | munge_ls 5
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 6
+} >"$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprint.out b/tests/bsd/uid_name.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_fprint.out
+++ b/tests/bsd/uid_name.out
diff --git a/tests/bsd/uid_name.sh b/tests/bsd/uid_name.sh
new file mode 100644
index 0000000..7d3ba82
--- /dev/null
+++ b/tests/bsd/uid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -un)"
diff --git a/tests/bsd/xattr.out b/tests/bsd/xattr.out
new file mode 100644
index 0000000..0afed35
--- /dev/null
+++ b/tests/bsd/xattr.out
@@ -0,0 +1,3 @@
+./xattr
+./xattr_2
+./xattr_link
diff --git a/tests/bsd/xattr.sh b/tests/bsd/xattr.sh
new file mode 100644
index 0000000..68f729a
--- /dev/null
+++ b/tests/bsd/xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff . -xattr
diff --git a/tests/bsd/xattrname.out b/tests/bsd/xattrname.out
new file mode 100644
index 0000000..ef732bd
--- /dev/null
+++ b/tests/bsd/xattrname.out
@@ -0,0 +1,2 @@
+./xattr
+./xattr_link
diff --git a/tests/bsd/xattrname.sh b/tests/bsd/xattrname.sh
new file mode 100644
index 0000000..38b111a
--- /dev/null
+++ b/tests/bsd/xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/color.sh b/tests/color.sh
new file mode 100644
index 0000000..805d2b8
--- /dev/null
+++ b/tests/color.sh
@@ -0,0 +1,37 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Colored output
+
+# Common escape sequences
+BLD=$'\e[01m'
+RED=$'\e[01;31m'
+GRN=$'\e[01;32m'
+YLW=$'\e[01;33m'
+BLU=$'\e[01;34m'
+MAG=$'\e[01;35m'
+CYN=$'\e[01;36m'
+RST=$'\e[0m'
+
+# Check if we should color output to the given fd
+color_fd() {
+ [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
+}
+
+# Cache the color status for std{out,err}
+color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0
+color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0
+
+# Save this in case the tests unset PATH
+SED=$(command -v sed)
+
+# Filter out escape sequences if necessary
+color() {
+ if color_fd 1; then
+ "$@"
+ else
+ "$@" | "$SED" $'s/\e\\[[^m]*m//g'
+ fi
+}
diff --git a/tests/test_anewer.out b/tests/common/H_newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_anewer.out
+++ b/tests/common/H_newer.out
diff --git a/tests/common/H_newer.sh b/tests/common/H_newer.sh
new file mode 100644
index 0000000..c72bff7
--- /dev/null
+++ b/tests/common/H_newer.sh
@@ -0,0 +1 @@
+bfs_diff -H times -newer times/l
diff --git a/tests/test_H_broken.out b/tests/common/H_samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_H_broken.out
+++ b/tests/common/H_samefile_broken.out
diff --git a/tests/common/H_samefile_broken.sh b/tests/common/H_samefile_broken.sh
new file mode 100644
index 0000000..7a3366d
--- /dev/null
+++ b/tests/common/H_samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/broken
diff --git a/tests/test_H_notdir.out b/tests/common/H_samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_H_notdir.out
+++ b/tests/common/H_samefile_notdir.out
diff --git a/tests/common/H_samefile_notdir.sh b/tests/common/H_samefile_notdir.sh
new file mode 100644
index 0000000..25ad88d
--- /dev/null
+++ b/tests/common/H_samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/notdir
diff --git a/tests/test_H_samefile_symlink.out b/tests/common/H_samefile_symlink.out
index 996ffc8..996ffc8 100644
--- a/tests/test_H_samefile_symlink.out
+++ b/tests/common/H_samefile_symlink.out
diff --git a/tests/common/H_samefile_symlink.sh b/tests/common/H_samefile_symlink.sh
new file mode 100644
index 0000000..c73ddb2
--- /dev/null
+++ b/tests/common/H_samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/symlink
diff --git a/tests/test_exit_no_implicit_print.out b/tests/common/L_ilname.out
index e69de29..e69de29 100644
--- a/tests/test_exit_no_implicit_print.out
+++ b/tests/common/L_ilname.out
diff --git a/tests/common/L_ilname.sh b/tests/common/L_ilname.sh
new file mode 100644
index 0000000..e0495ed
--- /dev/null
+++ b/tests/common/L_ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff -L links -ilname '[AQ]'
diff --git a/tests/test_false.out b/tests/common/L_lname.out
index e69de29..e69de29 100644
--- a/tests/test_false.out
+++ b/tests/common/L_lname.out
diff --git a/tests/common/L_lname.sh b/tests/common/L_lname.sh
new file mode 100644
index 0000000..65b9da5
--- /dev/null
+++ b/tests/common/L_lname.sh
@@ -0,0 +1 @@
+bfs_diff -L links -lname '[aq]'
diff --git a/tests/common/L_ls.sh b/tests/common/L_ls.sh
new file mode 100644
index 0000000..7ee2b44
--- /dev/null
+++ b/tests/common/L_ls.sh
@@ -0,0 +1 @@
+invoke_bfs -L rainbow -ls >"$OUT"
diff --git a/tests/common/L_mount.out b/tests/common/L_mount.out
new file mode 100644
index 0000000..788579d
--- /dev/null
+++ b/tests/common/L_mount.out
@@ -0,0 +1,5 @@
+.
+./foo
+./foo/bar
+./foo/qux
+./mnt
diff --git a/tests/common/L_mount.sh b/tests/common/L_mount.sh
new file mode 100644
index 0000000..fd8042a
--- /dev/null
+++ b/tests/common/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_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/test_H.out b/tests/common/P.out
index ff635ff..ff635ff 100644
--- a/tests/test_H.out
+++ b/tests/common/P.out
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/test_mnewer.out b/tests/common/anewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_mnewer.out
+++ b/tests/common/anewer.out
diff --git a/tests/common/anewer.sh b/tests/common/anewer.sh
new file mode 100644
index 0000000..0cdd759
--- /dev/null
+++ b/tests/common/anewer.sh
@@ -0,0 +1 @@
+bfs_diff times -anewer times/a
diff --git a/tests/common/delete.out b/tests/common/delete.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete.sh b/tests/common/delete.sh
new file mode 100644
index 0000000..638f307
--- /dev/null
+++ b/tests/common/delete.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -delete
+bfs_diff .
diff --git a/tests/common/delete_error.out b/tests/common/delete_error.out
new file mode 100644
index 0000000..b6b6505
--- /dev/null
+++ b/tests/common/delete_error.out
@@ -0,0 +1,8 @@
+.
+.
+./baz
+./baz
+./baz/qux
+./baz/qux
+./foo
+./foo/bar
diff --git a/tests/common/delete_error.sh b/tests/common/delete_error.sh
new file mode 100644
index 0000000..e6327f3
--- /dev/null
+++ b/tests/common/delete_error.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/bar baz/qux
+chmod -w foo
+defer chmod +w foo
+
+! invoke_bfs . -print -delete -print >"$OUT" || fail
+sort_output
+diff_output
diff --git a/tests/common/delete_many.out b/tests/common/delete_many.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete_many.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete_many.sh b/tests/common/delete_many.sh
new file mode 100644
index 0000000..48fe4c2
--- /dev/null
+++ b/tests/common/delete_many.sh
@@ -0,0 +1,8 @@
+# Test for https://github.com/tavianator/bfs/issues/67
+
+cd "$TEST"
+mkdir foo
+"$XTOUCH" foo/{1..256}
+
+invoke_bfs foo -delete
+bfs_diff .
diff --git a/tests/test_depth_n_minus.out b/tests/common/depth_maxdepth_1.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_n_minus.out
+++ b/tests/common/depth_maxdepth_1.out
diff --git a/tests/common/depth_maxdepth_1.sh b/tests/common/depth_maxdepth_1.sh
new file mode 100644
index 0000000..4b7e538
--- /dev/null
+++ b/tests/common/depth_maxdepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1 -depth
diff --git a/tests/test_depth_maxdepth_2.out b/tests/common/depth_maxdepth_2.out
index c53041e..c53041e 100644
--- a/tests/test_depth_maxdepth_2.out
+++ b/tests/common/depth_maxdepth_2.out
diff --git a/tests/common/depth_maxdepth_2.sh b/tests/common/depth_maxdepth_2.sh
new file mode 100644
index 0000000..2c49a65
--- /dev/null
+++ b/tests/common/depth_maxdepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 2 -depth
diff --git a/tests/test_depth_mindepth_1.out b/tests/common/depth_mindepth_1.out
index 3b461cf..3b461cf 100644
--- a/tests/test_depth_mindepth_1.out
+++ b/tests/common/depth_mindepth_1.out
diff --git a/tests/common/depth_mindepth_1.sh b/tests/common/depth_mindepth_1.sh
new file mode 100644
index 0000000..868d9e1
--- /dev/null
+++ b/tests/common/depth_mindepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1 -depth
diff --git a/tests/test_depth_mindepth_2.out b/tests/common/depth_mindepth_2.out
index 6ccd80a..6ccd80a 100644
--- a/tests/test_depth_mindepth_2.out
+++ b/tests/common/depth_mindepth_2.out
diff --git a/tests/common/depth_mindepth_2.sh b/tests/common/depth_mindepth_2.sh
new file mode 100644
index 0000000..2031b2c
--- /dev/null
+++ b/tests/common/depth_mindepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 2 -depth
diff --git a/tests/test_double_dash.out b/tests/common/double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_double_dash.out
+++ b/tests/common/double_dash.out
diff --git a/tests/common/double_dash.sh b/tests/common/double_dash.sh
new file mode 100644
index 0000000..1a7a118
--- /dev/null
+++ b/tests/common/double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -- . -type f
diff --git a/tests/test_empty.out b/tests/common/empty.out
index a0f4b76..a0f4b76 100644
--- a/tests/test_empty.out
+++ b/tests/common/empty.out
diff --git a/tests/common/empty.sh b/tests/common/empty.sh
new file mode 100644
index 0000000..95ee988
--- /dev/null
+++ b/tests/common/empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -empty
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
new file mode 100644
index 0000000..da45e23
--- /dev/null
+++ b/tests/common/empty_error.out
@@ -0,0 +1,3 @@
+./bar
+./baz
+./qux
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
new file mode 100644
index 0000000..7c8049c
--- /dev/null
+++ b/tests/common/empty_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/ bar/ baz qux
+chmod -r foo baz
+defer chmod +r foo baz
+
+! bfs_diff . -empty
diff --git a/tests/common/empty_special.out b/tests/common/empty_special.out
new file mode 100644
index 0000000..fa35478
--- /dev/null
+++ b/tests/common/empty_special.out
@@ -0,0 +1,20 @@
+rainbow//
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/sgid
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/common/empty_special.sh b/tests/common/empty_special.sh
new file mode 100644
index 0000000..31e9d2e
--- /dev/null
+++ b/tests/common/empty_special.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -empty
diff --git a/tests/test_exec_substring.out b/tests/common/exec_substring.out
index 32a6353..32a6353 100644
--- a/tests/test_exec_substring.out
+++ b/tests/common/exec_substring.out
diff --git a/tests/common/exec_substring.sh b/tests/common/exec_substring.sh
new file mode 100644
index 0000000..5cf8e85
--- /dev/null
+++ b/tests/common/exec_substring.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec echo '-{}-' \;
diff --git a/tests/test_fstype.out b/tests/common/execdir_nonexistent.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_fstype.out
+++ b/tests/common/execdir_nonexistent.out
diff --git a/tests/common/execdir_nonexistent.sh b/tests/common/execdir_nonexistent.sh
new file mode 100644
index 0000000..0ec013c
--- /dev/null
+++ b/tests/common/execdir_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_execdir_pwd.out b/tests/common/execdir_pwd.out
index a11cd25..a11cd25 100644
--- a/tests/test_execdir_pwd.out
+++ b/tests/common/execdir_pwd.out
diff --git a/tests/common/execdir_pwd.sh b/tests/common/execdir_pwd.sh
new file mode 100644
index 0000000..1c0165a
--- /dev/null
+++ b/tests/common/execdir_pwd.sh
@@ -0,0 +1,3 @@
+TMP_REAL=$(cd "$TMP" && pwd)
+OFFSET=$((${#TMP_REAL} + 2))
+bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \;
diff --git a/tests/test_execdir_slash.out b/tests/common/execdir_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash.out
+++ b/tests/common/execdir_slash.out
diff --git a/tests/common/execdir_slash.sh b/tests/common/execdir_slash.sh
new file mode 100644
index 0000000..965f679
--- /dev/null
+++ b/tests/common/execdir_slash.sh
@@ -0,0 +1,2 @@
+# Don't prepend ./ for absolute paths in -execdir
+bfs_diff / -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_slash_pwd.out b/tests/common/execdir_slash_pwd.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash_pwd.out
+++ b/tests/common/execdir_slash_pwd.out
diff --git a/tests/common/execdir_slash_pwd.sh b/tests/common/execdir_slash_pwd.sh
new file mode 100644
index 0000000..9c82e09
--- /dev/null
+++ b/tests/common/execdir_slash_pwd.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -execdir pwd \;
diff --git a/tests/test_execdir_slashes.out b/tests/common/execdir_slashes.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slashes.out
+++ b/tests/common/execdir_slashes.out
diff --git a/tests/common/execdir_slashes.sh b/tests/common/execdir_slashes.sh
new file mode 100644
index 0000000..4e2b327
--- /dev/null
+++ b/tests/common/execdir_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_ulimit.out b/tests/common/execdir_ulimit.out
index 7f53982..bf52c09 100644
--- a/tests/test_execdir_ulimit.out
+++ b/tests/common/execdir_ulimit.out
@@ -1,3 +1,4 @@
+./.
./0
./1
./2
@@ -30,7 +31,6 @@
./q
./r
./s
-./scratch
./t
./u
./v
diff --git a/tests/common/execdir_ulimit.sh b/tests/common/execdir_ulimit.sh
new file mode 100644
index 0000000..122c282
--- /dev/null
+++ b/tests/common/execdir_ulimit.sh
@@ -0,0 +1,6 @@
+cd "$TEST"
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
+
+ulimit -n $((NOPENFD + 10))
+bfs_diff . -execdir echo {} \;
diff --git a/tests/test_flag_double_dash.out b/tests/common/flag_double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_flag_double_dash.out
+++ b/tests/common/flag_double_dash.out
diff --git a/tests/common/flag_double_dash.sh b/tests/common/flag_double_dash.sh
new file mode 100644
index 0000000..1075b06
--- /dev/null
+++ b/tests/common/flag_double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -L -- . -type f
diff --git a/tests/test_L.out b/tests/common/follow.out
index ec9e861..ec9e861 100644
--- a/tests/test_L.out
+++ b/tests/common/follow.out
diff --git a/tests/common/follow.sh b/tests/common/follow.sh
new file mode 100644
index 0000000..b5a2ae1
--- /dev/null
+++ b/tests/common/follow.sh
@@ -0,0 +1 @@
+bfs_diff links -follow
diff --git a/tests/test_ilname.out b/tests/common/ilname.out
index e69de29..e69de29 100644
--- a/tests/test_ilname.out
+++ b/tests/common/ilname.out
diff --git a/tests/common/ilname.sh b/tests/common/ilname.sh
new file mode 100644
index 0000000..fc7e9e4
--- /dev/null
+++ b/tests/common/ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff links -ilname '[AQ]'
diff --git a/tests/test_iname.out b/tests/common/iname.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_iname.out
+++ b/tests/common/iname.out
diff --git a/tests/common/iname.sh b/tests/common/iname.sh
new file mode 100644
index 0000000..c25a646
--- /dev/null
+++ b/tests/common/iname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -iname PATTERN || skip
+bfs_diff basic -iname '*F*'
diff --git a/tests/test_inum.out b/tests/common/inum.out
index ebcaf79..ebcaf79 100644
--- a/tests/test_inum.out
+++ b/tests/common/inum.out
diff --git a/tests/common/inum.sh b/tests/common/inum.sh
new file mode 100644
index 0000000..ca63bbb
--- /dev/null
+++ b/tests/common/inum.sh
@@ -0,0 +1 @@
+bfs_diff basic -inum "$(inum basic/k/foo/bar)"
diff --git a/tests/common/inum_bind_mount.out b/tests/common/inum_bind_mount.out
new file mode 100644
index 0000000..ede8749
--- /dev/null
+++ b/tests/common/inum_bind_mount.out
@@ -0,0 +1,2 @@
+./bar
+./foo
diff --git a/tests/common/inum_bind_mount.sh b/tests/common/inum_bind_mount.sh
new file mode 100644
index 0000000..892713e
--- /dev/null
+++ b/tests/common/inum_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" foo bar baz
+
+bfs_sudo mount --bind foo bar || skip
+defer bfs_sudo umount bar
+
+bfs_diff . -inum "$(inum bar)"
diff --git a/tests/common/inum_mount.out b/tests/common/inum_mount.out
new file mode 100644
index 0000000..99fa01e
--- /dev/null
+++ b/tests/common/inum_mount.out
@@ -0,0 +1 @@
+./mnt
diff --git a/tests/common/inum_mount.sh b/tests/common/inum_mount.sh
new file mode 100644
index 0000000..7facf57
--- /dev/null
+++ b/tests/common/inum_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff . -inum "$(inum mnt)"
diff --git a/tests/test_ipath.out b/tests/common/ipath.out
index ae1ae21..ae1ae21 100644
--- a/tests/test_ipath.out
+++ b/tests/common/ipath.out
diff --git a/tests/common/ipath.sh b/tests/common/ipath.sh
new file mode 100644
index 0000000..7d05f31
--- /dev/null
+++ b/tests/common/ipath.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ipath PATTERN || skip
+bfs_diff basic -ipath 'basic/*F*'
diff --git a/tests/test_iregex.out b/tests/common/iregex.out
index cfc113b..cfc113b 100644
--- a/tests/test_iregex.out
+++ b/tests/common/iregex.out
diff --git a/tests/common/iregex.sh b/tests/common/iregex.sh
new file mode 100644
index 0000000..fc782f5
--- /dev/null
+++ b/tests/common/iregex.sh
@@ -0,0 +1 @@
+bfs_diff basic -iregex 'basic/[A-Z]/[a-z]'
diff --git a/tests/test_lname.out b/tests/common/lname.out
index e69de29..e69de29 100644
--- a/tests/test_lname.out
+++ b/tests/common/lname.out
diff --git a/tests/common/lname.sh b/tests/common/lname.sh
new file mode 100644
index 0000000..cf8a2a1
--- /dev/null
+++ b/tests/common/lname.sh
@@ -0,0 +1 @@
+bfs_diff links -lname '[aq]'
diff --git a/tests/common/ls.sh b/tests/common/ls.sh
new file mode 100644
index 0000000..bc50d90
--- /dev/null
+++ b/tests/common/ls.sh
@@ -0,0 +1 @@
+invoke_bfs rainbow -ls >"$OUT"
diff --git a/tests/test_maxdepth.out b/tests/common/maxdepth.out
index 7575ae4..7575ae4 100644
--- a/tests/test_maxdepth.out
+++ b/tests/common/maxdepth.out
diff --git a/tests/common/maxdepth.sh b/tests/common/maxdepth.sh
new file mode 100644
index 0000000..bb47cc9
--- /dev/null
+++ b/tests/common/maxdepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1
diff --git a/tests/common/maxdepth_incomplete.sh b/tests/common/maxdepth_incomplete.sh
new file mode 100644
index 0000000..0bcb461
--- /dev/null
+++ b/tests/common/maxdepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -maxdepth
diff --git a/tests/test_mindepth.out b/tests/common/mindepth.out
index 3b461cf..3b461cf 100644
--- a/tests/test_mindepth.out
+++ b/tests/common/mindepth.out
diff --git a/tests/common/mindepth.sh b/tests/common/mindepth.sh
new file mode 100644
index 0000000..22d7770
--- /dev/null
+++ b/tests/common/mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1
diff --git a/tests/common/mindepth_incomplete.sh b/tests/common/mindepth_incomplete.sh
new file mode 100644
index 0000000..6f55a42
--- /dev/null
+++ b/tests/common/mindepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -mindepth
diff --git a/tests/common/mount.out b/tests/common/mount.out
new file mode 100644
index 0000000..6253434
--- /dev/null
+++ b/tests/common/mount.out
@@ -0,0 +1,4 @@
+.
+./foo
+./foo/bar
+./mnt
diff --git a/tests/common/mount.sh b/tests/common/mount.sh
new file mode 100644
index 0000000..c9abde5
--- /dev/null
+++ b/tests/common/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/test_name_slash.out b/tests/common/name_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_name_slash.out
+++ b/tests/common/name_slash.out
diff --git a/tests/common/name_slash.sh b/tests/common/name_slash.sh
new file mode 100644
index 0000000..8d89623
--- /dev/null
+++ b/tests/common/name_slash.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -name /
diff --git a/tests/test_name_slashes.out b/tests/common/name_slashes.out
index 187b81f..187b81f 100644
--- a/tests/test_name_slashes.out
+++ b/tests/common/name_slashes.out
diff --git a/tests/common/name_slashes.sh b/tests/common/name_slashes.sh
new file mode 100644
index 0000000..78d0a84
--- /dev/null
+++ b/tests/common/name_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -name /
diff --git a/tests/test_newer.out b/tests/common/newerma.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newer.out
+++ b/tests/common/newerma.out
diff --git a/tests/common/newerma.sh b/tests/common/newerma.sh
new file mode 100644
index 0000000..b05af8d
--- /dev/null
+++ b/tests/common/newerma.sh
@@ -0,0 +1 @@
+bfs_diff times -newerma times/a
diff --git a/tests/test_newermt.out b/tests/common/newermt.out
index 650e550..650e550 100644
--- a/tests/test_newermt.out
+++ b/tests/common/newermt.out
diff --git a/tests/common/newermt.sh b/tests/common/newermt.sh
new file mode 100644
index 0000000..e816b29
--- /dev/null
+++ b/tests/common/newermt.sh
@@ -0,0 +1,3 @@
+bfs_diff times -newermt 1991-12-14T00:01 \
+ -newermt "1991-12-14 01:01+01:00" \
+ -newermt "19911213 20:31:00-0330"
diff --git a/tests/test_newermt_epoch_minus_one.out b/tests/common/newermt_epoch_minus_one.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_newermt_epoch_minus_one.out
+++ b/tests/common/newermt_epoch_minus_one.out
diff --git a/tests/common/newermt_epoch_minus_one.sh b/tests/common/newermt_epoch_minus_one.sh
new file mode 100644
index 0000000..568e2f3
--- /dev/null
+++ b/tests/common/newermt_epoch_minus_one.sh
@@ -0,0 +1 @@
+bfs_diff times -newermt 1969-12-31T23:59:59Z
diff --git a/tests/test_name_backslash.out b/tests/common/ok_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_name_backslash.out
+++ b/tests/common/ok_closed_stdin.out
diff --git a/tests/common/ok_closed_stdin.sh b/tests/common/ok_closed_stdin.sh
new file mode 100644
index 0000000..687e998
--- /dev/null
+++ b/tests/common/ok_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -ok echo \; <&-
diff --git a/tests/test_nogroup.out b/tests/common/okdir_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup.out
+++ b/tests/common/okdir_closed_stdin.out
diff --git a/tests/common/okdir_closed_stdin.sh b/tests/common/okdir_closed_stdin.sh
new file mode 100644
index 0000000..a515298
--- /dev/null
+++ b/tests/common/okdir_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -okdir echo {} \; <&-
diff --git a/tests/test_name_root_depth.out b/tests/common/quit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_name_root_depth.out
+++ b/tests/common/quit.out
diff --git a/tests/common/quit.sh b/tests/common/quit.sh
new file mode 100644
index 0000000..46b60c5
--- /dev/null
+++ b/tests/common/quit.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name g -quit
diff --git a/tests/test_comma_reachability.out b/tests/common/quit_after_print.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_reachability.out
+++ b/tests/common/quit_after_print.out
diff --git a/tests/common/quit_after_print.sh b/tests/common/quit_after_print.sh
new file mode 100644
index 0000000..ee5653a
--- /dev/null
+++ b/tests/common/quit_after_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -print -quit
diff --git a/tests/test_nogroup_ulimit.out b/tests/common/quit_before_print.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup_ulimit.out
+++ b/tests/common/quit_before_print.out
diff --git a/tests/common/quit_before_print.sh b/tests/common/quit_before_print.sh
new file mode 100644
index 0000000..cda3a2c
--- /dev/null
+++ b/tests/common/quit_before_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -quit -print
diff --git a/tests/test_quit_child.out b/tests/common/quit_child.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_child.out
+++ b/tests/common/quit_child.out
diff --git a/tests/common/quit_child.sh b/tests/common/quit_child.sh
new file mode 100644
index 0000000..bd27eff
--- /dev/null
+++ b/tests/common/quit_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name h -quit
diff --git a/tests/test_quit_depth.out b/tests/common/quit_depth.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_depth.out
+++ b/tests/common/quit_depth.out
diff --git a/tests/common/quit_depth.sh b/tests/common/quit_depth.sh
new file mode 100644
index 0000000..f5f82ba
--- /dev/null
+++ b/tests/common/quit_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name g -quit
diff --git a/tests/test_quit_depth_child.out b/tests/common/quit_depth_child.out
index 72b086d..72b086d 100644
--- a/tests/test_quit_depth_child.out
+++ b/tests/common/quit_depth_child.out
diff --git a/tests/common/quit_depth_child.sh b/tests/common/quit_depth_child.sh
new file mode 100644
index 0000000..dd09d5b
--- /dev/null
+++ b/tests/common/quit_depth_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name h -quit
diff --git a/tests/test_regex.out b/tests/common/regex.out
index cfc113b..cfc113b 100644
--- a/tests/test_regex.out
+++ b/tests/common/regex.out
diff --git a/tests/common/regex.sh b/tests/common/regex.sh
new file mode 100644
index 0000000..a3bdae8
--- /dev/null
+++ b/tests/common/regex.sh
@@ -0,0 +1 @@
+bfs_diff basic -regex 'basic/./.'
diff --git a/tests/test_regex_parens.out b/tests/common/regex_parens.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regex_parens.out
+++ b/tests/common/regex_parens.out
diff --git a/tests/common/regex_parens.sh b/tests/common/regex_parens.sh
new file mode 100644
index 0000000..fe0abf6
--- /dev/null
+++ b/tests/common/regex_parens.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . -regex '\./\((\)'
diff --git a/tests/test_links.out b/tests/common/samefile.out
index 996ffc8..996ffc8 100644
--- a/tests/test_links.out
+++ b/tests/common/samefile.out
diff --git a/tests/common/samefile.sh b/tests/common/samefile.sh
new file mode 100644
index 0000000..8e51966
--- /dev/null
+++ b/tests/common/samefile.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/file
diff --git a/tests/test_L_broken.out b/tests/common/samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_L_broken.out
+++ b/tests/common/samefile_broken.out
diff --git a/tests/common/samefile_broken.sh b/tests/common/samefile_broken.sh
new file mode 100644
index 0000000..1cb52db
--- /dev/null
+++ b/tests/common/samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/broken
diff --git a/tests/test_L_notdir.out b/tests/common/samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_L_notdir.out
+++ b/tests/common/samefile_notdir.out
diff --git a/tests/common/samefile_notdir.sh b/tests/common/samefile_notdir.sh
new file mode 100644
index 0000000..f274ef6
--- /dev/null
+++ b/tests/common/samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/notdir
diff --git a/tests/test_samefile_symlink.out b/tests/common/samefile_symlink.out
index 299a572..299a572 100644
--- a/tests/test_samefile_symlink.out
+++ b/tests/common/samefile_symlink.out
diff --git a/tests/common/samefile_symlink.sh b/tests/common/samefile_symlink.sh
new file mode 100644
index 0000000..55ccf5c
--- /dev/null
+++ b/tests/common/samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/symlink
diff --git a/tests/common/samefile_wordesc.sh b/tests/common/samefile_wordesc.sh
new file mode 100644
index 0000000..b5d158f
--- /dev/null
+++ b/tests/common/samefile_wordesc.sh
@@ -0,0 +1,4 @@
+# Regression test: don't abort on incomplete UTF-8 sequences
+export LC_ALL=$(locale -a | grep -Ei 'utf-?8$' | head -n1)
+test -n "$LC_ALL" || skip
+! invoke_bfs -samefile $'\xFA\xFA'
diff --git a/tests/test_nouser.out b/tests/common/size_big.out
index e69de29..e69de29 100644
--- a/tests/test_nouser.out
+++ b/tests/common/size_big.out
diff --git a/tests/common/size_big.sh b/tests/common/size_big.sh
new file mode 100644
index 0000000..6c100eb
--- /dev/null
+++ b/tests/common/size_big.sh
@@ -0,0 +1 @@
+bfs_diff basic -size 9223372036854775807
diff --git a/tests/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..5214e9f
--- /dev/null
+++ b/tests/getopts.sh
@@ -0,0 +1,179 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Argument parsing
+
+if command -v nproc &>/dev/null; then
+ JOBS=$(nproc)
+else
+ JOBS=1
+fi
+MAKE=
+PATTERNS=()
+SUDO=()
+STOP=0
+CLEAN=1
+UPDATE=0
+VERBOSE_COMMANDS=0
+VERBOSE_ERRORS=0
+VERBOSE_SKIPPED=0
+VERBOSE_TESTS=0
+
+# Print usage information
+usage() {
+ local pad=$(printf "%*s" ${#0} "")
+ color cat <<EOF
+Usage: ${GRN}$0${RST}
+ [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]]
+ [${BLU}--stop${RST}] [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
+ [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
+
+ ${BLU}-j${RST}${BLD}N${RST}
+ Run ${BLD}N${RST} tests in parallel (default: ${BLD}$JOBS${RST})
+
+ ${BLU}--make${RST}=${BLD}MAKE${RST}
+ Use the jobserver from ${BLD}MAKE${RST}, e.g. ${BLU}--make${RST}=${BLD}"make -j$JOBS"${RST}
+
+ ${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}
+ Set the path to the bfs executable to test (default: ${BLD}./bin/bfs${RST})
+
+ ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
+ Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
+
+ ${BLU}--stop${RST}
+ Stop when the first error occurs
+
+ ${BLU}--no-clean${RST}
+ Keep the test directories around after the run
+
+ ${BLU}--update${RST}
+ Update the expected outputs for the test cases
+
+ ${BLU}--verbose${RST}=${BLD}commands${RST}
+ Log the commands that get executed
+ ${BLU}--verbose${RST}=${BLD}errors${RST}
+ Don't redirect standard error
+ ${BLU}--verbose${RST}=${BLD}skipped${RST}
+ Log which tests get skipped
+ ${BLU}--verbose${RST}=${BLD}tests${RST}
+ Log all tests that get run
+ ${BLU}--verbose${RST}
+ Log everything
+
+ ${BLU}--help${RST}
+ This message
+
+ ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
+ Choose which test cases to run (default: ${BLU}--all${RST})
+
+ ${BLD}TEST${RST}
+ Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
+EOF
+}
+
+# Parse the command line
+parse_args() {
+ for arg; do
+ case "$arg" in
+ -j?*)
+ JOBS="${arg#-j}"
+ ;;
+ --make=*)
+ MAKE="${arg#*=}"
+ ;;
+ --bfs=*)
+ BFS="${arg#*=}"
+ ;;
+ --posix)
+ PATTERNS+=("posix/*")
+ ;;
+ --bsd)
+ PATTERNS+=("posix/*" "common/*" "bsd/*")
+ ;;
+ --gnu)
+ PATTERNS+=("posix/*" "common/*" "gnu/*")
+ ;;
+ --all)
+ PATTERNS+=("*")
+ ;;
+ --sudo)
+ SUDO=(sudo)
+ ;;
+ --sudo=*)
+ read -a SUDO <<<"${arg#*=}"
+ ;;
+ --stop)
+ STOP=1
+ ;;
+ --no-clean|--noclean)
+ CLEAN=0
+ ;;
+ --update)
+ UPDATE=1
+ ;;
+ --verbose=commands)
+ VERBOSE_COMMANDS=1
+ ;;
+ --verbose=errors)
+ VERBOSE_ERRORS=1
+ ;;
+ --verbose=skipped)
+ VERBOSE_SKIPPED=1
+ ;;
+ --verbose=tests)
+ VERBOSE_TESTS=1
+ ;;
+ --verbose)
+ VERBOSE_COMMANDS=1
+ VERBOSE_ERRORS=1
+ VERBOSE_SKIPPED=1
+ VERBOSE_TESTS=1
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ -*)
+ color printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
+ usage >&2
+ exit 1
+ ;;
+ *)
+ PATTERNS+=("$arg")
+ ;;
+ esac
+ done
+
+ read -a MAKE <<<"$MAKE"
+
+ # Try to resolve the path to $BFS before we cd, while also supporting
+ # --bfs="./bin/bfs -S ids"
+ read -a BFS <<<"${BFS:-$BIN/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/test_L_loops_continue.out b/tests/gnu/L_loops_continue.out
index a514555..a514555 100644
--- a/tests/test_L_loops_continue.out
+++ b/tests/gnu/L_loops_continue.out
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/test_L_xtype_f.out b/tests/gnu/L_xtype_f.out
index 8b95397..8b95397 100644
--- a/tests/test_L_xtype_f.out
+++ b/tests/gnu/L_xtype_f.out
diff --git a/tests/gnu/L_xtype_f.sh b/tests/gnu/L_xtype_f.sh
new file mode 100644
index 0000000..47f7be7
--- /dev/null
+++ b/tests/gnu/L_xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype f
diff --git a/tests/test_L_xtype_l.out b/tests/gnu/L_xtype_l.out
index 973864f..973864f 100644
--- a/tests/test_L_xtype_l.out
+++ b/tests/gnu/L_xtype_l.out
diff --git a/tests/gnu/L_xtype_l.sh b/tests/gnu/L_xtype_l.sh
new file mode 100644
index 0000000..afe52ef
--- /dev/null
+++ b/tests/gnu/L_xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype l
diff --git a/tests/test_a.out b/tests/gnu/and.out
index 722962c..722962c 100644
--- a/tests/test_a.out
+++ b/tests/gnu/and.out
diff --git a/tests/gnu/and.sh b/tests/gnu/and.sh
new file mode 100644
index 0000000..1606455
--- /dev/null
+++ b/tests/gnu/and.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -and -type d
diff --git a/tests/test_comma_redundant_false.out b/tests/gnu/and_false_or_true.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_false.out
+++ b/tests/gnu/and_false_or_true.out
diff --git a/tests/gnu/and_false_or_true.sh b/tests/gnu/and_false_or_true.sh
new file mode 100644
index 0000000..e500722
--- /dev/null
+++ b/tests/gnu/and_false_or_true.sh
@@ -0,0 +1,3 @@
+# Test (-a lhs(always_true) -false) <==> (! lhs),
+# (-o lhs(always_false) -true) <==> (! lhs)
+bfs_diff basic -prune -false -o -true
diff --git a/tests/test_nouser_ulimit.out b/tests/gnu/and_purity.out
index e69de29..e69de29 100644
--- a/tests/test_nouser_ulimit.out
+++ b/tests/gnu/and_purity.out
diff --git a/tests/gnu/and_purity.sh b/tests/gnu/and_purity.sh
new file mode 100644
index 0000000..55e2cfc
--- /dev/null
+++ b/tests/gnu/and_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name nonexistent \( -print , -false \)
diff --git a/tests/test_comma.out b/tests/gnu/comma.out
index 740eefc..740eefc 100644
--- a/tests/test_comma.out
+++ b/tests/gnu/comma.out
diff --git a/tests/gnu/comma.sh b/tests/gnu/comma.sh
new file mode 100644
index 0000000..cdcebf8
--- /dev/null
+++ b/tests/gnu/comma.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*' -print , -print
diff --git a/tests/test_comma_redundant_true.out b/tests/gnu/comma_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_true.out
+++ b/tests/gnu/comma_reachability.out
diff --git a/tests/gnu/comma_reachability.sh b/tests/gnu/comma_reachability.sh
new file mode 100644
index 0000000..60c26bc
--- /dev/null
+++ b/tests/gnu/comma_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -quit , -print
diff --git a/tests/test_fprint_truncate.out b/tests/gnu/comma_redundant_false.out
index 15a13db..15a13db 100644
--- a/tests/test_fprint_truncate.out
+++ b/tests/gnu/comma_redundant_false.out
diff --git a/tests/gnu/comma_redundant_false.sh b/tests/gnu/comma_redundant_false.sh
new file mode 100644
index 0000000..f35d9b8
--- /dev/null
+++ b/tests/gnu/comma_redundant_false.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_false) -false) <==> lhs
+bfs_diff basic -print -not -prune , -false
diff --git a/tests/test_not_reachability.out b/tests/gnu/comma_redundant_true.out
index 15a13db..15a13db 100644
--- a/tests/test_not_reachability.out
+++ b/tests/gnu/comma_redundant_true.out
diff --git a/tests/gnu/comma_redundant_true.sh b/tests/gnu/comma_redundant_true.sh
new file mode 100644
index 0000000..f9eef57
--- /dev/null
+++ b/tests/gnu/comma_redundant_true.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_true) -true) <==> lhs
+bfs_diff basic -prune , -true
diff --git a/tests/test_gid.out b/tests/gnu/daystart.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid.out
+++ b/tests/gnu/daystart.out
diff --git a/tests/gnu/daystart.sh b/tests/gnu/daystart.sh
new file mode 100644
index 0000000..9c3be1a
--- /dev/null
+++ b/tests/gnu/daystart.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -mtime 0
diff --git a/tests/test_gid_minus.out b/tests/gnu/daystart_twice.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid_minus.out
+++ b/tests/gnu/daystart_twice.out
diff --git a/tests/gnu/daystart_twice.sh b/tests/gnu/daystart_twice.sh
new file mode 100644
index 0000000..edbf18d
--- /dev/null
+++ b/tests/gnu/daystart_twice.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -daystart -mtime 0
diff --git a/tests/gnu/exec_flush.out b/tests/gnu/exec_flush.out
new file mode 100644
index 0000000..fdd7b16
--- /dev/null
+++ b/tests/gnu/exec_flush.out
@@ -0,0 +1,19 @@
+basic found
+basic/a found
+basic/b found
+basic/c found
+basic/c/d found
+basic/e found
+basic/e/f found
+basic/g found
+basic/g/h found
+basic/i found
+basic/j found
+basic/j/foo found
+basic/k found
+basic/k/foo found
+basic/k/foo/bar found
+basic/l found
+basic/l/foo found
+basic/l/foo/bar found
+basic/l/foo/bar/baz found
diff --git a/tests/gnu/exec_flush.sh b/tests/gnu/exec_flush.sh
new file mode 100644
index 0000000..ff6088e
--- /dev/null
+++ b/tests/gnu/exec_flush.sh
@@ -0,0 +1,4 @@
+# I/O streams should be flushed before executing programs
+invoke_bfs basic -print0 -exec echo found \; | tr '\0' ' ' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/exec_flush_fail.sh b/tests/gnu/exec_flush_fail.sh
new file mode 100644
index 0000000..5505f7a
--- /dev/null
+++ b/tests/gnu/exec_flush_fail.sh
@@ -0,0 +1,3 @@
+# Failure to flush streams before exec should be caught
+test -e /dev/full || skip
+! invoke_bfs basic -print0 -exec true \; >/dev/full
diff --git a/tests/gnu/exec_nothing.sh b/tests/gnu/exec_nothing.sh
new file mode 100644
index 0000000..443aa0d
--- /dev/null
+++ b/tests/gnu/exec_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -exec \;
diff --git a/tests/test_exec_plus_flush.out b/tests/gnu/exec_plus_flush.out
index 3e276be..3e276be 100644
--- a/tests/test_exec_plus_flush.out
+++ 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/test_execdir.out b/tests/gnu/execdir.out
index 62b31f6..62b31f6 100644
--- a/tests/test_execdir.out
+++ b/tests/gnu/execdir.out
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/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/test_executable.out b/tests/gnu/executable.out
index 49c1b21..49c1b21 100644
--- a/tests/test_executable.out
+++ b/tests/gnu/executable.out
diff --git a/tests/gnu/executable.sh b/tests/gnu/executable.sh
new file mode 100644
index 0000000..f7f6633
--- /dev/null
+++ b/tests/gnu/executable.sh
@@ -0,0 +1 @@
+bfs_diff perms -executable
diff --git a/tests/test_ok_closed_stdin.out b/tests/gnu/false.out
index e69de29..e69de29 100644
--- a/tests/test_ok_closed_stdin.out
+++ b/tests/gnu/false.out
diff --git a/tests/gnu/false.sh b/tests/gnu/false.sh
new file mode 100644
index 0000000..89d86c2
--- /dev/null
+++ b/tests/gnu/false.sh
@@ -0,0 +1 @@
+bfs_diff basic -false
diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh
new file mode 100644
index 0000000..85eee8f
--- /dev/null
+++ b/tests/gnu/files0_from_empty.sh
@@ -0,0 +1 @@
+! printf "\0" | invoke_bfs -files0-from -
diff --git a/tests/gnu/files0_from_error.sh b/tests/gnu/files0_from_error.sh
new file mode 100644
index 0000000..1515d0b
--- /dev/null
+++ b/tests/gnu/files0_from_error.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic
diff --git a/tests/test_files0_from_file.out b/tests/gnu/files0_from_file.out
index 1d87e6b..1d87e6b 100644
--- a/tests/test_files0_from_file.out
+++ b/tests/gnu/files0_from_file.out
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/test_okdir_closed_stdin.out b/tests/gnu/files0_from_none.out
index e69de29..e69de29 100644
--- a/tests/test_okdir_closed_stdin.out
+++ b/tests/gnu/files0_from_none.out
diff --git a/tests/gnu/files0_from_none.sh b/tests/gnu/files0_from_none.sh
new file mode 100644
index 0000000..1633163
--- /dev/null
+++ b/tests/gnu/files0_from_none.sh
@@ -0,0 +1 @@
+printf "" | bfs_diff -files0-from -
diff --git a/tests/gnu/files0_from_nothing.sh b/tests/gnu/files0_from_nothing.sh
new file mode 100644
index 0000000..fee50a8
--- /dev/null
+++ b/tests/gnu/files0_from_nothing.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic/nonexistent
diff --git a/tests/gnu/files0_from_nowhere.sh b/tests/gnu/files0_from_nowhere.sh
new file mode 100644
index 0000000..68eea4b
--- /dev/null
+++ b/tests/gnu/files0_from_nowhere.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from
diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh
new file mode 100644
index 0000000..8e145ce
--- /dev/null
+++ b/tests/gnu/files0_from_ok.sh
@@ -0,0 +1 @@
+! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \;
diff --git a/tests/test_files0_from_stdin.out b/tests/gnu/files0_from_stdin.out
index 1d87e6b..1d87e6b 100644
--- a/tests/test_files0_from_stdin.out
+++ b/tests/gnu/files0_from_stdin.out
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/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/test_follow_comma.out b/tests/gnu/follow_comma.out
index 920b3d3..920b3d3 100644
--- a/tests/test_follow_comma.out
+++ b/tests/gnu/follow_comma.out
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/test_gid_minus_plus.out b/tests/gnu/fprint.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid_minus_plus.out
+++ b/tests/gnu/fprint.out
diff --git a/tests/gnu/fprint.sh b/tests/gnu/fprint.sh
new file mode 100644
index 0000000..04b50fa
--- /dev/null
+++ b/tests/gnu/fprint.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprint0.out b/tests/gnu/fprint0.out
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_noerror.sh b/tests/gnu/fprint_noerror.sh
new file mode 100644
index 0000000..f13a62b
--- /dev/null
+++ b/tests/gnu/fprint_noerror.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/gnu/fprint_nonexistent.sh b/tests/gnu/fprint_nonexistent.sh
new file mode 100644
index 0000000..2a403a2
--- /dev/null
+++ b/tests/gnu/fprint_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint nonexistent/path
diff --git a/tests/test_printf_leak.out b/tests/gnu/fprint_truncate.out
index 15a13db..15a13db 100644
--- a/tests/test_printf_leak.out
+++ b/tests/gnu/fprint_truncate.out
diff --git a/tests/gnu/fprint_truncate.sh b/tests/gnu/fprint_truncate.sh
new file mode 100644
index 0000000..db58a7a
--- /dev/null
+++ b/tests/gnu/fprint_truncate.sh
@@ -0,0 +1,5 @@
+printf "basic\nbasic\n" >"$OUT"
+
+invoke_bfs basic -maxdepth 0 -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprintf.out b/tests/gnu/fprintf.out
index 77ce17a..77ce17a 100644
--- a/tests/test_fprintf.out
+++ b/tests/gnu/fprintf.out
diff --git a/tests/gnu/fprintf.sh b/tests/gnu/fprintf.sh
new file mode 100644
index 0000000..9c6355a
--- /dev/null
+++ b/tests/gnu/fprintf.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
+sort_output
+diff_output
diff --git a/tests/gnu/fprintf_nofile.sh b/tests/gnu/fprintf_nofile.sh
new file mode 100644
index 0000000..4e79002
--- /dev/null
+++ b/tests/gnu/fprintf_nofile.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf
diff --git a/tests/gnu/fprintf_noformat.sh b/tests/gnu/fprintf_noformat.sh
new file mode 100644
index 0000000..fd97f4c
--- /dev/null
+++ b/tests/gnu/fprintf_noformat.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf /dev/null
diff --git a/tests/gnu/fprintf_nonexistent.sh b/tests/gnu/fprintf_nonexistent.sh
new file mode 100644
index 0000000..b1eea10
--- /dev/null
+++ b/tests/gnu/fprintf_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf nonexistent/path '%p\n'
diff --git a/tests/test_gid_name.out b/tests/gnu/fstype.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid_name.out
+++ b/tests/gnu/fstype.out
diff --git a/tests/gnu/fstype.sh b/tests/gnu/fstype.sh
new file mode 100644
index 0000000..05645c3
--- /dev/null
+++ b/tests/gnu/fstype.sh
@@ -0,0 +1,2 @@
+fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') || skip
+bfs_diff basic -fstype "$fstype"
diff --git a/tests/gnu/fstype_stacked.out b/tests/gnu/fstype_stacked.out
new file mode 100644
index 0000000..c1e0e6c
--- /dev/null
+++ b/tests/gnu/fstype_stacked.out
@@ -0,0 +1 @@
+mnt
diff --git a/tests/gnu/fstype_stacked.sh b/tests/gnu/fstype_stacked.sh
new file mode 100644
index 0000000..a9739bb
--- /dev/null
+++ b/tests/gnu/fstype_stacked.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_sudo mount -t ramfs ramfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff mnt -fstype ramfs -print -o -printf '%p: %F\n'
diff --git a/tests/test_or_purity.out b/tests/gnu/fstype_umount.out
index e69de29..e69de29 100644
--- a/tests/test_or_purity.out
+++ b/tests/gnu/fstype_umount.out
diff --git a/tests/gnu/fstype_umount.sh b/tests/gnu/fstype_umount.sh
new file mode 100644
index 0000000..81c195f
--- /dev/null
+++ b/tests/gnu/fstype_umount.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+
+mkdir tmp
+bfs_sudo mount -t tmpfs tmpfs tmp || skip
+defer bfs_sudo umount -R tmp
+
+mkdir tmp/ram
+bfs_sudo mount -t ramfs ramfs tmp/ram || skip
+
+bfs_diff tmp -path tmp -exec "${SUDO[@]}" umount tmp/ram \; , -fstype ramfs -print
diff --git a/tests/test_gid_plus.out b/tests/gnu/gid.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid_plus.out
+++ b/tests/gnu/gid.out
diff --git a/tests/gnu/gid.sh b/tests/gnu/gid.sh
new file mode 100644
index 0000000..2707b4a
--- /dev/null
+++ b/tests/gnu/gid.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -g)"
diff --git a/tests/test_gid_plus_plus.out b/tests/gnu/gid_minus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_gid_plus_plus.out
+++ b/tests/gnu/gid_minus.out
diff --git a/tests/gnu/gid_minus.sh b/tests/gnu/gid_minus.sh
new file mode 100644
index 0000000..e3822f0
--- /dev/null
+++ b/tests/gnu/gid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-$(($(id -g) + 1))"
diff --git a/tests/test_group_id.out b/tests/gnu/gid_minus_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_group_id.out
+++ b/tests/gnu/gid_minus_plus.out
diff --git a/tests/gnu/gid_minus_plus.sh b/tests/gnu/gid_minus_plus.sh
new file mode 100644
index 0000000..4ff0877
--- /dev/null
+++ b/tests/gnu/gid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-+$(($(id -g) + 1))"
diff --git a/tests/test_group_name.out b/tests/gnu/gid_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_group_name.out
+++ b/tests/gnu/gid_plus.out
diff --git a/tests/gnu/gid_plus.sh b/tests/gnu/gid_plus.sh
new file mode 100644
index 0000000..ccba0e6
--- /dev/null
+++ b/tests/gnu/gid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid +0
diff --git a/tests/test_group_nogroup.out b/tests/gnu/gid_plus_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_group_nogroup.out
+++ b/tests/gnu/gid_plus_plus.out
diff --git a/tests/gnu/gid_plus_plus.sh b/tests/gnu/gid_plus_plus.sh
new file mode 100644
index 0000000..ec7ae86
--- /dev/null
+++ b/tests/gnu/gid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid ++0
diff --git a/tests/gnu/ignore_readdir_race.sh b/tests/gnu/ignore_readdir_race.sh
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_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_root.sh b/tests/gnu/ignore_readdir_race_root.sh
new file mode 100644
index 0000000..dc41e7f
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_root.sh
@@ -0,0 +1,2 @@
+# Make sure -ignore_readdir_race doesn't suppress ENOENT at the root
+! invoke_bfs basic/nonexistent -ignore_readdir_race
diff --git a/tests/gnu/inum_automount.out b/tests/gnu/inum_automount.out
new file mode 100644
index 0000000..3378e2d
--- /dev/null
+++ b/tests/gnu/inum_automount.out
@@ -0,0 +1 @@
+./automnt
diff --git a/tests/gnu/inum_automount.sh b/tests/gnu/inum_automount.sh
new file mode 100644
index 0000000..86b23e1
--- /dev/null
+++ b/tests/gnu/inum_automount.sh
@@ -0,0 +1,14 @@
+# bfs shouldn't trigger automounts unless it descends into them
+
+command -v systemd-mount &>/dev/null || skip
+
+cd "$TEST"
+mkdir foo automnt
+
+bfs_sudo systemd-mount -A -o bind "$TMP/basic" automnt || skip
+defer bfs_sudo systemd-umount automnt
+
+before=$(inum automnt)
+bfs_diff . -inum "$before" -prune
+after=$(inum automnt)
+((before == after))
diff --git a/tests/test_iwholename.out b/tests/gnu/iwholename.out
index ae1ae21..ae1ae21 100644
--- a/tests/test_iwholename.out
+++ b/tests/gnu/iwholename.out
diff --git a/tests/gnu/iwholename.sh b/tests/gnu/iwholename.sh
new file mode 100644
index 0000000..0b2d038
--- /dev/null
+++ b/tests/gnu/iwholename.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -iwholename PATTERN || skip
+bfs_diff basic -iwholename 'basic/*F*'
diff --git a/tests/test_newer_link.out b/tests/gnu/newer_link.out
index d2dcdd1..d2dcdd1 100644
--- a/tests/test_newer_link.out
+++ b/tests/gnu/newer_link.out
diff --git a/tests/gnu/newer_link.sh b/tests/gnu/newer_link.sh
new file mode 100644
index 0000000..685ac78
--- /dev/null
+++ b/tests/gnu/newer_link.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/l
diff --git a/tests/test_path_d.out b/tests/gnu/noleaf.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_path_d.out
+++ b/tests/gnu/noleaf.out
diff --git a/tests/gnu/noleaf.sh b/tests/gnu/noleaf.sh
new file mode 100644
index 0000000..b19438c
--- /dev/null
+++ b/tests/gnu/noleaf.sh
@@ -0,0 +1 @@
+bfs_diff basic -noleaf
diff --git a/tests/test_bang.out b/tests/gnu/not.out
index b286454..b286454 100644
--- a/tests/test_bang.out
+++ b/tests/gnu/not.out
diff --git a/tests/gnu/not.sh b/tests/gnu/not.sh
new file mode 100644
index 0000000..9fa9edc
--- /dev/null
+++ b/tests/gnu/not.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo
diff --git a/tests/test_quit_after_print.out b/tests/gnu/not_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_after_print.out
+++ b/tests/gnu/not_reachability.out
diff --git a/tests/gnu/not_reachability.sh b/tests/gnu/not_reachability.sh
new file mode 100644
index 0000000..7fd3c74
--- /dev/null
+++ b/tests/gnu/not_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print \! -quit -print
diff --git a/tests/gnu/ok_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..87c7298
--- /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 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/ok_nothing.sh b/tests/gnu/ok_nothing.sh
new file mode 100644
index 0000000..52c3547
--- /dev/null
+++ b/tests/gnu/ok_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -ok \;
diff --git a/tests/gnu/okdir_path_dot.sh b/tests/gnu/okdir_path_dot.sh
new file mode 100644
index 0000000..5b40e27
--- /dev/null
+++ b/tests/gnu/okdir_path_dot.sh
@@ -0,0 +1 @@
+! PATH=".:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_empty.sh b/tests/gnu/okdir_path_empty.sh
new file mode 100644
index 0000000..2669ee8
--- /dev/null
+++ b/tests/gnu/okdir_path_empty.sh
@@ -0,0 +1 @@
+! PATH=":$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_relative.sh b/tests/gnu/okdir_path_relative.sh
new file mode 100644
index 0000000..05100a1
--- /dev/null
+++ b/tests/gnu/okdir_path_relative.sh
@@ -0,0 +1 @@
+! PATH="foo:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/test_o.out b/tests/gnu/or.out
index 1650c4d..1650c4d 100644
--- a/tests/test_o.out
+++ b/tests/gnu/or.out
diff --git a/tests/gnu/or.sh b/tests/gnu/or.sh
new file mode 100644
index 0000000..eb28030
--- /dev/null
+++ b/tests/gnu/or.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -or -type d
diff --git a/tests/test_stderr_fails_silently.out b/tests/gnu/path_d.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_stderr_fails_silently.out
+++ b/tests/gnu/path_d.out
diff --git a/tests/gnu/path_d.sh b/tests/gnu/path_d.sh
new file mode 100644
index 0000000..e546ff3
--- /dev/null
+++ b/tests/gnu/path_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -d
diff --git a/tests/test_perm_000_plus.out b/tests/gnu/perm_000_slash.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_plus.out
+++ b/tests/gnu/perm_000_slash.out
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/test_perm_222_slash.out b/tests/gnu/perm_222_slash.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_perm_222_slash.out
+++ b/tests/gnu/perm_222_slash.out
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/test_perm_644_slash.out b/tests/gnu/perm_644_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_644_slash.out
+++ b/tests/gnu/perm_644_slash.out
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/test_perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_leading_plus_symbolic_slash.out
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.out
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/test_perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_symbolic_slash.out
+++ b/tests/gnu/perm_symbolic_slash.out
diff --git a/tests/gnu/perm_symbolic_slash.sh b/tests/gnu/perm_symbolic_slash.sh
new file mode 100644
index 0000000..253b14e
--- /dev/null
+++ b/tests/gnu/perm_symbolic_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /a+r,u=wX,g+wX-w
diff --git a/tests/test_precedence.out b/tests/gnu/precedence.out
index 7f589f2..7f589f2 100644
--- a/tests/test_precedence.out
+++ b/tests/gnu/precedence.out
diff --git a/tests/gnu/precedence.sh b/tests/gnu/precedence.sh
new file mode 100644
index 0000000..b35d160
--- /dev/null
+++ b/tests/gnu/precedence.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print
diff --git a/tests/gnu/print0.out b/tests/gnu/print0.out
new file mode 100644
index 0000000..1347444
--- /dev/null
+++ b/tests/gnu/print0.out
Binary files differ
diff --git a/tests/gnu/print0.sh b/tests/gnu/print0.sh
new file mode 100644
index 0000000..b916172
--- /dev/null
+++ b/tests/gnu/print0.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a basic/b -print0 >"$OUT"
+diff_output
diff --git a/tests/gnu/print_error.sh b/tests/gnu/print_error.sh
new file mode 100644
index 0000000..bc79637
--- /dev/null
+++ b/tests/gnu/print_error.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 >/dev/full
diff --git a/tests/test_printf.out b/tests/gnu/printf.out
index 77ce17a..77ce17a 100644
--- a/tests/test_printf.out
+++ b/tests/gnu/printf.out
diff --git a/tests/gnu/printf.sh b/tests/gnu/printf.sh
new file mode 100644
index 0000000..4dd48e8
--- /dev/null
+++ b/tests/gnu/printf.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
diff --git a/tests/test_printf_H.out b/tests/gnu/printf_H.out
index 6b605ff..6b605ff 100644
--- a/tests/test_printf_H.out
+++ b/tests/gnu/printf_H.out
diff --git a/tests/gnu/printf_H.sh b/tests/gnu/printf_H.sh
new file mode 100644
index 0000000..ddef7e2
--- /dev/null
+++ b/tests/gnu/printf_H.sh
@@ -0,0 +1 @@
+bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n'
diff --git a/tests/gnu/printf_Y_error.out b/tests/gnu/printf_Y_error.out
new file mode 100644
index 0000000..1dd554e
--- /dev/null
+++ b/tests/gnu/printf_Y_error.out
@@ -0,0 +1,3 @@
+(.) () d d
+(./bar) (foo/bar) l ?
+(./foo) () d d
diff --git a/tests/gnu/printf_Y_error.sh b/tests/gnu/printf_Y_error.sh
new file mode 100644
index 0000000..d3130ce
--- /dev/null
+++ b/tests/gnu/printf_Y_error.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+mkdir foo
+ln -s foo/bar bar
+
+chmod -x foo
+defer chmod +x foo
+
+! bfs_diff . -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/test_perm_leading_plus_symbolic.out b/tests/gnu/printf_empty.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic.out
+++ b/tests/gnu/printf_empty.out
diff --git a/tests/gnu/printf_empty.sh b/tests/gnu/printf_empty.sh
new file mode 100644
index 0000000..ed5eb04
--- /dev/null
+++ b/tests/gnu/printf_empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf ''
diff --git a/tests/test_printf_escapes.out b/tests/gnu/printf_escapes.out
index 20ea120..20ea120 100644
--- a/tests/test_printf_escapes.out
+++ b/tests/gnu/printf_escapes.out
diff --git a/tests/gnu/printf_escapes.sh b/tests/gnu/printf_escapes.sh
new file mode 100644
index 0000000..ece7c0e
--- /dev/null
+++ b/tests/gnu/printf_escapes.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo'
diff --git a/tests/test_printf_flags.out b/tests/gnu/printf_flags.out
index c2c1f0a..c2c1f0a 100644
--- a/tests/test_printf_flags.out
+++ b/tests/gnu/printf_flags.out
diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh
new file mode 100644
index 0000000..2ef37ad
--- /dev/null
+++ b/tests/gnu/printf_flags.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n'
diff --git a/tests/test_printf_l_nonlink.out b/tests/gnu/printf_l_nonlink.out
index 30df155..30df155 100644
--- a/tests/test_printf_l_nonlink.out
+++ b/tests/gnu/printf_l_nonlink.out
diff --git a/tests/gnu/printf_l_nonlink.sh b/tests/gnu/printf_l_nonlink.sh
new file mode 100644
index 0000000..1c66442
--- /dev/null
+++ b/tests/gnu/printf_l_nonlink.sh
@@ -0,0 +1 @@
+bfs_diff links -printf '| %26p -> %-26l |\n'
diff --git a/tests/test_quit_implicit_print.out b/tests/gnu/printf_leak.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_implicit_print.out
+++ b/tests/gnu/printf_leak.out
diff --git a/tests/gnu/printf_leak.sh b/tests/gnu/printf_leak.sh
new file mode 100644
index 0000000..c4092c7
--- /dev/null
+++ b/tests/gnu/printf_leak.sh
@@ -0,0 +1,2 @@
+# Memory leak regression test
+bfs_diff basic -maxdepth 0 -printf '%p'
diff --git a/tests/gnu/printf_nul.out b/tests/gnu/printf_nul.out
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 017ac0d..017ac0d 100644
--- a/tests/test_printf_trailing_slash.out
+++ b/tests/gnu/printf_trailing_slash.out
diff --git a/tests/gnu/printf_trailing_slash.sh b/tests/gnu/printf_trailing_slash.sh
new file mode 100644
index 0000000..2df818d
--- /dev/null
+++ b/tests/gnu/printf_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_trailing_slashes.out b/tests/gnu/printf_trailing_slashes.out
index fd27101..fd27101 100644
--- a/tests/test_printf_trailing_slashes.out
+++ b/tests/gnu/printf_trailing_slashes.out
diff --git a/tests/gnu/printf_trailing_slashes.sh b/tests/gnu/printf_trailing_slashes.sh
new file mode 100644
index 0000000..6dc532c
--- /dev/null
+++ b/tests/gnu/printf_trailing_slashes.sh
@@ -0,0 +1 @@
+bfs_diff basic/// -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_types.out b/tests/gnu/printf_types.out
index 8144c7c..8144c7c 100644
--- a/tests/test_printf_types.out
+++ b/tests/gnu/printf_types.out
diff --git a/tests/gnu/printf_types.sh b/tests/gnu/printf_types.sh
new file mode 100644
index 0000000..6ed1d75
--- /dev/null
+++ b/tests/gnu/printf_types.sh
@@ -0,0 +1 @@
+bfs_diff loops -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh
new file mode 100644
index 0000000..c621b9b
--- /dev/null
+++ b/tests/gnu/printf_u_g_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+[ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
diff --git a/tests/test_readable.out b/tests/gnu/readable.out
index 386feba..386feba 100644
--- a/tests/test_readable.out
+++ b/tests/gnu/readable.out
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/test_regextype_ed.out b/tests/gnu/regextype_ed.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_ed.out
+++ b/tests/gnu/regextype_ed.out
diff --git a/tests/gnu/regextype_ed.sh b/tests/gnu/regextype_ed.sh
new file mode 100644
index 0000000..0e92db3
--- /dev/null
+++ b/tests/gnu/regextype_ed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype ed -regex '\./\((\)'
diff --git a/tests/test_regextype_emacs.out b/tests/gnu/regextype_emacs.out
index 95942b4..95942b4 100644
--- a/tests/test_regextype_emacs.out
+++ b/tests/gnu/regextype_emacs.out
diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh
new file mode 100644
index 0000000..3cc388c
--- /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/test_name.out b/tests/gnu/regextype_grep.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_name.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/test_regextype_posix_basic.out b/tests/gnu/regextype_posix_basic.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_basic.out
+++ b/tests/gnu/regextype_posix_basic.out
diff --git a/tests/gnu/regextype_posix_basic.sh b/tests/gnu/regextype_posix_basic.sh
new file mode 100644
index 0000000..fa2254c
--- /dev/null
+++ b/tests/gnu/regextype_posix_basic.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-basic -regex '\./\((\)'
diff --git a/tests/test_regextype_posix_extended.out b/tests/gnu/regextype_posix_extended.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_extended.out
+++ b/tests/gnu/regextype_posix_extended.out
diff --git a/tests/gnu/regextype_posix_extended.sh b/tests/gnu/regextype_posix_extended.sh
new file mode 100644
index 0000000..f82ed65
--- /dev/null
+++ b/tests/gnu/regextype_posix_extended.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-extended -regex '\./(\()'
diff --git a/tests/test_regextype_sed.out b/tests/gnu/regextype_sed.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_sed.out
+++ b/tests/gnu/regextype_sed.out
diff --git a/tests/gnu/regextype_sed.sh b/tests/gnu/regextype_sed.sh
new file mode 100644
index 0000000..9ce6f4e
--- /dev/null
+++ b/tests/gnu/regextype_sed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype sed -regex '\./\((\)'
diff --git a/tests/test_true.out b/tests/gnu/true.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_true.out
+++ b/tests/gnu/true.out
diff --git a/tests/gnu/true.sh b/tests/gnu/true.sh
new file mode 100644
index 0000000..65f3254
--- /dev/null
+++ b/tests/gnu/true.sh
@@ -0,0 +1 @@
+bfs_diff basic -true
diff --git a/tests/test_uid.out b/tests/gnu/uid.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid.out
+++ b/tests/gnu/uid.out
diff --git a/tests/gnu/uid.sh b/tests/gnu/uid.sh
new file mode 100644
index 0000000..fb3cd93
--- /dev/null
+++ b/tests/gnu/uid.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -u)"
diff --git a/tests/test_uid_minus.out b/tests/gnu/uid_minus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid_minus.out
+++ b/tests/gnu/uid_minus.out
diff --git a/tests/gnu/uid_minus.sh b/tests/gnu/uid_minus.sh
new file mode 100644
index 0000000..6d371f2
--- /dev/null
+++ b/tests/gnu/uid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-$(($(id -u) + 1))"
diff --git a/tests/test_uid_minus_plus.out b/tests/gnu/uid_minus_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid_minus_plus.out
+++ b/tests/gnu/uid_minus_plus.out
diff --git a/tests/gnu/uid_minus_plus.sh b/tests/gnu/uid_minus_plus.sh
new file mode 100644
index 0000000..e7a0496
--- /dev/null
+++ b/tests/gnu/uid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-+$(($(id -u) + 1))"
diff --git a/tests/test_uid_name.out b/tests/gnu/uid_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid_name.out
+++ b/tests/gnu/uid_plus.out
diff --git a/tests/gnu/uid_plus.sh b/tests/gnu/uid_plus.sh
new file mode 100644
index 0000000..22b2c8e
--- /dev/null
+++ b/tests/gnu/uid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid +0
diff --git a/tests/test_uid_plus.out b/tests/gnu/uid_plus_plus.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid_plus.out
+++ b/tests/gnu/uid_plus_plus.out
diff --git a/tests/gnu/uid_plus_plus.sh b/tests/gnu/uid_plus_plus.sh
new file mode 100644
index 0000000..e021888
--- /dev/null
+++ b/tests/gnu/uid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid ++0
diff --git a/tests/gnu/used.out b/tests/gnu/used.out
new file mode 100644
index 0000000..647621b
--- /dev/null
+++ b/tests/gnu/used.out
@@ -0,0 +1,4 @@
+-used +7: ./nextyear
+-used 1: ./tomorrow
+-used 2: ./dayafter
+-used 7: ./nextweek
diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh
new file mode 100644
index 0000000..5e5d4e9
--- /dev/null
+++ b/tests/gnu/used.sh
@@ -0,0 +1,40 @@
+iso8601() {
+ printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@"
+}
+
+cd "$TEST"
+
+now=$(date '+%Y-%m-%dT%H:%M:%S')
+
+# Parse the current date
+[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail
+# Treat leading zeros as decimal, not octal
+YYYY=$((10#${BASH_REMATCH[1]}))
+MM=$((10#${BASH_REMATCH[2]}))
+DD=$((10#${BASH_REMATCH[3]}))
+hh=$((10#${BASH_REMATCH[4]}))
+mm=$((10#${BASH_REMATCH[5]}))
+ss=$((10#${BASH_REMATCH[6]}))
+
+# -used is always false if atime < ctime
+yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss)
+"$XTOUCH" -at "$yesterday" yesterday
+
+# -used rounds up
+tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$tomorrow" tomorrow
+
+dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$dayafter" dayafter
+
+nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$nextweek" nextweek
+
+nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss)
+"$XTOUCH" -at "$nextyear" nextyear
+
+bfs_diff -mindepth 1 \
+ -a -used 1 -printf '-used 1: %p\n' \
+ -o -used 2 -printf '-used 2: %p\n' \
+ -o -used 7 -printf '-used 7: %p\n' \
+ -o -used +7 -printf '-used +7: %p\n'
diff --git a/tests/test_path.out b/tests/gnu/wholename.out
index ae1ae21..ae1ae21 100644
--- a/tests/test_path.out
+++ b/tests/gnu/wholename.out
diff --git a/tests/gnu/wholename.sh b/tests/gnu/wholename.sh
new file mode 100644
index 0000000..4c641b8
--- /dev/null
+++ b/tests/gnu/wholename.sh
@@ -0,0 +1 @@
+bfs_diff basic -wholename 'basic/*f*'
diff --git a/tests/test_writable.out b/tests/gnu/writable.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_writable.out
+++ b/tests/gnu/writable.out
diff --git a/tests/gnu/writable.sh b/tests/gnu/writable.sh
new file mode 100644
index 0000000..93c666f
--- /dev/null
+++ b/tests/gnu/writable.sh
@@ -0,0 +1 @@
+bfs_diff perms -writable
diff --git a/tests/gnu/xtype_bind_mount.out b/tests/gnu/xtype_bind_mount.out
new file mode 100644
index 0000000..d18d706
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.out
@@ -0,0 +1,2 @@
+./link
+./null
diff --git a/tests/gnu/xtype_bind_mount.sh b/tests/gnu/xtype_bind_mount.sh
new file mode 100644
index 0000000..35fb3f5
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+ln -s /dev/null link
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -xtype c
diff --git a/tests/test_xtype_f.out b/tests/gnu/xtype_f.out
index e6ba322..e6ba322 100644
--- a/tests/test_xtype_f.out
+++ b/tests/gnu/xtype_f.out
diff --git a/tests/gnu/xtype_f.sh b/tests/gnu/xtype_f.sh
new file mode 100644
index 0000000..0ea27bb
--- /dev/null
+++ b/tests/gnu/xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f
diff --git a/tests/test_xtype_l.out b/tests/gnu/xtype_l.out
index f29c978..f29c978 100644
--- a/tests/test_xtype_l.out
+++ b/tests/gnu/xtype_l.out
diff --git a/tests/gnu/xtype_l.sh b/tests/gnu/xtype_l.sh
new file mode 100644
index 0000000..39c8ea4
--- /dev/null
+++ b/tests/gnu/xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype l
diff --git a/tests/ioq.c b/tests/ioq.c
new file mode 100644
index 0000000..ef5ee3b
--- /dev/null
+++ b/tests/ioq.c
@@ -0,0 +1,77 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "ioq.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "dir.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+/**
+ * Test for blocking within ioq_slot_push().
+ *
+ * struct ioqq only supports non-blocking reads; if a write encounters a full
+ * slot, it must block until someone pops from that slot:
+ *
+ * Reader Writer
+ * ────────────────────────── ─────────────────────────
+ * tail: 0 → 1
+ * slots[0]: empty → full
+ * tail: 1 → 0
+ * slots[1]: empty → full
+ * tail: 0 → 1
+ * slots[0]: full → full* (IOQ_BLOCKED)
+ * ioq_slot_wait() ...
+ * head: 0 → 1
+ * slots[0]: full* → empty
+ * ioq_slot_wake()
+ * ...
+ * slots[0]: empty → full
+ *
+ * To reproduce this unlikely scenario, we must fill up the ready queue, then
+ * call ioq_cancel() which pushes an additional sentinel IOQ_STOP operation.
+ */
+static void check_ioq_push_block(void) {
+ // Must be a power of two to fill the entire queue
+ const size_t depth = 2;
+
+ struct ioq *ioq = ioq_create(depth, 1);
+ bfs_verify(ioq, "ioq_create(): %s", xstrerror(errno));
+
+ // Push enough operations to fill the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct bfs_dir *dir = bfs_allocdir();
+ bfs_verify(dir, "bfs_allocdir(): %s", xstrerror(errno));
+
+ int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL);
+ bfs_verify(ret == 0, "ioq_opendir(): %s", xstrerror(errno));
+ }
+ bfs_verify(ioq_capacity(ioq) == 0);
+
+ // Now cancel the queue, pushing an additional IOQ_STOP message
+ ioq_cancel(ioq);
+
+ // Drain the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct ioq_ent *ent = ioq_pop(ioq, true);
+ bfs_verify(ent && ent->op == IOQ_OPENDIR);
+
+ if (ent->result >= 0) {
+ bfs_closedir(ent->opendir.dir);
+ }
+ free(ent->opendir.dir);
+ ioq_free(ioq, ent);
+ }
+ bfs_verify(!ioq_pop(ioq, true));
+
+ ioq_destroy(ioq);
+}
+
+bool check_ioq(void) {
+ check_ioq_push_block();
+ return true;
+}
diff --git a/tests/ls-color.sh b/tests/ls-color.sh
index c82a58d..b9a0402 100755
--- a/tests/ls-color.sh
+++ b/tests/ls-color.sh
@@ -1,36 +1,50 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
# Prints the "ground truth" coloring of a path using ls
set -e
+parse_ls_colors() {
+ for key; do
+ local -n var="$key"
+ if [[ "$LS_COLORS" =~ (^|:)$key=(([^:]|\\:)*) ]]; then
+ var="${BASH_REMATCH[2]}"
+ # Interpret escapes
+ var=$(printf "$var" | sed $'s/\^\[/\033/g; s/\\\\:/:/g')
+ fi
+ done
+}
+
+re_escape() {
+ # https://stackoverflow.com/a/29613573/502399
+ sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"
+}
+
+rs=0
+lc=$'\033['
+rc=m
+ec=
+no=
+
+parse_ls_colors rs lc rc ec no
+: "${ec:=$lc$rs$rc}"
+
+strip="(($(re_escape "$lc$no$rc"))?($(re_escape "$ec")|$(re_escape "$lc$rc")))+"
+
+ls_color() {
+ # Strip the leading reset sequence from the ls output
+ ls -1d --color "$@" | sed -E "s/^$strip([a-z].*)$strip/\4/; s/^$strip//"
+}
+
L=
if [ "$1" = "-L" ]; then
L="$1"
shift
fi
-function ls_color() {
- # Strip the leading reset sequence from the ls output
- ls -1d --color "$@" | sed $'s/^\033\\[0m//'
-}
-
DIR="${1%/*}"
if [ "$DIR" = "$1" ]; then
ls_color "$1"
diff --git a/tests/main.c b/tests/main.c
new file mode 100644
index 0000000..429772b
--- /dev/null
+++ b/tests/main.c
@@ -0,0 +1,125 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Entry point for unit tests.
+ */
+
+#include "prelude.h"
+#include "tests.h"
+#include "bfstd.h"
+#include "color.h"
+#include <errno.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/**
+ * Test context.
+ */
+struct test_ctx {
+ /** Number of command line arguments. */
+ int argc;
+ /** The arguments themselves. */
+ char **argv;
+
+ /** Parsed colors. */
+ struct colors *colors;
+ /** Colorized output stream. */
+ CFILE *cout;
+
+ /** Eventual exit status. */
+ int ret;
+};
+
+/** Initialize the test context. */
+static int test_init(struct test_ctx *ctx, int argc, char **argv) {
+ ctx->argc = argc;
+ ctx->argv = argv;
+
+ ctx->colors = parse_colors();
+ ctx->cout = cfwrap(stdout, ctx->colors, false);
+ if (!ctx->cout) {
+ ctx->ret = EXIT_FAILURE;
+ return -1;
+ }
+
+ ctx->ret = EXIT_SUCCESS;
+ return 0;
+}
+
+/** Finalize the test context. */
+static int test_fini(struct test_ctx *ctx) {
+ if (ctx->cout) {
+ cfclose(ctx->cout);
+ }
+
+ free_colors(ctx->colors);
+
+ return ctx->ret;
+}
+
+/** Check if a test case is enabled for this run. */
+static bool should_run(const struct test_ctx *ctx, const char *test) {
+ // Run all tests by default
+ if (ctx->argc < 2) {
+ return true;
+ }
+
+ // With args, run only specified tests
+ for (int i = 1; i < ctx->argc; ++i) {
+ if (strcmp(test, ctx->argv[i]) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** Run a test if it's enabled. */
+static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
+ if (should_run(ctx, test)) {
+ if (fn()) {
+ cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test);
+ } else {
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test);
+ ctx->ret = EXIT_FAILURE;
+ }
+ }
+}
+
+const char *bfs_errstr(void) {
+ return xstrerror(errno);
+}
+
+int main(int argc, char *argv[]) {
+ // Try to set a UTF-8 locale
+ if (!setlocale(LC_ALL, "C.UTF-8")) {
+ setlocale(LC_ALL, "");
+ }
+
+ // Run tests in UTC
+ if (setenv("TZ", "UTC0", true) != 0) {
+ perror("setenv()");
+ return EXIT_FAILURE;
+ }
+ tzset();
+
+ struct test_ctx ctx;
+ if (test_init(&ctx, argc, argv) != 0) {
+ goto done;
+ }
+
+ run_test(&ctx, "alloc", check_alloc);
+ run_test(&ctx, "bfstd", check_bfstd);
+ run_test(&ctx, "bit", check_bit);
+ run_test(&ctx, "ioq", check_ioq);
+ run_test(&ctx, "trie", check_trie);
+ run_test(&ctx, "xspawn", check_xspawn);
+ run_test(&ctx, "xtime", check_xtime);
+
+done:
+ return test_fini(&ctx);
+}
diff --git a/tests/mksock.c b/tests/mksock.c
index d1776b3..5786cb6 100644
--- a/tests/mksock.c
+++ b/tests/mksock.c
@@ -1,29 +1,16 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* There's no standard Unix utility that creates a socket file, so this small
* program does the job.
*/
+#include "bfstd.h"
#include <errno.h>
-#include <libgen.h>
#include <stdio.h>
-#include <string.h>
#include <stdlib.h>
+#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -32,7 +19,7 @@
* Print an error message.
*/
static void errmsg(const char *cmd, const char *path) {
- fprintf(stderr, "%s: '%s': %s.\n", cmd, path, strerror(errno));
+ fprintf(stderr, "%s: '%s': %s.\n", cmd, path, xstrerror(errno));
}
/**
@@ -41,18 +28,13 @@ static void errmsg(const char *cmd, const char *path) {
* file name is not.
*/
static int chdir_parent(const char *path) {
- char *copy = strdup(path);
- if (!copy) {
+ char *dir = xdirname(path);
+ if (!dir) {
return -1;
}
- const char *dir = dirname(copy);
int ret = chdir(dir);
-
- int error = errno;
- free(copy);
- errno = error;
-
+ free(dir);
return ret;
}
@@ -66,22 +48,21 @@ static int init_sun(struct sockaddr_un *sock, const char *path) {
return -1;
}
- char *copy = strdup(path);
- if (!copy) {
+ char *base = xbasename(path);
+ if (!base) {
return -1;
}
- const char *base = basename(copy);
len = strlen(base);
if (len >= sizeof(sock->sun_path)) {
- free(copy);
+ free(base);
errno = ENAMETOOLONG;
return -1;
}
sock->sun_family = AF_UNIX;
memcpy(sock->sun_path, base, len + 1);
- free(copy);
+ free(base);
return 0;
}
@@ -119,7 +100,7 @@ int main(int argc, char *argv[]) {
ret = EXIT_FAILURE;
}
- if (close(fd) != 0) {
+ if (xclose(fd) != 0) {
errmsg(cmd, path);
ret = EXIT_FAILURE;
}
diff --git a/tests/test_P.out b/tests/posix/H.out
index ff635ff..ff635ff 100644
--- a/tests/test_P.out
+++ b/tests/posix/H.out
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/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/test_L_depth.out b/tests/posix/L.out
index ec9e861..ec9e861 100644
--- a/tests/test_L_depth.out
+++ b/tests/posix/L.out
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/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/test_follow.out b/tests/posix/L_depth.out
index ec9e861..ec9e861 100644
--- a/tests/test_follow.out
+++ b/tests/posix/L_depth.out
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/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/test_not.out b/tests/posix/bang.out
index b286454..b286454 100644
--- a/tests/test_not.out
+++ b/tests/posix/bang.out
diff --git a/tests/posix/bang.sh b/tests/posix/bang.sh
new file mode 100644
index 0000000..27840cd
--- /dev/null
+++ b/tests/posix/bang.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -name foo
diff --git a/tests/test_uid_plus_plus.out b/tests/posix/basic.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_uid_plus_plus.out
+++ b/tests/posix/basic.out
diff --git a/tests/posix/basic.sh b/tests/posix/basic.sh
new file mode 100644
index 0000000..3d43529
--- /dev/null
+++ b/tests/posix/basic.sh
@@ -0,0 +1 @@
+bfs_diff basic
diff --git a/tests/test_data_flow_and_swap.out b/tests/posix/data_flow_and_swap.out
index e604709..e604709 100644
--- a/tests/test_data_flow_and_swap.out
+++ b/tests/posix/data_flow_and_swap.out
diff --git a/tests/posix/data_flow_and_swap.sh b/tests/posix/data_flow_and_swap.sh
new file mode 100644
index 0000000..9a141af
--- /dev/null
+++ b/tests/posix/data_flow_and_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -type f -a -type d
diff --git a/tests/test_unique_depth.out b/tests/posix/data_flow_group.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_unique_depth.out
+++ b/tests/posix/data_flow_group.out
diff --git a/tests/posix/data_flow_group.sh b/tests/posix/data_flow_group.sh
new file mode 100644
index 0000000..453dc3e
--- /dev/null
+++ b/tests/posix/data_flow_group.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \)
diff --git a/tests/test_data_flow_or_swap.out b/tests/posix/data_flow_or_swap.out
index e604709..e604709 100644
--- a/tests/test_data_flow_or_swap.out
+++ b/tests/posix/data_flow_or_swap.out
diff --git a/tests/posix/data_flow_or_swap.sh b/tests/posix/data_flow_or_swap.sh
new file mode 100644
index 0000000..e8f504b
--- /dev/null
+++ b/tests/posix/data_flow_or_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type d \)
diff --git a/tests/test_perm_leading_plus_symbolic_minus.out b/tests/posix/data_flow_type.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic_minus.out
+++ b/tests/posix/data_flow_type.out
diff --git a/tests/posix/data_flow_type.sh b/tests/posix/data_flow_type.sh
new file mode 100644
index 0000000..33339df
--- /dev/null
+++ b/tests/posix/data_flow_type.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type f \)
diff --git a/tests/test_user_id.out b/tests/posix/data_flow_user.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_user_id.out
+++ b/tests/posix/data_flow_user.out
diff --git a/tests/posix/data_flow_user.sh b/tests/posix/data_flow_user.sh
new file mode 100644
index 0000000..44b6e1f
--- /dev/null
+++ b/tests/posix/data_flow_user.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \)
diff --git a/tests/test_de_morgan_and.out b/tests/posix/de_morgan_and.out
index 7b7afd2..7b7afd2 100644
--- a/tests/test_de_morgan_and.out
+++ b/tests/posix/de_morgan_and.out
diff --git a/tests/posix/de_morgan_and.sh b/tests/posix/de_morgan_and.sh
new file mode 100644
index 0000000..d52975e
--- /dev/null
+++ b/tests/posix/de_morgan_and.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -a \! -type f \)
diff --git a/tests/test_de_morgan_not.out b/tests/posix/de_morgan_not.out
index 5916da3..5916da3 100644
--- a/tests/test_de_morgan_not.out
+++ b/tests/posix/de_morgan_not.out
diff --git a/tests/posix/de_morgan_not.sh b/tests/posix/de_morgan_not.sh
new file mode 100644
index 0000000..7393ce0
--- /dev/null
+++ b/tests/posix/de_morgan_not.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name 'foo' -o \! -type f \)
diff --git a/tests/test_de_morgan_or.out b/tests/posix/de_morgan_or.out
index 2a57066..2a57066 100644
--- a/tests/test_de_morgan_or.out
+++ b/tests/posix/de_morgan_or.out
diff --git a/tests/posix/de_morgan_or.sh b/tests/posix/de_morgan_or.sh
new file mode 100644
index 0000000..378aab2
--- /dev/null
+++ b/tests/posix/de_morgan_or.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -o \! -type f \)
diff --git a/tests/test_deep_strict.out b/tests/posix/deep.out
index c385fce..c385fce 100644
--- a/tests/test_deep_strict.out
+++ b/tests/posix/deep.out
diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh
new file mode 100644
index 0000000..36a88c0
--- /dev/null
+++ b/tests/posix/deep.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/test_user_name.out b/tests/posix/depth.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_user_name.out
+++ b/tests/posix/depth.out
diff --git a/tests/posix/depth.sh b/tests/posix/depth.sh
new file mode 100644
index 0000000..444eba5
--- /dev/null
+++ b/tests/posix/depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth
diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out
new file mode 100644
index 0000000..7ed5f0d
--- /dev/null
+++ b/tests/posix/depth_error.out
@@ -0,0 +1,2 @@
+.
+./foo
diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh
new file mode 100644
index 0000000..db414ba
--- /dev/null
+++ b/tests/posix/depth_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar
+
+chmod a-r foo
+defer chmod +r foo
+
+! bfs_diff . -depth
diff --git a/tests/test_depth_slash.out b/tests/posix/depth_slash.out
index 77526d5..77526d5 100644
--- a/tests/test_depth_slash.out
+++ b/tests/posix/depth_slash.out
diff --git a/tests/posix/depth_slash.sh b/tests/posix/depth_slash.sh
new file mode 100644
index 0000000..f73e9f1
--- /dev/null
+++ b/tests/posix/depth_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -depth
diff --git a/tests/test_double_negation.out b/tests/posix/double_negation.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_double_negation.out
+++ b/tests/posix/double_negation.out
diff --git a/tests/posix/double_negation.sh b/tests/posix/double_negation.sh
new file mode 100644
index 0000000..eefe464
--- /dev/null
+++ b/tests/posix/double_negation.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \! -name 'foo'
diff --git a/tests/test_user_nouser.out b/tests/posix/exec.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_user_nouser.out
+++ b/tests/posix/exec.out
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/test_fprint_duplicate_stdout.out b/tests/posix/exec_nonexistent.out
index 6c21751..a7ccfe4 100644
--- a/tests/test_fprint_duplicate_stdout.out
+++ b/tests/posix/exec_nonexistent.out
@@ -1,38 +1,19 @@
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/e/f
basic/g
-basic/g
basic/g/h
-basic/g/h
-basic/i
basic/i
basic/j
-basic/j
-basic/j/foo
basic/j/foo
basic/k
-basic/k
basic/k/foo
-basic/k/foo
-basic/k/foo/bar
basic/k/foo/bar
basic/l
-basic/l
-basic/l/foo
basic/l/foo
basic/l/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nonexistent.sh b/tests/posix/exec_nonexistent.sh
new file mode 100644
index 0000000..a9ff052
--- /dev/null
+++ b/tests/posix/exec_nonexistent.sh
@@ -0,0 +1,4 @@
+# Failure to execute the command should lead to an error message and
+# non-zero exit status. See https://unix.stackexchange.com/q/704522/56202
+bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_nopath.out b/tests/posix/exec_nopath.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_nopath.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nopath.sh b/tests/posix/exec_nopath.sh
new file mode 100644
index 0000000..6e05d2e
--- /dev/null
+++ b/tests/posix/exec_nopath.sh
@@ -0,0 +1,7 @@
+(
+ unset PATH
+ invoke_bfs basic -exec echo {} \; >"$OUT"
+)
+
+sort_output
+diff_output
diff --git a/tests/test_exec_plus.out b/tests/posix/exec_plus.out
index f6b423b..f6b423b 100644
--- a/tests/test_exec_plus.out
+++ b/tests/posix/exec_plus.out
diff --git a/tests/posix/exec_plus.sh b/tests/posix/exec_plus.sh
new file mode 100644
index 0000000..56a93f1
--- /dev/null
+++ b/tests/posix/exec_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec "$TESTS/sort-args.sh" {} +
diff --git a/tests/posix/exec_plus_nonexistent.out b/tests/posix/exec_plus_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_nonexistent.sh b/tests/posix/exec_plus_nonexistent.sh
new file mode 100644
index 0000000..24582a3
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -exec "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_plus_nothing.sh b/tests/posix/exec_plus_nothing.sh
new file mode 100644
index 0000000..347722d
--- /dev/null
+++ b/tests/posix/exec_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -exec +
diff --git a/tests/test_exec_plus_semicolon.out b/tests/posix/exec_plus_semicolon.out
index f33c48f..f33c48f 100644
--- a/tests/test_exec_plus_semicolon.out
+++ b/tests/posix/exec_plus_semicolon.out
diff --git a/tests/posix/exec_plus_semicolon.sh b/tests/posix/exec_plus_semicolon.sh
new file mode 100644
index 0000000..449a3f9
--- /dev/null
+++ b/tests/posix/exec_plus_semicolon.sh
@@ -0,0 +1,5 @@
+# POSIX says:
+# Only a <plus-sign> that immediately follows an argument containing only the two characters "{}"
+# shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be
+# treated as special.
+bfs_diff basic -exec echo foo {} bar + baz \;
diff --git a/tests/posix/exec_plus_status.out b/tests/posix/exec_plus_status.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_status.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_status.sh b/tests/posix/exec_plus_status.sh
new file mode 100644
index 0000000..a814c4e
--- /dev/null
+++ b/tests/posix/exec_plus_status.sh
@@ -0,0 +1,3 @@
+# -exec ... {} + should always return true, but if the command fails, bfs
+# should exit with a non-zero status
+! bfs_diff basic -exec false {} + -print
diff --git a/tests/posix/exec_ulimit.out b/tests/posix/exec_ulimit.out
new file mode 100644
index 0000000..144169e
--- /dev/null
+++ b/tests/posix/exec_ulimit.out
@@ -0,0 +1,16 @@
+64 deep/0/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/1/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/2/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/3/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/4/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/5/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/6/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/7/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/8/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/9/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/A/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/B/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/C/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/D/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/E/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/F/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/posix/exec_ulimit.sh b/tests/posix/exec_ulimit.sh
new file mode 100644
index 0000000..655fbec
--- /dev/null
+++ b/tests/posix/exec_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -Sn 64
+bfs_diff deep -type f -exec bash -c 'printf "%d %s\n" $(ulimit -Sn) "${1:0:6}/.../${1##*/}"' bash {} \;
diff --git a/tests/posix/extra_paren.sh b/tests/posix/extra_paren.sh
new file mode 100644
index 0000000..d15022f
--- /dev/null
+++ b/tests/posix/extra_paren.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print \)
diff --git a/tests/test_flag_comma.out b/tests/posix/flag_comma.out
index 3574388..3574388 100644
--- a/tests/test_flag_comma.out
+++ b/tests/posix/flag_comma.out
diff --git a/tests/posix/flag_comma.sh b/tests/posix/flag_comma.sh
new file mode 100644
index 0000000..cec87e7
--- /dev/null
+++ b/tests/posix/flag_comma.sh
@@ -0,0 +1,3 @@
+# , is a filename until a non-flag is seen
+cd weirdnames
+bfs_diff -L ',' -print
diff --git a/tests/test_flag_weird_names.out b/tests/posix/flag_weird_names.out
index c395659..c395659 100644
--- a/tests/test_flag_weird_names.out
+++ b/tests/posix/flag_weird_names.out
diff --git a/tests/posix/flag_weird_names.sh b/tests/posix/flag_weird_names.sh
new file mode 100644
index 0000000..f6596e9
--- /dev/null
+++ b/tests/posix/flag_weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/group_id.out b/tests/posix/group_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_id.sh b/tests/posix/group_id.sh
new file mode 100644
index 0000000..2ff7bb3
--- /dev/null
+++ b/tests/posix/group_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -group "$(id -g)"
diff --git a/tests/posix/group_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/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/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/test_name_star_star.out b/tests/posix/name.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_name_star_star.out
+++ b/tests/posix/name.out
diff --git a/tests/posix/name.sh b/tests/posix/name.sh
new file mode 100644
index 0000000..a673ad0
--- /dev/null
+++ b/tests/posix/name.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*'
diff --git a/tests/test_perm_symbolic.out b/tests/posix/name_backslash.out
index e69de29..e69de29 100644
--- a/tests/test_perm_symbolic.out
+++ b/tests/posix/name_backslash.out
diff --git a/tests/posix/name_backslash.sh b/tests/posix/name_backslash.sh
new file mode 100644
index 0000000..ff9b539
--- /dev/null
+++ b/tests/posix/name_backslash.sh
@@ -0,0 +1,2 @@
+# An unescaped \ doesn't match
+bfs_diff weirdnames -name '\'
diff --git a/tests/test_name_bracket.out b/tests/posix/name_bracket.out
index 5ff3c0c..5ff3c0c 100644
--- a/tests/test_name_bracket.out
+++ b/tests/posix/name_bracket.out
diff --git a/tests/posix/name_bracket.sh b/tests/posix/name_bracket.sh
new file mode 100644
index 0000000..e2f943d
--- /dev/null
+++ b/tests/posix/name_bracket.sh
@@ -0,0 +1,9 @@
+# fnmatch() is broken on some platforms
+case "$UNAME" in
+ Darwin|NetBSD)
+ skip
+ ;;
+esac
+
+# An unclosed [ should be matched literally
+bfs_diff weirdnames -name '['
diff --git a/tests/test_name_character_class.out b/tests/posix/name_character_class.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_name_character_class.out
+++ b/tests/posix/name_character_class.out
diff --git a/tests/posix/name_character_class.sh b/tests/posix/name_character_class.sh
new file mode 100644
index 0000000..ecda190
--- /dev/null
+++ b/tests/posix/name_character_class.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '[e-g][!a-n][!p-z]'
diff --git a/tests/test_name_double_backslash.out b/tests/posix/name_double_backslash.out
index 45ceda0..45ceda0 100644
--- a/tests/test_name_double_backslash.out
+++ b/tests/posix/name_double_backslash.out
diff --git a/tests/posix/name_double_backslash.sh b/tests/posix/name_double_backslash.sh
new file mode 100644
index 0000000..009553a
--- /dev/null
+++ b/tests/posix/name_double_backslash.sh
@@ -0,0 +1,2 @@
+# An escaped \\ matches
+bfs_diff weirdnames -name '\\'
diff --git a/tests/test_name_root.out b/tests/posix/name_root.out
index 511198f..511198f 100644
--- a/tests/test_name_root.out
+++ b/tests/posix/name_root.out
diff --git a/tests/posix/name_root.sh b/tests/posix/name_root.sh
new file mode 100644
index 0000000..785861e
--- /dev/null
+++ b/tests/posix/name_root.sh
@@ -0,0 +1 @@
+bfs_diff basic/a -name a
diff --git a/tests/test_quit.out b/tests/posix/name_root_depth.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_quit.out
+++ b/tests/posix/name_root_depth.out
diff --git a/tests/posix/name_root_depth.sh b/tests/posix/name_root_depth.sh
new file mode 100644
index 0000000..dc3b8bb
--- /dev/null
+++ b/tests/posix/name_root_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -name g
diff --git a/tests/test_parens.out b/tests/posix/name_star_star.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_parens.out
+++ b/tests/posix/name_star_star.out
diff --git a/tests/posix/name_star_star.sh b/tests/posix/name_star_star.sh
new file mode 100644
index 0000000..035f635
--- /dev/null
+++ b/tests/posix/name_star_star.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '**f**'
diff --git a/tests/test_name_trailing_slash.out b/tests/posix/name_trailing_slash.out
index daff2f5..daff2f5 100644
--- a/tests/test_name_trailing_slash.out
+++ b/tests/posix/name_trailing_slash.out
diff --git a/tests/posix/name_trailing_slash.sh b/tests/posix/name_trailing_slash.sh
new file mode 100644
index 0000000..ab058d1
--- /dev/null
+++ b/tests/posix/name_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/g/ -name g
diff --git a/tests/test_newerma.out b/tests/posix/newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newerma.out
+++ b/tests/posix/newer.out
diff --git a/tests/posix/newer.sh b/tests/posix/newer.sh
new file mode 100644
index 0000000..860623a
--- /dev/null
+++ b/tests/posix/newer.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/a
diff --git a/tests/posix/newer_broken.out b/tests/posix/newer_broken.out
new file mode 100644
index 0000000..d2dcdd1
--- /dev/null
+++ b/tests/posix/newer_broken.out
@@ -0,0 +1 @@
+times
diff --git a/tests/posix/newer_broken.sh b/tests/posix/newer_broken.sh
new file mode 100644
index 0000000..dccaa73
--- /dev/null
+++ b/tests/posix/newer_broken.sh
@@ -0,0 +1,4 @@
+ln -s nowhere "$TEST/broken"
+"$XTOUCH" -h -t "1991-12-14 00:03" "$TEST/broken"
+
+bfs_diff times -newer "$TEST/broken"
diff --git a/tests/posix/newer_nonexistent.sh b/tests/posix/newer_nonexistent.sh
new file mode 100644
index 0000000..5f2da4b
--- /dev/null
+++ b/tests/posix/newer_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newer times/nonexistent
diff --git a/tests/test_printf_empty.out b/tests/posix/nogroup.out
index e69de29..e69de29 100644
--- a/tests/test_printf_empty.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_printf_w.out b/tests/posix/nogroup_ulimit.out
index e69de29..e69de29 100644
--- a/tests/test_printf_w.out
+++ b/tests/posix/nogroup_ulimit.out
diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh
new file mode 100644
index 0000000..a39dd1f
--- /dev/null
+++ b/tests/posix/nogroup_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nogroup
diff --git a/tests/test_not_prune.out b/tests/posix/not_prune.out
index 59e3c42..59e3c42 100644
--- a/tests/test_not_prune.out
+++ b/tests/posix/not_prune.out
diff --git a/tests/posix/not_prune.sh b/tests/posix/not_prune.sh
new file mode 100644
index 0000000..6d7b092
--- /dev/null
+++ b/tests/posix/not_prune.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name foo -prune \)
diff --git a/tests/test_quit_before_print.out b/tests/posix/nouser.out
index e69de29..e69de29 100644
--- a/tests/test_quit_before_print.out
+++ b/tests/posix/nouser.out
diff --git a/tests/posix/nouser.sh b/tests/posix/nouser.sh
new file mode 100644
index 0000000..e7c48c0
--- /dev/null
+++ b/tests/posix/nouser.sh
@@ -0,0 +1 @@
+bfs_diff basic -nouser
diff --git a/tests/test_size_big.out b/tests/posix/nouser_ulimit.out
index e69de29..e69de29 100644
--- a/tests/test_size_big.out
+++ b/tests/posix/nouser_ulimit.out
diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh
new file mode 100644
index 0000000..a94b8c5
--- /dev/null
+++ b/tests/posix/nouser_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nouser
diff --git a/tests/test_or.out b/tests/posix/o.out
index 1650c4d..1650c4d 100644
--- a/tests/test_or.out
+++ b/tests/posix/o.out
diff --git a/tests/posix/o.sh b/tests/posix/o.sh
new file mode 100644
index 0000000..6dcd442
--- /dev/null
+++ b/tests/posix/o.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -o -type d
diff --git a/tests/posix/ok_plus_nothing.sh b/tests/posix/ok_plus_nothing.sh
new file mode 100644
index 0000000..77c7644
--- /dev/null
+++ b/tests/posix/ok_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -ok +
diff --git a/tests/test_ok_stdin.out b/tests/posix/ok_stdin.out
index 7acf711..7acf711 100644
--- a/tests/test_ok_stdin.out
+++ b/tests/posix/ok_stdin.out
diff --git a/tests/posix/ok_stdin.sh b/tests/posix/ok_stdin.sh
new file mode 100644
index 0000000..a190d81
--- /dev/null
+++ b/tests/posix/ok_stdin.sh
@@ -0,0 +1,3 @@
+# -ok should *not* close stdin
+# See https://savannah.gnu.org/bugs/?24561
+yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/test_xtype_reorder.out b/tests/posix/or_purity.out
index e69de29..e69de29 100644
--- a/tests/test_xtype_reorder.out
+++ b/tests/posix/or_purity.out
diff --git a/tests/posix/or_purity.sh b/tests/posix/or_purity.sh
new file mode 100644
index 0000000..277b18b
--- /dev/null
+++ b/tests/posix/or_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name '*' -o -print
diff --git a/tests/posix/overlayfs.out b/tests/posix/overlayfs.out
new file mode 100644
index 0000000..b472b56
--- /dev/null
+++ b/tests/posix/overlayfs.out
@@ -0,0 +1,5 @@
+merged
+merged/bar
+merged/baz
+merged/baz/qux
+merged/foo
diff --git a/tests/posix/overlayfs.sh b/tests/posix/overlayfs.sh
new file mode 100644
index 0000000..21ef22f
--- /dev/null
+++ b/tests/posix/overlayfs.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+mkdir -p work merged
+bfs_sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged || skip
+defer bfs_sudo rm -rf work
+defer bfs_sudo umount merged
+
+bfs_diff merged
diff --git a/tests/test_regextype_grep.out b/tests/posix/parens.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_regextype_grep.out
+++ b/tests/posix/parens.out
diff --git a/tests/posix/parens.sh b/tests/posix/parens.sh
new file mode 100644
index 0000000..abbb20f
--- /dev/null
+++ b/tests/posix/parens.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name '*f*' \)
diff --git a/tests/test_wholename.out b/tests/posix/path.out
index ae1ae21..ae1ae21 100644
--- a/tests/test_wholename.out
+++ b/tests/posix/path.out
diff --git a/tests/posix/path.sh b/tests/posix/path.sh
new file mode 100644
index 0000000..04606eb
--- /dev/null
+++ b/tests/posix/path.sh
@@ -0,0 +1 @@
+bfs_diff basic -path 'basic/*f*'
diff --git a/tests/test_perm_000.out b/tests/posix/perm_000.out
index 5fd30bc..5fd30bc 100644
--- a/tests/test_perm_000.out
+++ b/tests/posix/perm_000.out
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/test_perm_000_slash.out b/tests/posix/perm_000_minus.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_slash.out
+++ b/tests/posix/perm_000_minus.out
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/test_perm_222.out b/tests/posix/perm_222.out
index 1690e43..1690e43 100644
--- a/tests/test_perm_222.out
+++ b/tests/posix/perm_222.out
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/test_perm_222_minus.out b/tests/posix/perm_222_minus.out
index 1690e43..1690e43 100644
--- a/tests/test_perm_222_minus.out
+++ b/tests/posix/perm_222_minus.out
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/test_perm_644.out b/tests/posix/perm_644.out
index 4e64e49..4e64e49 100644
--- a/tests/test_perm_644.out
+++ b/tests/posix/perm_644.out
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/test_perm_644_minus.out b/tests/posix/perm_644_minus.out
index 2e2576b..2e2576b 100644
--- a/tests/test_perm_644_minus.out
+++ b/tests/posix/perm_644_minus.out
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..e69de29
--- /dev/null
+++ b/tests/posix/perm_leading_plus_symbolic_minus.out
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/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/test_perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out
index 2e2576b..2e2576b 100644
--- a/tests/test_perm_symbolic_minus.out
+++ b/tests/posix/perm_symbolic_minus.out
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/test_permcopy.out b/tests/posix/permcopy.out
index 4e64e49..4e64e49 100644
--- a/tests/test_permcopy.out
+++ b/tests/posix/permcopy.out
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_prune.out b/tests/posix/prune.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_prune.out
+++ b/tests/posix/prune.out
diff --git a/tests/posix/prune.sh b/tests/posix/prune.sh
new file mode 100644
index 0000000..b48ab48
--- /dev/null
+++ b/tests/posix/prune.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune
diff --git a/tests/test_prune_file.out b/tests/posix/prune_file.out
index 7575ae4..7575ae4 100644
--- a/tests/test_prune_file.out
+++ b/tests/posix/prune_file.out
diff --git a/tests/posix/prune_file.sh b/tests/posix/prune_file.sh
new file mode 100644
index 0000000..29a3a33
--- /dev/null
+++ b/tests/posix/prune_file.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -name '?' -prune
diff --git a/tests/test_prune_or_print.out b/tests/posix/prune_or_print.out
index 59e3c42..59e3c42 100644
--- a/tests/test_prune_or_print.out
+++ b/tests/posix/prune_or_print.out
diff --git a/tests/posix/prune_or_print.sh b/tests/posix/prune_or_print.sh
new file mode 100644
index 0000000..85b97fd
--- /dev/null
+++ b/tests/posix/prune_or_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune -o -print
diff --git a/tests/posix/readdir_error.sh b/tests/posix/readdir_error.sh
new file mode 100644
index 0000000..82fcd17
--- /dev/null
+++ b/tests/posix/readdir_error.sh
@@ -0,0 +1,37 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkfifo hang pid wait running
+
+(
+ # Create a zombie process
+ cat hang >/dev/null &
+ # Write the PID to pid
+ echo $! >pid
+ # Don't wait on the zombie process
+ exec cat wait hang >running
+) &
+
+# Kill the parent cat on exit
+defer kill -9 %1
+
+# Read the child PID
+read -r pid <pid
+
+# Make sure the parent cat is running before we kill the child, because bash
+# will wait() on its children
+echo >wait &
+read -r _ <running
+
+# Turn the child into a zombie
+kill -9 "$pid"
+
+# Wait until it's really a zombie
+state=R
+while [ "$state" != "Z" ]; do
+ read -r _ _ state _ <"/proc/$pid/stat"
+done
+
+# On Linux, open(/proc/$pid/net) will succeed but readdir() will fail
+test -r "/proc/$pid/net" || skip
+! invoke_bfs "/proc/$pid/net" >/dev/null
diff --git a/tests/test_size.out b/tests/posix/size.out
index eeabbd7..eeabbd7 100644
--- a/tests/test_size.out
+++ b/tests/posix/size.out
diff --git a/tests/posix/size.sh b/tests/posix/size.sh
new file mode 100644
index 0000000..1e7528a
--- /dev/null
+++ b/tests/posix/size.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 0
diff --git a/tests/test_size_bytes.out b/tests/posix/size_bytes.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_bytes.out
+++ b/tests/posix/size_bytes.out
diff --git a/tests/posix/size_bytes.sh b/tests/posix/size_bytes.sh
new file mode 100644
index 0000000..6a68321
--- /dev/null
+++ b/tests/posix/size_bytes.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0c
diff --git a/tests/test_size_plus.out b/tests/posix/size_plus.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_plus.out
+++ b/tests/posix/size_plus.out
diff --git a/tests/posix/size_plus.sh b/tests/posix/size_plus.sh
new file mode 100644
index 0000000..01853d5
--- /dev/null
+++ b/tests/posix/size_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0
diff --git a/tests/posix/type_bind_mount.out b/tests/posix/type_bind_mount.out
new file mode 100644
index 0000000..2f06c47
--- /dev/null
+++ b/tests/posix/type_bind_mount.out
@@ -0,0 +1 @@
+./null
diff --git a/tests/posix/type_bind_mount.sh b/tests/posix/type_bind_mount.sh
new file mode 100644
index 0000000..97b7305
--- /dev/null
+++ b/tests/posix/type_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -type c
diff --git a/tests/test_type_d.out b/tests/posix/type_d.out
index e604709..e604709 100644
--- a/tests/test_type_d.out
+++ b/tests/posix/type_d.out
diff --git a/tests/posix/type_d.sh b/tests/posix/type_d.sh
new file mode 100644
index 0000000..8d06b73
--- /dev/null
+++ b/tests/posix/type_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -type d
diff --git a/tests/posix/type_f.out b/tests/posix/type_f.out
new file mode 100644
index 0000000..6218a0c
--- /dev/null
+++ b/tests/posix/type_f.out
@@ -0,0 +1,7 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
+basic/j/foo
+basic/k/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/type_f.sh b/tests/posix/type_f.sh
new file mode 100644
index 0000000..1fd0c8c
--- /dev/null
+++ b/tests/posix/type_f.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f
diff --git a/tests/test_type_l.out b/tests/posix/type_l.out
index f2c8b19..f2c8b19 100644
--- a/tests/test_type_l.out
+++ b/tests/posix/type_l.out
diff --git a/tests/posix/type_l.sh b/tests/posix/type_l.sh
new file mode 100644
index 0000000..457f74d
--- /dev/null
+++ b/tests/posix/type_l.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l
diff --git a/tests/posix/unionfs.out b/tests/posix/unionfs.out
new file mode 100644
index 0000000..28c4ec1
--- /dev/null
+++ b/tests/posix/unionfs.out
@@ -0,0 +1,10 @@
+.
+./lower
+./lower/bar
+./lower/baz
+./lower/foo
+./upper
+./upper/bar
+./upper/baz
+./upper/baz/qux
+./upper/foo
diff --git a/tests/posix/unionfs.sh b/tests/posix/unionfs.sh
new file mode 100644
index 0000000..94d3929
--- /dev/null
+++ b/tests/posix/unionfs.sh
@@ -0,0 +1,9 @@
+[[ "$UNAME" == *BSD* ]] || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+bfs_sudo mount -t unionfs -o below lower upper || skip
+defer bfs_sudo umount upper
+
+bfs_diff .
diff --git a/tests/posix/user_id.out b/tests/posix/user_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_id.sh b/tests/posix/user_id.sh
new file mode 100644
index 0000000..c3e4b31
--- /dev/null
+++ b/tests/posix/user_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -user "$(id -u)"
diff --git a/tests/posix/user_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/test_weird_names.out b/tests/posix/weird_names.out
index c395659..c395659 100644
--- a/tests/test_weird_names.out
+++ b/tests/posix/weird_names.out
diff --git a/tests/posix/weird_names.sh b/tests/posix/weird_names.sh
new file mode 100644
index 0000000..8a9a8cd
--- /dev/null
+++ b/tests/posix/weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/xdev.out b/tests/posix/xdev.out
new file mode 100644
index 0000000..6253434
--- /dev/null
+++ b/tests/posix/xdev.out
@@ -0,0 +1,4 @@
+.
+./foo
+./foo/bar
+./mnt
diff --git a/tests/posix/xdev.sh b/tests/posix/xdev.sh
new file mode 100644
index 0000000..c59c5c8
--- /dev/null
+++ b/tests/posix/xdev.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+"$XTOUCH" foo/bar mnt/baz
+
+bfs_diff . -xdev
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100644
index 0000000..ad9c0be
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,464 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Running test cases
+
+# Beginning/end of line escape sequences
+BOL=$'\n'
+EOL=$'\n'
+
+# Update $EOL for the terminal size
+update_eol() {
+ # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
+ local cols="${COLUMNS-}"
+ if [ -z "$cols" ]; then
+ cols=$(tput cols 2>/dev/tty)
+ fi
+
+ # Put the cursor at the last column, then write a space so the next
+ # character will wrap
+ EOL=$'\e['"${cols}G "
+}
+
+# ERR trap for tests
+debug_err() {
+ local ret=$? line func file
+ callers | while read -r line func file; do
+ if [ "$func" = source ]; then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ break
+ fi
+ done
+}
+
+# Source a test
+source_test() (
+ set -eE
+ trap debug_err ERR
+
+ if ((${#MAKE[@]})); then
+ # Close the jobserver pipes
+ exec {READY_PIPE}<&- {DONE_PIPE}>&-
+ fi
+
+ cd "$TMP"
+ source "$@"
+)
+
+# Run a test
+run_test() {
+ if ((VERBOSE_ERRORS)); then
+ source_test "$1"
+ else
+ source_test "$1" 2>"$TMP/$TEST.err"
+ fi
+ ret=$?
+
+ if ((${#MAKE[@]})); then
+ # Write one byte to the done pipe
+ printf . >&$DONE_PIPE
+ fi
+
+ case $ret in
+ 0)
+ if ((VERBOSE_TESTS)); then
+ color printf "${BOL}${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ $EX_SKIP)
+ if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then
+ color printf "${BOL}${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ *)
+ if ((!VERBOSE_ERRORS)); then
+ cat "$TMP/$TEST.err" >&2
+ fi
+ color printf "${BOL}${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
+ ;;
+ esac
+
+ return $ret
+}
+
+# Count the tests running in the background
+BG=0
+
+# Run a test in the background
+bg_test() {
+ run_test "$1" &
+ ((++BG))
+}
+
+# Reap a finished background test
+reap_test() {
+ ((BG--))
+
+ case "$1" in
+ 0)
+ ((++passed))
+ ;;
+ $EX_SKIP)
+ ((++skipped))
+ ;;
+ *)
+ ((++failed))
+ ;;
+ esac
+}
+
+# Wait for a background test to finish
+wait_test() {
+ local pid
+ wait -n -ppid
+ ret=$?
+ if [ -z "${pid:-}" ]; then
+ debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR
+ exit 1
+ fi
+
+ reap_test $ret
+}
+
+# Wait until we're ready to run another test
+wait_ready() {
+ if ((${#MAKE[@]})); then
+ # We'd like to parse the output of jobs -n, but we can't run it in a
+ # subshell or we won't get the right output
+ jobs -n >"$TMP/jobs"
+ while read -r job status ret foo; do
+ case "$status" in
+ Done)
+ reap_test 0
+ ;;
+ Exit)
+ reap_test $ret
+ ;;
+ esac
+ done <"$TMP/jobs"
+
+ # Read one byte from the ready pipe
+ read -r -N1 -u$READY_PIPE
+ elif ((BG >= JOBS)); then
+ wait_test
+ fi
+}
+
+# Run make as a co-process to use its job control
+comake() {
+ coproc {
+ # We can't just use std{in,out}, due to
+ # https://www.gnu.org/software/make/manual/html_node/Parallel-Input.html
+ exec {DONE_PIPE}<&0 {READY_PIPE}>&1
+ exec "${MAKE[@]}" -s \
+ -f "$TESTS/tests.mk" \
+ DONE=$DONE_PIPE \
+ READY=$READY_PIPE \
+ "${!TEST_CASES[@]}" \
+ </dev/null >/dev/null
+ }
+
+ # coproc pipes aren't inherited by subshells, so dup them
+ exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]}
+}
+
+# Run all the tests
+run_tests() {
+ if ((VERBOSE_TESTS)); then
+ BOL=''
+ elif ((COLOR_STDOUT)); then
+ # Carriage return + clear line
+ BOL=$'\r\e[K'
+
+ # Workaround for bash 4: checkwinsize is off by default. We can turn it
+ # on, but we also have to explicitly trigger a foreground job to finish
+ # so that it will update the window size before we use $COLUMNS
+ shopt -s checkwinsize
+ (:)
+
+ update_eol
+ trap update_eol WINCH
+ fi
+
+ passed=0
+ failed=0
+ skipped=0
+ ran=0
+ total=${#TEST_CASES[@]}
+
+ if ((COLOR_STDOUT || VERBOSE_TESTS)); then
+ TEST_FMT="${BOL}${YLW}[%3d%%]${RST} ${BLD}%s${RST}${EOL}"
+ else
+ TEST_FMT="."
+ fi
+
+ if ((${#MAKE[@]})); then
+ comake
+ fi
+
+ # Turn off set -e (but turn it back on in run_test)
+ set +e
+
+ for TEST in "${TEST_CASES[@]}"; do
+ wait_ready
+ if ((STOP && failed > 0)); then
+ break
+ fi
+
+ percent=$((100 * ran / total))
+ color printf "$TEST_FMT" $percent "$TEST"
+
+ mkdir -p "$TMP/$TEST"
+ OUT="$TMP/$TEST.out"
+
+ bg_test "$TESTS/$TEST.sh"
+ ((++ran))
+ done
+
+ while ((BG > 0)); do
+ wait_test
+ done
+
+ printf "${BOL}"
+
+ if ((passed > 0)); then
+ color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total
+ fi
+ if ((skipped > 0)); then
+ color printf "${CYN}[SKIP]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $skipped $total
+ fi
+ if ((failed > 0)); then
+ color printf "${RED}[FAIL]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $failed $total
+ exit 1
+ fi
+}
+
+## Utilities for the tests themselves
+
+# Default return value for failed tests
+EX_FAIL=1
+
+# Fail the current test
+fail() {
+ exit $EX_FAIL
+}
+
+# Return value when a test is skipped
+EX_SKIP=77
+
+# Skip the current test
+skip() {
+ if ((VERBOSE_SKIPPED)); then
+ caller | {
+ read -r line file
+ printf "${BOL}"
+ debug "$file" $line "" >&$DUPOUT
+ }
+ fi
+
+ exit $EX_SKIP
+}
+
+# Run a command and check its exit status
+check_exit() {
+ local expected="$1"
+ local actual=0
+ shift
+ "$@" || actual=$?
+ ((actual == expected))
+}
+
+# Run a command with sudo
+bfs_sudo() {
+ if ((${#SUDO[@]})); then
+ "${SUDO[@]}" "$@"
+ else
+ return 1
+ fi
+}
+
+# Get the inode number of a file
+inum() {
+ ls -id "$@" | awk '{ print $1 }'
+}
+
+# Set an ACL on a file
+set_acl() {
+ case "$UNAME" in
+ Darwin)
+ chmod +a "$(id -un) allow read,write" "$1"
+ ;;
+ FreeBSD)
+ if (($(getconf ACL_NFS4 "$1") > 0)); then
+ setfacl -m "u:$(id -un):rw::allow" "$1"
+ else
+ setfacl -m "u:$(id -un):rw" "$1"
+ fi
+ ;;
+ *)
+ setfacl -m "u:$(id -un):rw" "$1"
+ ;;
+ esac
+}
+
+# Print a bfs invocation for --verbose=commands
+bfs_verbose() {
+ if ((VERBOSE_COMMANDS)); then
+ (
+ # Close some fds to make room for the pipe,
+ # even with extremely low ulimit -n
+ exec >&- {DUPERR}>&-
+ exec >&$DUPOUT {DUPOUT}>&-
+ color bfs_verbose_impl "$@"
+ )
+ fi
+}
+
+bfs_verbose_impl() {
+ printf "${GRN}%q${RST}" "${BFS[0]}"
+ if ((${#BFS[@]} > 1)); then
+ printf " ${GRN}%q${RST}" "${BFS[@]:1}"
+ fi
+
+ local expr_started=0 color
+ for arg; do
+ case "$arg" in
+ -[A-Z]*|-[dsxf]|-j*)
+ color="${CYN}"
+ ;;
+ \(|!|-[ao]|-and|-or|-not|-exclude)
+ expr_started=1
+ color="${RED}"
+ ;;
+ \)|,)
+ if ((expr_started)); then
+ color="${RED}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ -?*)
+ expr_started=1
+ color="${BLU}"
+ ;;
+ *)
+ if ((expr_started)); then
+ color="${BLD}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ esac
+ printf " ${color}%q${RST}" "$arg"
+ done
+
+ printf '\n'
+}
+
+# Run the bfs we're testing
+invoke_bfs() {
+ bfs_verbose "$@"
+
+ local ret=0
+ # Close the logging fds
+ "${BFS[@]}" "$@" {DUPOUT}>&- {DUPERR}>&- || ret=$?
+
+ # Allow bfs to fail, but not crash
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+if command -v unbuffer &>/dev/null; then
+ UNBUFFER=unbuffer
+elif command -v expect_unbuffer &>/dev/null; then
+ UNBUFFER=expect_unbuffer
+fi
+
+# Run bfs with a pseudo-terminal attached
+bfs_pty() {
+ test -n "${UNBUFFER:-}" || skip
+
+ bfs_verbose "$@"
+
+ local ret=0
+ "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$?
+
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+# Create a directory tree with xattrs in scratch
+make_xattrs() {
+ cd "$TEST"
+
+ "$XTOUCH" normal xattr xattr_2
+ ln -s xattr link
+ ln -s normal xattr_link
+
+ case "$UNAME" in
+ Darwin)
+ xattr -w bfs_test true xattr \
+ && xattr -w bfs_test_2 true xattr_2 \
+ && xattr -s -w bfs_test true xattr_link
+ ;;
+ FreeBSD)
+ setextattr user bfs_test true xattr \
+ && setextattr user bfs_test_2 true xattr_2 \
+ && setextattr -h user bfs_test true xattr_link
+ ;;
+ *)
+ # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
+ # namespace, which is writable by root and readable by others
+ bfs_sudo setfattr -n security.bfs_test xattr \
+ && bfs_sudo setfattr -n security.bfs_test_2 xattr_2 \
+ && bfs_sudo setfattr -h -n security.bfs_test xattr_link
+ ;;
+ esac
+}
+
+## Snapshot testing
+
+# Return value when a difference is detected
+EX_DIFF=20
+
+# Detect colored diff support
+if diff --color /dev/null /dev/null &>/dev/null; then
+ DIFF="diff --color"
+else
+ DIFF="diff"
+fi
+
+# Sort the output file
+sort_output() {
+ sort -o "$OUT" "$OUT"
+}
+
+# Diff against the expected output
+diff_output() {
+ local GOLD="$TESTS/$TEST.out"
+
+ if ((UPDATE)); then
+ cp "$OUT" "$GOLD"
+ elif ! cmp -s "$GOLD" "$OUT"; then
+ $DIFF -u "$GOLD" "$OUT" >&$DUPERR
+ fi
+}
+
+# Run bfs, and diff it against the expected output
+bfs_diff() {
+ local ret=0
+ invoke_bfs "$@" >"$OUT" || ret=$?
+
+ sort_output
+ diff_output || exit $EX_DIFF
+
+ return $ret
+}
diff --git a/tests/stddirs.sh b/tests/stddirs.sh
new file mode 100644
index 0000000..e08e6bf
--- /dev/null
+++ b/tests/stddirs.sh
@@ -0,0 +1,152 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Standard directory trees for tests
+
+# Creates a simple file+directory structure for tests
+make_basic() {
+ "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
+ "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
+ echo baz >"$1/l/foo/bar/baz"
+}
+
+# Creates a file+directory structure with various permissions for tests
+make_perms() {
+ "$XTOUCH" -p -M000 "$1/0"
+ "$XTOUCH" -p -M444 "$1/r"
+ "$XTOUCH" -p -M222 "$1/w"
+ "$XTOUCH" -p -M644 "$1/rw"
+ "$XTOUCH" -p -M555 "$1/rx"
+ "$XTOUCH" -p -M311 "$1/wx"
+ "$XTOUCH" -p -M755 "$1/rwx"
+}
+
+# Creates a file+directory structure with various symbolic and hard links
+make_links() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln "$1/file" "$1/hardlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
+ ln -s file "$1/deeply/nested/link"
+ ln -s nowhere "$1/deeply/nested/broken"
+ ln -s deeply/nested "$1/skip"
+}
+
+# Creates a file+directory structure with symbolic link loops
+make_loops() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ ln -s loop "$1/loop"
+ mkdir -p "$1/deeply/nested/dir"
+ ln -s ../../deeply "$1/deeply/nested/loop"
+ ln -s deeply/nested/loop/nested "$1/skip"
+}
+
+# Creates a file+directory structure with varying timestamps
+make_times() {
+ "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
+ "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
+ "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
+ ln -s a "$1/l"
+ "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
+ "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
+}
+
+# Creates a file+directory structure with various weird file/directory names
+make_weirdnames() {
+ "$XTOUCH" -p "$1/-/a"
+ "$XTOUCH" -p "$1/(/b"
+ "$XTOUCH" -p "$1/(-/c"
+ "$XTOUCH" -p "$1/!/d"
+ "$XTOUCH" -p "$1/!-/e"
+ "$XTOUCH" -p "$1/,/f"
+ "$XTOUCH" -p "$1/)/g"
+ "$XTOUCH" -p "$1/.../h"
+ "$XTOUCH" -p "$1/\\/i"
+ "$XTOUCH" -p "$1/ /j"
+ "$XTOUCH" -p "$1/[/k"
+}
+
+# Creates a very deep directory structure for testing PATH_MAX handling
+make_deep() {
+ mkdir -p "$1"
+
+ # $name will be 255 characters, aka _XOPEN_NAME_MAX
+ local name="0123456789ABCDEF"
+ name="$name$name$name$name"
+ name="$name$name$name$name"
+ name="${name:0:255}"
+
+ # 4 * 4 * 256 == 4096 >= PATH_MAX
+ local path="$name/$name/$name/$name"
+ path="$path/$path/$path/$path"
+
+ "$XTOUCH" -p "$1"/{{0..9},A,B,C,D,E,F}/"$path/$name"
+}
+
+# Creates a directory structure with many different types, and therefore colors
+make_rainbow() {
+ "$XTOUCH" -p "$1/file.txt"
+ "$XTOUCH" -p "$1/file.dat"
+ "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
+ "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
+ "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
+ ln -s file.txt "$1/link.txt"
+ "$XTOUCH" -p "$1/mh1"
+ ln "$1/mh1" "$1/mh2"
+ mkfifo "$1/pipe"
+ # TODO: block
+ ln -s /dev/null "$1/chardev_link"
+ ln -s nowhere "$1/broken"
+ "$MKSOCK" "$1/socket"
+ "$XTOUCH" -p "$1"/s{u,g,ug}id
+ chmod 06644 "$1"/sugid
+ chmod 04644 "$1"/suid
+ chmod 02644 "$1"/sgid
+ mkdir "$1/ow" "$1"/sticky{,_ow}
+ chmod o+w "$1"/*ow
+ chmod +t "$1"/sticky*
+ "$XTOUCH" -p "$1"/exec.sh
+ chmod +x "$1"/exec.sh
+ "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
+}
+
+# Create all standard directory trees
+make_stddirs() {
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
+
+ if ((CLEAN)); then
+ defer clean_stddirs
+ else
+ printf "Test files saved to ${BLD}%s${RST}\n" "$TMP"
+ fi
+
+ chown "$(id -u):$(id -g)" "$TMP"
+
+ make_basic "$TMP/basic"
+ make_perms "$TMP/perms"
+ make_links "$TMP/links"
+ make_loops "$TMP/loops"
+ make_times "$TMP/times"
+ make_weirdnames "$TMP/weirdnames"
+ make_deep "$TMP/deep"
+ make_rainbow "$TMP/rainbow"
+}
+
+# Clean up temporary directories on exit
+clean_stddirs() {
+ # Don't force rm to deal with long paths
+ for dir in "$TMP"/deep/*/*; do
+ if [ -d "$dir" ]; then
+ (cd "$dir" && rm -rf *)
+ fi
+ done
+
+ rm -rf "$TMP"
+}
diff --git a/tests/test_L_acl.out b/tests/test_L_acl.out
deleted file mode 100644
index 1dae00a..0000000
--- a/tests/test_L_acl.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/acl
-scratch/link
diff --git a/tests/test_L_capable.out b/tests/test_L_capable.out
deleted file mode 100644
index e5ba3c7..0000000
--- a/tests/test_L_capable.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/capable
-scratch/link
diff --git a/tests/test_L_delete.out b/tests/test_L_delete.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_L_delete.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_L_mount.out b/tests/test_L_mount.out
deleted file mode 100644
index 2e80082..0000000
--- a/tests/test_L_mount.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/foo/bar
-scratch/foo/qux
-scratch/mnt
diff --git a/tests/test_L_xattr.out b/tests/test_L_xattr.out
deleted file mode 100644
index 12fac95..0000000
--- a/tests/test_L_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/link
-scratch/xattr
-scratch/xattr_2
diff --git a/tests/test_L_xattrname.out b/tests/test_L_xattrname.out
deleted file mode 100644
index 4dc4836..0000000
--- a/tests/test_L_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/xattr
diff --git a/tests/test_L_xdev.out b/tests/test_L_xdev.out
deleted file mode 100644
index 2e80082..0000000
--- a/tests/test_L_xdev.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/foo/bar
-scratch/foo/qux
-scratch/mnt
diff --git a/tests/test_acl.out b/tests/test_acl.out
deleted file mode 100644
index ddf8446..0000000
--- a/tests/test_acl.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/acl
diff --git a/tests/test_capable.out b/tests/test_capable.out
deleted file mode 100644
index 78b5bd9..0000000
--- a/tests/test_capable.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/capable
diff --git a/tests/test_color_ls.out b/tests/test_color_ls.out
deleted file mode 100644
index b08d894..0000000
--- a/tests/test_color_ls.out
+++ /dev/null
@@ -1,12 +0,0 @@
-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_delete.out b/tests/test_delete.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_delete_many.out b/tests/test_delete_many.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete_many.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_depth_error.out b/tests/test_depth_error.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_depth_error.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_empty_special.out b/tests/test_empty_special.out
deleted file mode 100644
index 3927f2b..0000000
--- a/tests/test_empty_special.out
+++ /dev/null
@@ -1,14 +0,0 @@
-rainbow/exec.sh
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/ow
-rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
-rainbow/sticky
-rainbow/sticky_ow
-rainbow/sugid
-rainbow/suid
diff --git a/tests/test_exec_flush.out b/tests/test_exec_flush.out
deleted file mode 100644
index 7e93fdf..0000000
--- a/tests/test_exec_flush.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_flags.out b/tests/test_flags.out
deleted file mode 100644
index 11998ed..0000000
--- a/tests/test_flags.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/bar
diff --git a/tests/test_inum_automount.out b/tests/test_inum_automount.out
deleted file mode 100644
index 99c7511..0000000
--- a/tests/test_inum_automount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/mnt
diff --git a/tests/test_inum_bind_mount.out b/tests/test_inum_bind_mount.out
deleted file mode 100644
index a520de3..0000000
--- a/tests/test_inum_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/bar
-scratch/foo
diff --git a/tests/test_inum_mount.out b/tests/test_inum_mount.out
deleted file mode 100644
index 99c7511..0000000
--- a/tests/test_inum_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/mnt
diff --git a/tests/test_mount.out b/tests/test_mount.out
deleted file mode 100644
index f7839fb..0000000
--- a/tests/test_mount.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/foo/bar
-scratch/mnt
diff --git a/tests/test_print0.out b/tests/test_print0.out
deleted file mode 100644
index ed2b7e8..0000000
--- a/tests/test_print0.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_printf_Y_error.out b/tests/test_printf_Y_error.out
deleted file mode 100644
index 410a9b5..0000000
--- a/tests/test_printf_Y_error.out
+++ /dev/null
@@ -1,3 +0,0 @@
-(scratch) () d d
-(scratch/bar) (foo/bar) l ?
-(scratch/foo) () d d
diff --git a/tests/test_printf_nul.out b/tests/test_printf_nul.out
deleted file mode 100644
index 6833fdd..0000000
--- a/tests/test_printf_nul.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_regex_invalid_utf8.out b/tests/test_regex_invalid_utf8.out
deleted file mode 100644
index 03f3f58..0000000
--- a/tests/test_regex_invalid_utf8.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/
diff --git a/tests/test_rm.out b/tests/test_rm.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_rm.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_type_bind_mount.out b/tests/test_type_bind_mount.out
deleted file mode 100644
index 6435159..0000000
--- a/tests/test_type_bind_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/null
diff --git a/tests/test_xattr.out b/tests/test_xattr.out
deleted file mode 100644
index 109e7c9..0000000
--- a/tests/test_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/xattr
-scratch/xattr_2
-scratch/xattr_link
diff --git a/tests/test_xattrname.out b/tests/test_xattrname.out
deleted file mode 100644
index 0285ac1..0000000
--- a/tests/test_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/xattr
-scratch/xattr_link
diff --git a/tests/test_xdev.out b/tests/test_xdev.out
deleted file mode 100644
index f7839fb..0000000
--- a/tests/test_xdev.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/foo/bar
-scratch/mnt
diff --git a/tests/test_xtype_bind_mount.out b/tests/test_xtype_bind_mount.out
deleted file mode 100644
index 16804ea..0000000
--- a/tests/test_xtype_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/null
diff --git a/tests/tests.h b/tests/tests.h
new file mode 100644
index 0000000..9078938
--- /dev/null
+++ b/tests/tests.h
@@ -0,0 +1,73 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Unit tests.
+ */
+
+#ifndef BFS_TESTS_H
+#define BFS_TESTS_H
+
+#include "prelude.h"
+#include "diag.h"
+
+/** Unit test function type. */
+typedef bool test_fn(void);
+
+/** Memory allocation tests. */
+bool check_alloc(void);
+
+/** Standard library wrapper tests. */
+bool check_bfstd(void);
+
+/** Bit manipulation tests. */
+bool check_bit(void);
+
+/** I/O queue tests. */
+bool check_ioq(void);
+
+/** Trie tests. */
+bool check_trie(void);
+
+/** Process spawning tests. */
+bool check_xspawn(void);
+
+/** Time tests. */
+bool check_xtime(void);
+
+/** Don't ignore the bfs_check() return value. */
+attr(nodiscard)
+static inline bool bfs_check(bool ret) {
+ return ret;
+}
+
+/**
+ * Check a condition, logging a message on failure but continuing.
+ */
+#define bfs_check(...) \
+ bfs_check(bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", ""))
+
+#define bfs_check_(str, cond, format, ...) \
+ ((cond) ? true : (bfs_diag( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s%s" \
+ : "Check failed: `%s`%s", \
+ str, __VA_ARGS__), false))
+
+/** Get a string description of the last error. */
+const char *bfs_errstr(void);
+
+/**
+ * Check a condition, logging the current error string on failure.
+ */
+#define bfs_pcheck(...) \
+ bfs_pcheck_(#__VA_ARGS__, __VA_ARGS__, "", "")
+
+#define bfs_pcheck_(str, cond, format, ...) \
+ ((cond) ? true : (bfs_diag( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s%s: %s" \
+ : "Check failed: `%s`%s: %s", \
+ str, __VA_ARGS__, bfs_errstr()), false))
+
+#endif // BFS_TESTS_H
diff --git a/tests/tests.mk b/tests/tests.mk
new file mode 100644
index 0000000..035ca79
--- /dev/null
+++ b/tests/tests.mk
@@ -0,0 +1,13 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that exposes make's job control to tests.sh
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# Turn off implicit rules
+.SUFFIXES:
+
+.DEFAULT::
+ bash -c 'printf . >&$(READY) && read -r -N1 -u$(DONE)'
diff --git a/tests/tests.sh b/tests/tests.sh
index dce421a..3890243 100755
--- a/tests/tests.sh
+++ b/tests/tests.sh
@@ -1,3460 +1,20 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2015-2022 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
set -euP
umask 022
-export LC_ALL=C
-export TZ=UTC0
-
-export ASAN_OPTIONS="abort_on_error=1"
-export LSAN_OPTIONS="abort_on_error=1"
-export MSAN_OPTIONS="abort_on_error=1"
-export TSAN_OPTIONS="abort_on_error=1"
-export UBSAN_OPTIONS="abort_on_error=1"
-
-export LS_COLORS=""
-unset BFS_COLORS
-
-if [ -t 1 ]; then
- BLD=$'\033[01m'
- RED=$'\033[01;31m'
- GRN=$'\033[01;32m'
- YLW=$'\033[01;33m'
- BLU=$'\033[01;34m'
- MAG=$'\033[01;35m'
- CYN=$'\033[01;36m'
- RST=$'\033[0m'
-else
- BLD=
- RED=
- GRN=
- YLW=
- BLU=
- MAG=
- CYN=
- RST=
-fi
-
-UNAME=$(uname)
-
-if command -v capsh &>/dev/null; then
- if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
- if [ -n "$BFS_TRIED_DROP" ]; then
- cat >&2 <<EOF
-${RED}error:${RST} Failed to drop capabilities.
-EOF
-
- exit 1
- fi
-
- cat >&2 <<EOF
-${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
-${BLD}cap_dac_read_search${RST}.
-
-EOF
-
- BFS_TRIED_DROP=y exec capsh \
- --drop=cap_dac_override,cap_dac_read_search \
- --caps=cap_dac_override,cap_dac_read_search-eip \
- -- "$0" "$@"
- fi
-elif [ "$EUID" -eq 0 ]; then
- UNLESS=
- if [ "$UNAME" = "Linux" ]; then
- UNLESS=" unless ${GRN}capsh${RST} is installed"
- fi
-
- cat >&2 <<EOF
-${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
-will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
-EOF
- exit 1
-fi
-
-function usage() {
- local pad=$(printf "%*s" ${#0} "")
- cat <<EOF
-Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLU}--sudo${RST}]
- $pad [${BLU}--stop${RST}] [${BLU}--noclean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
- $pad [${BLD}test_*${RST} [${BLD}test_*${RST} ...]]
-
- ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
- Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST})
-
- ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
- Choose which test cases to run (default: ${BLU}--all${RST})
-
- ${BLU}--sudo${RST}
- Run tests that require root
-
- ${BLU}--stop${RST}
- Stop when the first error occurs
-
- ${BLU}--noclean${RST}
- Keep the test directories around after the run
-
- ${BLU}--update${RST}
- Update the expected outputs for the test cases
-
- ${BLU}--verbose${RST}=${BLD}commands${RST}
- Log the commands that get executed
- ${BLU}--verbose${RST}=${BLD}errors${RST}
- Don't redirect standard error
- ${BLU}--verbose${RST}=${BLD}skipped${RST}
- Log which tests get skipped
- ${BLU}--verbose${RST}=${BLD}tests${RST}
- Log all tests that get run
- ${BLU}--verbose${RST}
- Log everything
-
- ${BLU}--help${RST}
- This message
-
- ${BLD}test_*${RST}
- Select individual test cases to run
-EOF
-}
-
-DEFAULT=yes
-POSIX=
-BSD=
-GNU=
-ALL=
-SUDO=
-STOP=
-CLEAN=yes
-UPDATE=
-VERBOSE_COMMANDS=
-VERBOSE_ERRORS=
-VERBOSE_SKIPPED=
-VERBOSE_TESTS=
-EXPLICIT=
-
-enabled_tests=()
-
-for arg; do
- case "$arg" in
- --bfs=*)
- BFS="${arg#*=}"
- ;;
- --posix)
- DEFAULT=
- POSIX=yes
- ;;
- --bsd)
- DEFAULT=
- POSIX=yes
- BSD=yes
- ;;
- --gnu)
- DEFAULT=
- POSIX=yes
- GNU=yes
- ;;
- --all)
- DEFAULT=
- POSIX=yes
- BSD=yes
- GNU=yes
- ALL=yes
- ;;
- --sudo)
- SUDO=yes
- ;;
- --stop)
- STOP=yes
- ;;
- --noclean)
- CLEAN=
- ;;
- --update)
- UPDATE=yes
- ;;
- --verbose=commands)
- VERBOSE_COMMANDS=yes
- ;;
- --verbose=errors)
- VERBOSE_ERRORS=yes
- ;;
- --verbose=skipped)
- VERBOSE_SKIPPED=yes
- ;;
- --verbose=tests)
- VERBOSE_SKIPPED=yes
- VERBOSE_TESTS=yes
- ;;
- --verbose)
- VERBOSE_COMMANDS=yes
- VERBOSE_ERRORS=yes
- VERBOSE_SKIPPED=yes
- VERBOSE_TESTS=yes
- ;;
- --help)
- usage
- exit 0
- ;;
- test_*)
- EXPLICIT=yes
- SUDO=yes
- enabled_tests+=("$arg")
- ;;
- *)
- printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
- usage >&2
- exit 1
- ;;
- esac
-done
-
-posix_tests=(
- # General parsing
- test_basic
-
- test_parens
- test_bang
- test_implicit_and
- test_a
- test_o
-
- test_weird_names
-
- test_incomplete
- test_missing_paren
- test_extra_paren
-
- # Flags
-
- test_H
- test_H_slash
- test_H_broken
- test_H_notdir
- test_H_loops
-
- test_L
- test_L_broken
- test_L_notdir
- test_L_loops
-
- test_flag_weird_names
- test_flag_comma
-
- # Primaries
-
- test_depth
- test_depth_slash
- test_depth_error
- test_L_depth
-
- test_exec
- test_exec_nopath
- test_exec_plus
- test_exec_plus_status
- test_exec_plus_semicolon
-
- test_group_name
- test_group_id
- test_group_nogroup
-
- test_links
- test_links_plus
- test_links_minus
-
- test_name
- test_name_root
- test_name_root_depth
- test_name_trailing_slash
- test_name_star_star
- test_name_character_class
- test_name_bracket
- test_name_backslash
- test_name_double_backslash
-
- test_newer
- test_newer_link
-
- test_nogroup
- test_nogroup_ulimit
-
- test_nouser
- test_nouser_ulimit
-
- test_ok_stdin
- test_ok_plus_semicolon
-
- test_path
-
- test_perm_000
- test_perm_000_minus
- test_perm_222
- test_perm_222_minus
- test_perm_644
- test_perm_644_minus
- test_perm_symbolic
- test_perm_symbolic_minus
- test_perm_leading_plus_symbolic_minus
- test_permcopy
- test_perm_setid
- test_perm_sticky
-
- test_prune
- test_prune_file
- test_prune_or_print
- test_not_prune
-
- test_size
- test_size_plus
- test_size_bytes
-
- test_type_d
- test_type_f
- test_type_l
- test_H_type_l
- test_L_type_l
- test_type_bind_mount
-
- test_user_name
- test_user_id
- test_user_nouser
-
- test_xdev
- test_L_xdev
-
- # Closed file descriptors
- test_closed_stdin
- test_closed_stdout
- test_closed_stderr
-
- # PATH_MAX handling
- test_deep
-
- # Optimizer tests
- test_or_purity
- test_double_negation
- test_de_morgan_not
- test_de_morgan_and
- test_de_morgan_or
- test_data_flow_group
- test_data_flow_user
- test_data_flow_type
- test_data_flow_and_swap
- test_data_flow_or_swap
-)
-
-bsd_tests=(
- # Flags
-
- test_E
-
- test_P
- test_P_slash
-
- test_X
-
- test_d_path
-
- test_f
-
- test_s
-
- test_double_dash
- test_flag_double_dash
-
- # Primaries
-
- test_acl
- test_L_acl
-
- test_anewer
- test_asince
-
- test_delete
- test_delete_many
-
- test_depth_maxdepth_1
- test_depth_maxdepth_2
- test_depth_mindepth_1
- test_depth_mindepth_2
-
- test_depth_n
- test_depth_n_plus
- test_depth_n_minus
- test_depth_depth_n
- test_depth_depth_n_plus
- test_depth_depth_n_minus
- test_depth_overflow
- test_data_flow_depth
-
- test_exec_substring
-
- test_execdir_pwd
- test_execdir_slash
- test_execdir_slash_pwd
- test_execdir_slashes
- test_execdir_ulimit
-
- test_exit
- test_exit_no_implicit_print
-
- test_flags
-
- test_follow
-
- test_gid_name
-
- test_ilname
- test_L_ilname
-
- test_iname
-
- test_inum
- test_inum_mount
- test_inum_bind_mount
-
- test_ipath
-
- test_iregex
-
- test_lname
- test_L_lname
-
- test_ls
- test_L_ls
-
- test_maxdepth
-
- test_mindepth
-
- test_mnewer
- test_H_mnewer
-
- test_mount
- test_L_mount
-
- test_msince
-
- test_mtime_units
-
- test_name_slash
- test_name_slashes
-
- test_H_newer
-
- test_newerma
- test_newermt
- test_newermt_epoch_minus_one
-
- test_ok_stdin
- test_ok_closed_stdin
-
- test_okdir_stdin
- test_okdir_closed_stdin
-
- test_perm_000_plus
- test_perm_222_plus
- test_perm_644_plus
-
- test_printx
-
- test_quit
- test_quit_child
- test_quit_depth
- test_quit_depth_child
- test_quit_after_print
- test_quit_before_print
- test_quit_implicit_print
-
- test_rm
-
- test_regex
- test_regex_parens
-
- test_samefile
- test_samefile_symlink
- test_H_samefile_symlink
- test_L_samefile_symlink
- test_samefile_broken
- test_H_samefile_broken
- test_L_samefile_broken
- test_samefile_notdir
- test_H_samefile_notdir
- test_L_samefile_notdir
-
- test_size_T
- test_size_big
-
- test_uid_name
-
- test_xattr
- test_L_xattr
-
- test_xattrname
- test_L_xattrname
-
- # Optimizer tests
- test_data_flow_sparse
-)
-
-gnu_tests=(
- # General parsing
-
- test_not
- test_and
- test_or
- test_comma
- test_precedence
-
- test_follow_comma
-
- # Flags
-
- test_P
- test_P_slash
-
- test_L_loops_continue
-
- test_double_dash
- test_flag_double_dash
-
- # Primaries
-
- test_anewer
-
- test_path_d
-
- test_daystart
- test_daystart_twice
-
- test_delete
- test_delete_many
- test_L_delete
-
- test_depth_mindepth_1
- test_depth_mindepth_2
- test_depth_maxdepth_1
- test_depth_maxdepth_2
-
- test_empty
- test_empty_special
-
- test_exec_nothing
- test_exec_substring
- test_exec_flush
- test_exec_flush_fail
- test_exec_plus_flush
- test_exec_plus_flush_fail
-
- test_execdir
- test_execdir_substring
- test_execdir_plus_semicolon
- test_execdir_pwd
- test_execdir_slash
- test_execdir_slash_pwd
- test_execdir_slashes
- test_execdir_ulimit
-
- test_executable
-
- test_false
-
- test_files0_from_file
- test_files0_from_stdin
- test_files0_from_none
- test_files0_from_empty
- test_files0_from_nowhere
- test_files0_from_nothing
- test_files0_from_ok
-
- test_fls
-
- test_follow
-
- test_fprint
- test_fprint_duplicate
- test_fprint_error
- test_fprint_noerror
- test_fprint_noarg
- test_fprint_nonexistent
- test_fprint_truncate
-
- test_fprint0
-
- test_fprintf
- test_fprintf_nofile
- test_fprintf_noformat
-
- test_fstype
-
- test_gid
- test_gid_plus
- test_gid_plus_plus
- test_gid_minus
- test_gid_minus_plus
-
- test_ignore_readdir_race
- test_ignore_readdir_race_root
- test_ignore_readdir_race_notdir
-
- test_ilname
- test_L_ilname
-
- test_iname
-
- test_inum
- test_inum_mount
- test_inum_bind_mount
- test_inum_automount
-
- test_ipath
-
- test_iregex
-
- test_iwholename
-
- test_lname
- test_L_lname
-
- test_ls
- test_L_ls
-
- test_maxdepth
-
- test_mindepth
-
- test_mount
- test_L_mount
-
- test_name_slash
- test_name_slashes
-
- test_H_newer
-
- test_newerma
- test_newermt
- test_newermt_epoch_minus_one
-
- test_ok_closed_stdin
- test_ok_nothing
-
- test_okdir_closed_stdin
- test_okdir_plus_semicolon
-
- test_perm_000_slash
- test_perm_222_slash
- test_perm_644_slash
- test_perm_symbolic_slash
- test_perm_leading_plus_symbolic_slash
-
- test_print_error
-
- test_print0
-
- test_printf
- test_printf_empty
- test_printf_slash
- test_printf_slashes
- test_printf_trailing_slash
- test_printf_trailing_slashes
- test_printf_flags
- test_printf_types
- test_printf_escapes
- test_printf_times
- test_printf_leak
- test_printf_nul
- test_printf_Y_error
- test_printf_H
- test_printf_u_g_ulimit
- test_printf_l_nonlink
-
- test_quit
- test_quit_child
- test_quit_depth
- test_quit_depth_child
- test_quit_after_print
- test_quit_before_print
-
- test_readable
-
- test_regex
- test_regex_parens
- test_regex_error
- test_regex_invalid_utf8
-
- test_regextype_posix_basic
- test_regextype_posix_extended
- test_regextype_ed
- test_regextype_emacs
- test_regextype_grep
- test_regextype_sed
-
- test_samefile
- test_samefile_symlink
- test_H_samefile_symlink
- test_L_samefile_symlink
- test_samefile_broken
- test_H_samefile_broken
- test_L_samefile_broken
- test_samefile_notdir
- test_H_samefile_notdir
- test_L_samefile_notdir
-
- test_size_big
-
- test_true
-
- test_uid
- test_uid_plus
- test_uid_plus_plus
- test_uid_minus
- test_uid_minus_plus
-
- test_wholename
-
- test_writable
-
- test_xtype_l
- test_xtype_f
- test_L_xtype_l
- test_L_xtype_f
- test_xtype_bind_mount
-
- # Optimizer tests
- test_and_purity
- test_not_reachability
- test_comma_reachability
- test_and_false_or_true
- test_comma_redundant_true
- test_comma_redundant_false
-)
-
-bfs_tests=(
- # General parsing
- test_path_flag_expr
- test_path_expr_flag
- test_flag_expr_path
- test_expr_flag_path
- test_expr_path_flag
-
- test_unexpected_operator
- test_and_incomplete
- test_or_incomplete
- test_comma_incomplete
-
- test_typo
-
- # Flags
-
- test_D_multi
- test_D_all
-
- test_O0
- test_O1
- test_O2
- test_O3
- test_Ofast
-
- test_S_bfs
- test_S_dfs
- test_S_ids
-
- # Special forms
-
- test_exclude_name
- test_exclude_depth
- test_exclude_mindepth
- test_exclude_print
- test_exclude_exclude
-
- # Primaries
-
- test_capable
- test_L_capable
-
- test_color
- test_color_L
- test_color_rs_lc_rc_ec
- test_color_escapes
- test_color_nul
- test_color_ln_target
- test_color_L_ln_target
- test_color_mh
- test_color_mh0
- test_color_or
- test_color_mi
- test_color_or_mi
- test_color_or_mi0
- test_color_or0_mi
- test_color_or0_mi0
- test_color_su_sg0
- test_color_su0_sg
- test_color_su0_sg0
- test_color_st_tw_ow0
- test_color_st_tw0_ow
- test_color_st_tw0_ow0
- test_color_st0_tw_ow
- test_color_st0_tw_ow0
- test_color_st0_tw0_ow
- test_color_st0_tw0_ow0
- test_color_ext
- test_color_ext0
- test_color_ext_override
- test_color_ext_underride
- test_color_missing_colon
- test_color_no_stat
- test_color_L_no_stat
- test_color_star
- test_color_ls
-
- test_exec_flush_fprint
- test_exec_flush_fprint_fail
-
- test_execdir_plus
-
- test_fprint_duplicate_stdout
- test_fprint_error_stdout
- test_fprint_error_stderr
-
- test_help
-
- test_hidden
- test_hidden_root
-
- test_links_noarg
- test_links_empty
- test_links_negative
- test_links_invalid
-
- test_newerma_nonexistent
- test_newermt_invalid
- test_newermq
- test_newerqm
-
- test_nohidden
- test_nohidden_depth
-
- test_perm_symbolic_trailing_comma
- test_perm_symbolic_double_comma
- test_perm_symbolic_missing_action
- test_perm_leading_plus_symbolic
-
- test_printf_w
- test_printf_incomplete_escape
- test_printf_invalid_escape
- test_printf_incomplete_format
- test_printf_invalid_format
- test_printf_duplicate_flag
- test_printf_must_be_numeric
- test_printf_color
- test_printf_everything
-
- test_type_multi
-
- test_unique
- test_unique_depth
- test_L_unique
- test_L_unique_loops
- test_L_unique_depth
-
- test_version
-
- test_warn
- test_nowarn
-
- test_xtype_multi
-
- # Optimizer tests
- test_data_flow_hidden
- test_xtype_reorder
- test_xtype_depth
-
- # PATH_MAX handling
- test_deep_strict
-
- # Error handling
- test_stderr_fails_silently
- test_stderr_fails_loudly
-)
-
-if [ "$DEFAULT" ]; then
- POSIX=yes
- BSD=yes
- GNU=yes
- ALL=yes
-fi
-
-if [ ! "$EXPLICIT" ]; then
- [ "$POSIX" ] && enabled_tests+=("${posix_tests[@]}")
- [ "$BSD" ] && enabled_tests+=("${bsd_tests[@]}")
- [ "$GNU" ] && enabled_tests+=("${gnu_tests[@]}")
- [ "$ALL" ] && enabled_tests+=("${bfs_tests[@]}")
-fi
-
-eval "enabled_tests=($(printf '%q\n' "${enabled_tests[@]}" | sort -u))"
-
-function _realpath() {
- (
- cd "$(dirname -- "$1")"
- echo "$PWD/$(basename -- "$1")"
- )
-}
-
-TESTS=$(_realpath "$(dirname -- "${BASH_SOURCE[0]}")")
-
-if [ "${BUILDDIR-}" ]; then
- BIN=$(_realpath "$BUILDDIR/bin")
-else
- BIN=$(_realpath "$TESTS/../bin")
-fi
-
-# Try to resolve the path to $BFS before we cd, while also supporting
-# --bfs="./bin/bfs -S ids"
-read -a BFS <<<"${BFS:-$BIN/bfs}"
-BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
-
-# The temporary directory that will hold our test data
-TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
-chown "$(id -u):$(id -g)" "$TMP"
-
-# Clean up temporary directories on exit
-function cleanup() {
- # Don't force rm to deal with long paths
- for dir in "$TMP"/deep/*/*; do
- if [ -d "$dir" ]; then
- (cd "$dir" && rm -rf *)
- fi
- done
-
- # In case a test left anything weird in scratch/
- if [ -e "$TMP"/scratch ]; then
- chmod -R +rX "$TMP"/scratch
- fi
-
- rm -rf "$TMP"
-}
-
-if [ "$CLEAN" ]; then
- trap cleanup EXIT
-else
- echo "Test files saved to $TMP"
-fi
-
-# Install a file, creating any parent directories
-function installp() {
- local target="${@: -1}"
- mkdir -p "${target%/*}"
- install "$@"
-}
-
-# Prefer GNU touch to work around https://apple.stackexchange.com/a/425730/397839
-if command -v gtouch &>/dev/null; then
- TOUCH=gtouch
-else
- TOUCH=touch
-fi
-
-# Like a mythical touch -p
-function touchp() {
- for arg; do
- installp -m644 /dev/null "$arg"
- done
-}
-
-# Creates a simple file+directory structure for tests
-function make_basic() {
- touchp "$1/a"
- touchp "$1/b"
- touchp "$1/c/d"
- touchp "$1/e/f"
- mkdir -p "$1/g/h"
- mkdir -p "$1/i"
- touchp "$1/j/foo"
- touchp "$1/k/foo/bar"
- touchp "$1/l/foo/bar/baz"
- echo baz >"$1/l/foo/bar/baz"
-}
-make_basic "$TMP/basic"
-
-# Creates a file+directory structure with various permissions for tests
-function make_perms() {
- installp -m000 /dev/null "$1/0"
- installp -m444 /dev/null "$1/r"
- installp -m222 /dev/null "$1/w"
- installp -m644 /dev/null "$1/rw"
- installp -m555 /dev/null "$1/rx"
- installp -m311 /dev/null "$1/wx"
- installp -m755 /dev/null "$1/rwx"
-}
-make_perms "$TMP/perms"
-
-# Creates a file+directory structure with various symbolic and hard links
-function make_links() {
- touchp "$1/file"
- ln -s file "$1/symlink"
- ln "$1/file" "$1/hardlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- mkdir -p "$1/deeply/nested/dir"
- touchp "$1/deeply/nested/file"
- ln -s file "$1/deeply/nested/link"
- ln -s nowhere "$1/deeply/nested/broken"
- ln -s deeply/nested "$1/skip"
-}
-make_links "$TMP/links"
-
-# Creates a file+directory structure with symbolic link loops
-function make_loops() {
- touchp "$1/file"
- ln -s file "$1/symlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- ln -s loop "$1/loop"
- mkdir -p "$1/deeply/nested/dir"
- ln -s ../../deeply "$1/deeply/nested/loop"
- ln -s deeply/nested/loop/nested "$1/skip"
-}
-make_loops "$TMP/loops"
-
-# Creates a file+directory structure with varying timestamps
-function make_times() {
- mkdir -p "$1"
- $TOUCH -t 199112140000 "$1/a"
- $TOUCH -t 199112140001 "$1/b"
- $TOUCH -t 199112140002 "$1/c"
- ln -s a "$1/l"
- $TOUCH -h -t 199112140003 "$1/l"
- $TOUCH -t 199112140004 "$1"
-}
-make_times "$TMP/times"
-
-# Creates a file+directory structure with various weird file/directory names
-function make_weirdnames() {
- touchp "$1/-/a"
- touchp "$1/(/b"
- touchp "$1/(-/c"
- touchp "$1/!/d"
- touchp "$1/!-/e"
- touchp "$1/,/f"
- touchp "$1/)/g"
- touchp "$1/.../h"
- touchp "$1/\\/i"
- touchp "$1/ /j"
- touchp "$1/[/k"
-}
-make_weirdnames "$TMP/weirdnames"
-
-# Creates a very deep directory structure for testing PATH_MAX handling
-function make_deep() {
- mkdir -p "$1"
-
- # $name will be 255 characters, aka _XOPEN_NAME_MAX
- local name="0123456789ABCDEF"
- name="${name}${name}${name}${name}"
- name="${name}${name}${name}${name}"
- name="${name:0:255}"
-
- # 4 * 256 - 1 == 1023
- local names="$name/$name/$name/$name"
-
- for i in {0..9} A B C D E F; do
- (
- mkdir "$1/$i"
- cd "$1/$i"
-
- # 4 * 1024 == 4096 == PATH_MAX
- for _ in {1..4}; do
- mkdir -p "$names"
- cd "$names"
- done
-
- $TOUCH "$name"
- )
- done
-}
-make_deep "$TMP/deep"
-
-# Creates a directory structure with many different types, and therefore colors
-function make_rainbow() {
- touchp "$1/file.txt"
- touchp "$1/file.dat"
- touchp "$1/star".{gz,tar,tar.gz}
- ln -s file.txt "$1/link.txt"
- touchp "$1/mh1"
- ln "$1/mh1" "$1/mh2"
- mkfifo "$1/pipe"
- # TODO: block
- ln -s /dev/null "$1/chardev_link"
- ln -s nowhere "$1/broken"
- "$BIN/tests/mksock" "$1/socket"
- touchp "$1"/s{u,g,ug}id
- chmod u+s "$1"/su{,g}id
- chmod g+s "$1"/s{u,}gid
- mkdir "$1/ow" "$1"/sticky{,_ow}
- chmod o+w "$1"/*ow
- chmod +t "$1"/sticky*
- touchp "$1"/exec.sh
- chmod +x "$1"/exec.sh
-}
-make_rainbow "$TMP/rainbow"
-
-# Creates a scratch directory that tests can modify
-function make_scratch() {
- mkdir -p "$1"
-}
-make_scratch "$TMP/scratch"
-
-# Close stdin so bfs doesn't think we're interactive
-exec </dev/null
-
-if [ "$VERBOSE_COMMANDS" ]; then
- # dup stdout for verbose logging even when redirected
- exec 3>&1
-fi
-
-function bfs_verbose() {
- if [ "$VERBOSE_COMMANDS" ]; then
- if [ -t 3 ]; then
- printf "${GRN}%q${RST} " "${BFS[@]}" >&3
-
- local expr_started=
- for arg; do
- if [[ $arg == -[A-Z]* ]]; then
- printf "${CYN}%q${RST} " "$arg" >&3
- elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then
- expr_started=yes
- printf "${RED}%q${RST} " "$arg" >&3
- elif [[ $expr_started && $arg == [\),] ]]; then
- printf "${RED}%q${RST} " "$arg" >&3
- elif [[ $arg == -?* ]]; then
- expr_started=yes
- printf "${BLU}%q${RST} " "$arg" >&3
- elif [ "$expr_started" ]; then
- printf "${BLD}%q${RST} " "$arg" >&3
- else
- printf "${MAG}%q${RST} " "$arg" >&3
- fi
- done
- else
- printf '%q ' "${BFS[@]}" "$@" >&3
- fi
- printf '\n' >&3
- fi
-}
-
-function invoke_bfs() {
- bfs_verbose "$@"
- "${BFS[@]}" "$@"
-}
-
-# Expect a command to fail, but not crash
-function fail() {
- "$@"
- local STATUS="$?"
-
- if ((STATUS > 125)); then
- exit "$STATUS"
- elif ((STATUS > 0)); then
- return 0
- else
- return 1
- fi
-}
-
-# Detect colored diff support
-if [ -t 2 ] && diff --color=always /dev/null /dev/null 2>/dev/null; then
- DIFF="diff --color=always"
-else
- DIFF="diff"
-fi
-
-# Return value when bfs fails
-EX_BFS=10
-# Return value when a difference is detected
-EX_DIFF=20
-# Return value when a test is skipped
-EX_SKIP=77
-
-function sort_output() {
- sort -o "$OUT" "$OUT"
-}
-
-function diff_output() {
- local GOLD="$TESTS/$TEST.out"
-
- if [ "$UPDATE" ]; then
- cp "$OUT" "$GOLD"
- else
- $DIFF -u "$GOLD" "$OUT" >&2
- fi
-}
-
-function bfs_diff() (
- bfs_verbose "$@"
-
- # Close the dup()'d stdout to make sure we have enough fd's for the process
- # substitution, even with low ulimit -n
- exec 3>&-
-
- "${BFS[@]}" "$@" | sort >"$OUT"
- local STATUS="${PIPESTATUS[0]}"
-
- diff_output || return $EX_DIFF
-
- if [ "$STATUS" -eq 0 ]; then
- return 0
- else
- return $EX_BFS
- fi
-)
-
-function skip() {
- exit $EX_SKIP
-}
-
-function skip_if() {
- if "$@"; then
- skip
- fi
-}
-
-function skip_unless() {
- skip_if fail "$@"
-}
-
-function closefrom() {
- if [ -d /proc/self/fd ]; then
- local fds=/proc/self/fd
- else
- local fds=/dev/fd
- fi
-
- for fd in "$fds"/*; do
- if [ ! -e "$fd" ]; then
- continue
- fi
-
- local fd="${fd##*/}"
- if [ "$fd" -ge "$1" ]; then
- eval "exec ${fd}<&-"
- fi
- done
-}
-
-function inum() {
- ls -id "$@" | awk '{ print $1 }'
-}
-
-
-cd "$TMP"
-set +e
-
-# Test cases
-
-function test_basic() {
- bfs_diff basic
-}
-
-function test_type_d() {
- bfs_diff basic -type d
-}
-
-function test_type_f() {
- bfs_diff basic -type f
-}
-
-function test_type_l() {
- bfs_diff links/skip -type l
-}
-
-function test_H_type_l() {
- bfs_diff -H links/skip -type l
-}
-
-function test_L_type_l() {
- bfs_diff -L links/skip -type l
-}
-
-function test_type_multi() {
- bfs_diff links -type f,d,c
-}
-
-function test_mindepth() {
- bfs_diff basic -mindepth 1
-}
-
-function test_maxdepth() {
- bfs_diff basic -maxdepth 1
-}
-
-function test_depth() {
- bfs_diff basic -depth
-}
-
-function test_depth_slash() {
- bfs_diff basic/ -depth
-}
-
-function test_depth_mindepth_1() {
- bfs_diff basic -mindepth 1 -depth
-}
-
-function test_depth_mindepth_2() {
- bfs_diff basic -mindepth 2 -depth
-}
-
-function test_depth_maxdepth_1() {
- bfs_diff basic -maxdepth 1 -depth
-}
-
-function test_depth_maxdepth_2() {
- bfs_diff basic -maxdepth 2 -depth
-}
-
-function test_depth_error() {
- rm -rf scratch/*
- touchp scratch/foo/bar
- chmod a-r scratch/foo
-
- bfs_diff scratch -depth
- local ret=$?
-
- chmod +r scratch/foo
- rm -rf scratch/*
-
- [ $ret -eq $EX_BFS ]
-}
-
-function test_name() {
- bfs_diff basic -name '*f*'
-}
-
-function test_name_root() {
- bfs_diff basic/a -name a
-}
-
-function test_name_root_depth() {
- bfs_diff basic/g -depth -name g
-}
-
-function test_name_trailing_slash() {
- bfs_diff basic/g/ -name g
-}
-
-function test_name_slash() {
- bfs_diff / -maxdepth 0 -name /
-}
-
-function test_name_slashes() {
- bfs_diff /// -maxdepth 0 -name /
-}
-
-function test_name_star_star() {
- bfs_diff basic -name '**f**'
-}
-
-function test_name_character_class() {
- bfs_diff basic -name '[e-g][!a-n][!p-z]'
-}
-
-function test_name_bracket() {
- # fnmatch() is broken on macOS
- skip_if test "$UNAME" = "Darwin"
-
- # An unclosed [ should be matched literally
- bfs_diff weirdnames -name '['
-}
-
-function test_name_backslash() {
- # An unescaped \ doesn't match
- bfs_diff weirdnames -name '\'
-}
-
-function test_name_double_backslash() {
- # An escaped \\ matches
- bfs_diff weirdnames -name '\\'
-}
-
-function test_path() {
- bfs_diff basic -path 'basic/*f*'
-}
-
-function test_wholename() {
- bfs_diff basic -wholename 'basic/*f*'
-}
-
-function test_true() {
- bfs_diff basic -true
-}
-
-function test_false() {
- bfs_diff basic -false
-}
-
-function test_executable() {
- bfs_diff perms -executable
-}
-
-function test_readable() {
- bfs_diff perms -readable
-}
-
-function test_writable() {
- bfs_diff perms -writable
-}
-
-function test_empty() {
- bfs_diff basic -empty
-}
-
-function test_empty_special() {
- bfs_diff rainbow -empty
-}
-
-function test_gid() {
- bfs_diff basic -gid "$(id -g)"
-}
-
-function test_gid_plus() {
- skip_if test "$(id -g)" -eq 0
- bfs_diff basic -gid +0
-}
-
-function test_gid_plus_plus() {
- skip_if test "$(id -g)" -eq 0
- bfs_diff basic -gid ++0
-}
-
-function test_gid_minus() {
- bfs_diff basic -gid "-$(($(id -g) + 1))"
-}
-
-function test_gid_minus_plus() {
- bfs_diff basic -gid "-+$(($(id -g) + 1))"
-}
-
-function test_uid() {
- bfs_diff basic -uid "$(id -u)"
-}
-
-function test_uid_plus() {
- skip_if test "$(id -u)" -eq 0
- bfs_diff basic -uid +0
-}
-
-function test_uid_plus_plus() {
- skip_if test "$(id -u)" -eq 0
- bfs_diff basic -uid ++0
-}
-
-function test_uid_minus() {
- bfs_diff basic -uid "-$(($(id -u) + 1))"
-}
-
-function test_uid_minus_plus() {
- bfs_diff basic -uid "-+$(($(id -u) + 1))"
-}
-
-function test_newer() {
- bfs_diff times -newer times/a
-}
-
-function test_newer_link() {
- bfs_diff times -newer times/l
-}
-
-function test_anewer() {
- bfs_diff times -anewer times/a
-}
-
-function test_asince() {
- bfs_diff times -asince 1991-12-14T00:01
-}
-
-function test_links() {
- bfs_diff links -type f -links 2
-}
-
-function test_links_plus() {
- bfs_diff links -type f -links +1
-}
-
-function test_links_minus() {
- bfs_diff links -type f -links -2
-}
-
-function test_links_noarg() {
- fail invoke_bfs links -links
-}
-
-function test_links_empty() {
- fail invoke_bfs links -links ''
-}
-
-function test_links_negative() {
- fail invoke_bfs links -links +-1
-}
-
-function test_links_invalid() {
- fail invoke_bfs links -links ASDF
-}
-
-function test_P() {
- bfs_diff -P links/deeply/nested/dir
-}
-
-function test_P_slash() {
- bfs_diff -P links/deeply/nested/dir/
-}
-
-function test_H() {
- bfs_diff -H links/deeply/nested/dir
-}
-
-function test_H_slash() {
- bfs_diff -H links/deeply/nested/dir/
-}
-
-function test_H_broken() {
- bfs_diff -H links/broken
-}
-
-function test_H_notdir() {
- bfs_diff -H links/notdir
-}
-
-function test_H_newer() {
- bfs_diff -H times -newer times/l
-}
-
-function test_H_loops() {
- bfs_diff -H loops/deeply/nested/loop
-}
-
-function test_L() {
- bfs_diff -L links
-}
-
-function test_L_broken() {
- bfs_diff -H links/broken
-}
-
-function test_L_notdir() {
- bfs_diff -H links/notdir
-}
-
-function test_L_loops() {
- # POSIX says it's okay to either stop or keep going on seeing a filesystem
- # loop, as long as a diagnostic is printed
- local errors=$(invoke_bfs -L loops 2>&1 >/dev/null)
- [ -n "$errors" ]
-}
-
-function test_L_loops_continue() {
- bfs_diff -L loops
- [ $? -eq $EX_BFS ]
-}
-
-function test_X() {
- bfs_diff -X weirdnames
- [ $? -eq $EX_BFS ]
-}
-
-function test_follow() {
- bfs_diff links -follow
-}
-
-function test_L_depth() {
- bfs_diff -L links -depth
-}
-
-function test_samefile() {
- bfs_diff links -samefile links/file
-}
-
-function test_samefile_symlink() {
- bfs_diff links -samefile links/symlink
-}
-
-function test_H_samefile_symlink() {
- bfs_diff -H links -samefile links/symlink
-}
-
-function test_L_samefile_symlink() {
- bfs_diff -L links -samefile links/symlink
-}
-
-function test_samefile_broken() {
- bfs_diff links -samefile links/broken
-}
-
-function test_H_samefile_broken() {
- bfs_diff -H links -samefile links/broken
-}
-
-function test_L_samefile_broken() {
- bfs_diff -L links -samefile links/broken
-}
-
-function test_samefile_notdir() {
- bfs_diff links -samefile links/notdir
-}
-
-function test_H_samefile_notdir() {
- bfs_diff -H links -samefile links/notdir
-}
-
-function test_L_samefile_notdir() {
- bfs_diff -L links -samefile links/notdir
-}
-
-function test_xtype_l() {
- bfs_diff links -xtype l
-}
-
-function test_xtype_f() {
- bfs_diff links -xtype f
-}
-
-function test_L_xtype_l() {
- bfs_diff -L links -xtype l
-}
-
-function test_L_xtype_f() {
- bfs_diff -L links -xtype f
-}
-
-function test_xtype_multi() {
- bfs_diff links -xtype f,d,c
-}
-
-function test_xtype_reorder() {
- # Make sure -xtype is not reordered in front of anything -- if -xtype runs
- # before -links 100, it will report an ELOOP error
- bfs_diff loops -links 100 -xtype l
- invoke_bfs loops -links 100 -xtype l
-}
-
-function test_xtype_depth() {
- # Make sure -xtype is considered side-effecting for facts_when_impure
- fail invoke_bfs loops -xtype l -depth 100
-}
-
-function test_iname() {
- skip_unless invoke_bfs -quit -iname PATTERN
- bfs_diff basic -iname '*F*'
-}
-
-function test_ipath() {
- skip_unless invoke_bfs -quit -ipath PATTERN
- bfs_diff basic -ipath 'basic/*F*'
-}
-
-function test_iwholename() {
- skip_unless invoke_bfs -quit -iwholename PATTERN
- bfs_diff basic -iwholename 'basic/*F*'
-}
-
-function test_lname() {
- bfs_diff links -lname '[aq]'
-}
-
-function test_ilname() {
- skip_unless invoke_bfs -quit -ilname PATTERN
- bfs_diff links -ilname '[AQ]'
-}
-
-function test_L_lname() {
- bfs_diff -L links -lname '[aq]'
-}
-
-function test_L_ilname() {
- skip_unless invoke_bfs -quit -ilname PATTERN
- bfs_diff -L links -ilname '[AQ]'
-}
-
-function test_user_name() {
- bfs_diff basic -user "$(id -un)"
-}
-
-function test_user_id() {
- bfs_diff basic -user "$(id -u)"
-}
-
-function test_user_nouser() {
- # Regression test: this was wrongly optimized to -false
- bfs_diff basic -user "$(id -u)" \! -nouser
-}
-
-function test_group_name() {
- bfs_diff basic -group "$(id -gn)"
-}
-
-function test_group_id() {
- bfs_diff basic -group "$(id -g)"
-}
-
-function test_group_nogroup() {
- # Regression test: this was wrongly optimized to -false
- bfs_diff basic -group "$(id -g)" \! -nogroup
-}
-
-function test_daystart() {
- bfs_diff basic -daystart -mtime 0
-}
-
-function test_daystart_twice() {
- bfs_diff basic -daystart -daystart -mtime 0
-}
-
-function test_newerma() {
- bfs_diff times -newerma times/a
-}
-
-function test_newermt() {
- bfs_diff times -newermt 1991-12-14T00:01
-}
-
-function test_newermt_epoch_minus_one() {
- bfs_diff times -newermt 1969-12-31T23:59:59Z
-}
-
-function test_newermt_invalid() {
- fail invoke_bfs times -newermt not_a_date_time
-}
-
-function test_newerma_nonexistent() {
- fail invoke_bfs times -newerma basic/nonexistent
-}
-
-function test_newermq() {
- fail invoke_bfs times -newermq times/a
-}
-
-function test_newerqm() {
- fail invoke_bfs times -newerqm times/a
-}
-
-function test_size() {
- bfs_diff basic -type f -size 0
-}
-
-function test_size_plus() {
- bfs_diff basic -type f -size +0
-}
-
-function test_size_bytes() {
- bfs_diff basic -type f -size +0c
-}
-
-function test_size_big() {
- bfs_diff basic -size 9223372036854775807
-}
-
-function test_exec() {
- bfs_diff basic -exec echo {} \;
-}
-
-function test_exec_nopath() {
- (
- unset PATH
- invoke_bfs basic -exec echo {} \; >"$OUT"
- )
-
- sort_output
- diff_output
-}
-
-function test_exec_nothing() {
- # Regression test: don't segfault on missing command
- fail invoke_bfs basic -exec \;
-}
-
-function test_exec_plus() {
- bfs_diff basic -exec "$TESTS/sort-args.sh" {} +
-}
-
-function test_exec_plus_status() {
- # -exec ... {} + should always return true, but if the command fails, bfs
- # should exit with a non-zero status
- bfs_diff basic -exec false {} + -print
- (($? == EX_BFS))
-}
-
-function test_exec_plus_semicolon() {
- # POSIX says:
- # Only a <plus-sign> that immediately follows an argument containing only the two characters "{}"
- # shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be
- # treated as special.
- bfs_diff basic -exec echo foo {} bar + baz \;
-}
-
-function test_exec_substring() {
- bfs_diff basic -exec echo '-{}-' \;
-}
-
-function test_exec_flush() {
- # IO streams should be flushed before executing programs
- bfs_diff basic -print0 -exec echo found \;
-}
-
-function test_exec_flush_fail() {
- # Failure to flush streams before exec should be caught
- skip_unless test -e /dev/full
- fail invoke_bfs basic -print0 -exec true \; >/dev/full
-}
-
-function test_exec_flush_fprint() {
- # Even non-stdstreams should be flushed
- bfs_diff basic/a -fprint scratch/foo -exec cat scratch/foo \;
-}
-
-function test_exec_flush_fprint_fail() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic/a -fprint /dev/full -exec true \;
-}
-
-function test_exec_plus_flush() {
- bfs_diff basic/a -print0 -exec echo found {} +
-}
-
-function test_exec_plus_flush_fail() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full
-}
-
-function test_execdir() {
- bfs_diff basic -execdir echo {} \;
-}
-
-function test_execdir_plus() {
- local tree=$(invoke_bfs -D tree 2>&1 -quit)
-
- if [[ "$tree" == *"-S dfs"* ]]; then
- skip
- fi
-
- bfs_diff basic -execdir "$TESTS/sort-args.sh" {} +
-}
-
-function test_execdir_substring() {
- bfs_diff basic -execdir echo '-{}-' \;
-}
-
-function test_execdir_plus_semicolon() {
- bfs_diff basic -execdir echo foo {} bar + baz \;
-}
-
-function test_execdir_pwd() {
- local TMP_REAL=$(cd "$TMP" && pwd)
- local OFFSET=$((${#TMP_REAL} + 2))
- bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \;
-}
-
-function test_execdir_slash() {
- # Don't prepend ./ for absolute paths in -execdir
- bfs_diff / -maxdepth 0 -execdir echo {} \;
-}
-
-function test_execdir_slash_pwd() {
- bfs_diff / -maxdepth 0 -execdir pwd \;
-}
-
-function test_execdir_slashes() {
- bfs_diff /// -maxdepth 0 -execdir echo {} \;
-}
-
-function test_execdir_ulimit() {
- rm -rf scratch/*
- mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
- mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
-
- closefrom 4
- ulimit -n 13
- bfs_diff scratch -execdir echo {} \;
-}
-
-function test_weird_names() {
- cd weirdnames
- bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
-}
-
-function test_flag_weird_names() {
- cd weirdnames
- bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
-}
-
-function test_flag_comma() {
- # , is a filename until a non-flag is seen
- cd weirdnames
- bfs_diff -L ',' -print
-}
-
-function test_follow_comma() {
- # , is an operator after a non-flag is seen
- cd weirdnames
- bfs_diff -follow ',' -print
-}
-
-function test_fprint() {
- invoke_bfs basic -fprint "$OUT"
- sort_output
- diff_output
-}
-
-function test_fprint_duplicate() {
- touchp scratch/$TEST.out
- ln scratch/$TEST.out scratch/$TEST.hard
- ln -s $TEST.out scratch/$TEST.soft
-
- invoke_bfs basic -fprint scratch/$TEST.out -fprint scratch/$TEST.hard -fprint scratch/$TEST.soft
- sort scratch/$TEST.out >"$OUT"
- diff_output
-}
-
-function test_fprint_duplicate_stdout() {
- invoke_bfs basic -fprint "$OUT" -print >"$OUT"
- sort_output
- diff_output
-}
-
-function test_fprint_noarg() {
- fail invoke_bfs basic -fprint
-}
-
-function test_fprint_nonexistent() {
- fail invoke_bfs basic -fprint scratch/nonexistent/path
-}
-
-function test_fprint_truncate() {
- printf "basic\nbasic\n" >"$OUT"
-
- invoke_bfs basic -maxdepth 0 -fprint "$OUT"
- sort_output
- diff_output
-}
-
-function test_double_dash() {
- cd basic
- bfs_diff -- . -type f
-}
-
-function test_flag_double_dash() {
- cd basic
- bfs_diff -L -- . -type f
-}
-
-function test_ignore_readdir_race() {
- rm -rf scratch/*
- $TOUCH scratch/{foo,bar}
-
- # -links 1 forces a stat() call, which will fail for the second file
- invoke_bfs scratch -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \;
-}
-
-function test_ignore_readdir_race_root() {
- # Make sure -ignore_readdir_race doesn't suppress ENOENT at the root
- fail invoke_bfs basic/nonexistent -ignore_readdir_race
-}
-
-function test_ignore_readdir_race_notdir() {
- # Check -ignore_readdir_race handling when a directory is replaced with a file
- rm -rf scratch/*
- touchp scratch/foo/bar
-
- invoke_bfs scratch -mindepth 1 -ignore_readdir_race -execdir rm -r {} \; -execdir $TOUCH {} \;
-}
-
-function test_perm_000() {
- bfs_diff perms -perm 000
-}
-
-function test_perm_000_minus() {
- bfs_diff perms -perm -000
-}
-
-function test_perm_000_slash() {
- bfs_diff perms -perm /000
-}
-
-function test_perm_000_plus() {
- bfs_diff perms -perm +000
-}
-
-function test_perm_222() {
- bfs_diff perms -perm 222
-}
-
-function test_perm_222_minus() {
- bfs_diff perms -perm -222
-}
-
-function test_perm_222_slash() {
- bfs_diff perms -perm /222
-}
-
-function test_perm_222_plus() {
- bfs_diff perms -perm +222
-}
-
-function test_perm_644() {
- bfs_diff perms -perm 644
-}
-
-function test_perm_644_minus() {
- bfs_diff perms -perm -644
-}
-
-function test_perm_644_slash() {
- bfs_diff perms -perm /644
-}
-
-function test_perm_644_plus() {
- bfs_diff perms -perm +644
-}
-
-function test_perm_symbolic() {
- bfs_diff perms -perm a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_minus() {
- bfs_diff perms -perm -a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_slash() {
- bfs_diff perms -perm /a+r,u=wX,g+wX-w
-}
-
-function test_perm_symbolic_trailing_comma() {
- fail invoke_bfs perms -perm a+r,
-}
-
-function test_perm_symbolic_double_comma() {
- fail invoke_bfs perms -perm a+r,,u+w
-}
-
-function test_perm_symbolic_missing_action() {
- fail invoke_bfs perms -perm a
-}
-
-function test_perm_leading_plus_symbolic() {
- bfs_diff perms -perm +rwx
-}
-
-function test_perm_leading_plus_symbolic_minus() {
- bfs_diff perms -perm -+rwx
-}
-
-function test_perm_leading_plus_symbolic_slash() {
- bfs_diff perms -perm /+rwx
-}
-
-function test_permcopy() {
- bfs_diff perms -perm u+rw,g+u-w,o=g
-}
-
-function test_perm_setid() {
- bfs_diff rainbow -perm -u+s -o -perm -g+s
-}
-
-function test_perm_sticky() {
- bfs_diff rainbow -perm -a+t
-}
-
-function test_prune() {
- bfs_diff basic -name foo -prune
-}
-
-function test_prune_file() {
- bfs_diff basic -print -name '?' -prune
-}
-
-function test_prune_or_print() {
- bfs_diff basic -name foo -prune -o -print
-}
-
-function test_not_prune() {
- bfs_diff basic \! \( -name foo -prune \)
-}
-
-function test_ok_nothing() {
- # Regression test: don't segfault on missing command
- fail invoke_bfs basic -ok \;
-}
-
-function test_ok_stdin() {
- # -ok should *not* close stdin
- # See https://savannah.gnu.org/bugs/?24561
- yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
-}
-
-function test_okdir_stdin() {
- # -okdir should *not* close stdin
- yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
-}
-
-function test_ok_plus_semicolon() {
- yes | bfs_diff basic -ok echo {} + \;
-}
-
-function test_okdir_plus_semicolon() {
- yes | bfs_diff basic -okdir echo {} + \;
-}
-
-function test_delete() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
-
- # Don't try to delete '.'
- (cd scratch && invoke_bfs . -delete)
-
- bfs_diff scratch
-}
-
-function test_delete_many() {
- # Test for https://github.com/tavianator/bfs/issues/67
-
- rm -rf scratch/*
- mkdir scratch/foo
- $TOUCH scratch/foo/{1..256}
-
- invoke_bfs scratch/foo -delete
- bfs_diff scratch
-}
-
-function test_L_delete() {
- rm -rf scratch/*
- mkdir scratch/foo
- mkdir scratch/bar
- ln -s ../foo scratch/bar/baz
-
- # Don't try to rmdir() a symlink
- invoke_bfs -L scratch/bar -delete || return 1
-
- bfs_diff scratch
-}
-
-function test_rm() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
-
- (cd scratch && invoke_bfs . -rm)
-
- bfs_diff scratch
-}
-
-function test_regex() {
- bfs_diff basic -regex 'basic/./.'
-}
-
-function test_iregex() {
- bfs_diff basic -iregex 'basic/[A-Z]/[a-z]'
-}
-
-function test_regex_parens() {
- cd weirdnames
- bfs_diff . -regex '\./\((\)'
-}
-
-function test_regex_error() {
- fail invoke_bfs basic -regex '['
-}
-
-function test_regex_invalid_utf8() {
- rm -rf scratch/*
-
- # Incomplete UTF-8 sequences
- skip_unless touch scratch/$'\xC3'
- skip_unless touch scratch/$'\xE2\x84'
- skip_unless touch scratch/$'\xF0\x9F\x92'
-
- bfs_diff scratch -regex 'scratch/..'
-}
-
-function test_E() {
- cd weirdnames
- bfs_diff -E . -regex '\./(\()'
-}
-
-function test_regextype_posix_basic() {
- cd weirdnames
- bfs_diff -regextype posix-basic -regex '\./\((\)'
-}
-
-function test_regextype_posix_extended() {
- cd weirdnames
- bfs_diff -regextype posix-extended -regex '\./(\()'
-}
-
-function test_regextype_ed() {
- cd weirdnames
- bfs_diff -regextype ed -regex '\./\((\)'
-}
-
-function test_regextype_emacs() {
- skip_unless invoke_bfs -regextype emacs -quit
-
- bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)'
-}
-
-function test_regextype_grep() {
- skip_unless invoke_bfs -regextype grep -quit
-
- bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?'
-}
-
-function test_regextype_sed() {
- cd weirdnames
- bfs_diff -regextype sed -regex '\./\((\)'
-}
-
-function test_d_path() {
- bfs_diff -d basic
-}
-
-function test_path_d() {
- bfs_diff basic -d
-}
-
-function test_f() {
- cd weirdnames
- bfs_diff -f '-' -f '('
-}
-
-function test_s() {
- invoke_bfs -s weirdnames -maxdepth 1 >"$OUT"
- diff_output
-}
-
-function test_hidden() {
- bfs_diff weirdnames -hidden
-}
-
-function test_hidden_root() {
- cd weirdnames
- bfs_diff . ./. ... ./... .../.. -hidden
-}
-
-function test_nohidden() {
- bfs_diff weirdnames -nohidden
-}
-
-function test_nohidden_depth() {
- bfs_diff weirdnames -depth -nohidden
-}
-
-function test_depth_n() {
- bfs_diff basic -depth 2
-}
-
-function test_depth_n_plus() {
- bfs_diff basic -depth +2
-}
-
-function test_depth_n_minus() {
- bfs_diff basic -depth -2
-}
-
-function test_depth_depth_n() {
- bfs_diff basic -depth -depth 2
-}
-
-function test_depth_depth_n_plus() {
- bfs_diff basic -depth -depth +2
-}
-
-function test_depth_depth_n_minus() {
- bfs_diff basic -depth -depth -2
-}
-
-function test_depth_overflow() {
- bfs_diff basic -depth -4294967296
-}
-
-function test_gid_name() {
- bfs_diff basic -gid "$(id -gn)"
-}
-
-function test_uid_name() {
- bfs_diff basic -uid "$(id -un)"
-}
-
-function test_mnewer() {
- bfs_diff times -mnewer times/a
-}
-
-function test_H_mnewer() {
- bfs_diff -H times -mnewer times/l
-}
-
-function test_msince() {
- bfs_diff times -msince 1991-12-14T00:01
-}
-
-function test_mtime_units() {
- bfs_diff times -mtime +500w400d300h200m100s
-}
-
-function test_size_T() {
- bfs_diff basic -type f -size 1T
-}
-
-function test_quit() {
- bfs_diff basic/g -print -name g -quit
-}
-
-function test_quit_child() {
- bfs_diff basic/g -print -name h -quit
-}
-
-function test_quit_depth() {
- bfs_diff basic/g -depth -print -name g -quit
-}
-
-function test_quit_depth_child() {
- bfs_diff basic/g -depth -print -name h -quit
-}
-
-function test_quit_after_print() {
- bfs_diff basic basic -print -quit
-}
-
-function test_quit_before_print() {
- bfs_diff basic basic -quit -print
-}
-
-function test_quit_implicit_print() {
- bfs_diff basic -name basic -o -quit
-}
-
-function test_inum() {
- bfs_diff basic -inum "$(inum basic/k/foo/bar)"
-}
-
-function test_nogroup() {
- bfs_diff basic -nogroup
-}
-
-function test_nogroup_ulimit() {
- closefrom 4
- ulimit -n 16
- bfs_diff deep -nogroup
-}
-
-function test_nouser() {
- bfs_diff basic -nouser
-}
-
-function test_nouser_ulimit() {
- closefrom 4
- ulimit -n 16
- bfs_diff deep -nouser
-}
-
-function test_ls() {
- invoke_bfs rainbow -ls >scratch/test_ls.out
-}
-
-function test_L_ls() {
- invoke_bfs -L rainbow -ls >scratch/test_L_ls.out
-}
-
-function test_fls() {
- invoke_bfs rainbow -fls scratch/test_fls.out
-}
-
-function test_printf() {
- bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
-}
-
-function test_printf_empty() {
- bfs_diff basic -printf ''
-}
-
-function test_printf_slash() {
- bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n'
-}
-
-function test_printf_slashes() {
- bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n'
-}
-
-function test_printf_trailing_slash() {
- bfs_diff basic/ -printf '(%h)/(%f)\n'
-}
-
-function test_printf_trailing_slashes() {
- bfs_diff basic/// -printf '(%h)/(%f)\n'
-}
-
-function test_printf_flags() {
- bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n'
-}
-
-function test_printf_types() {
- bfs_diff loops -printf '(%p) (%l) %y %Y\n'
-}
-
-function test_printf_escapes() {
- bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo'
-}
-
-function test_printf_times() {
- bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %T@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n'
-}
-
-function test_printf_leak() {
- # Memory leak regression test
- bfs_diff basic -maxdepth 0 -printf '%p'
-}
-
-function test_printf_nul() {
- # NUL byte regression test
- bfs_diff basic -maxdepth 0 -printf '%h\0%f\n'
-}
-
-function test_printf_w() {
- # Birth times may not be supported, so just check that %w/%W/%B can be parsed
- bfs_diff times -false -printf '%w %WY %BY\n'
-}
-
-function test_printf_Y_error() {
- rm -rf scratch/*
- mkdir scratch/foo
- chmod -x scratch/foo
- ln -s foo/bar scratch/bar
-
- bfs_diff scratch -printf '(%p) (%l) %y %Y\n'
- local ret=$?
-
- chmod +x scratch/foo
- rm -rf scratch/*
-
- [ $ret -eq $EX_BFS ]
-}
-
-function test_printf_H() {
- bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n'
-}
-
-function test_printf_u_g_ulimit() {
- closefrom 4
- ulimit -n 16
- [ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
-}
-
-function test_printf_l_nonlink() {
- bfs_diff links -printf '| %26p -> %-26l |\n'
-}
-
-function test_printf_incomplete_escape() {
- fail invoke_bfs basic -printf '\'
-}
-
-function test_printf_invalid_escape() {
- fail invoke_bfs basic -printf '\!'
-}
-
-function test_printf_incomplete_format() {
- fail invoke_bfs basic -printf '%'
-}
-
-function test_printf_invalid_format() {
- fail invoke_bfs basic -printf '%!'
-}
-
-function test_printf_duplicate_flag() {
- fail invoke_bfs basic -printf '%--p'
-}
-
-function test_printf_must_be_numeric() {
- fail invoke_bfs basic -printf '%+p'
-}
-
-function test_printf_color() {
- bfs_diff -color -path './rainbow*' -printf '%H %h %f %p %P %l\n'
-}
-
-function test_printf_everything() {
- local everything=(%{a,b,c,d,D,f,F,g,G,h,H,i,k,l,m,M,n,p,P,s,S,t,u,U,y,Y})
- everything+=(%{A,C,T}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
-
- # Check if we have birth times
- if ! fail invoke_bfs basic -printf '%w' -quit >/dev/null; then
- everything+=(%w %{B,W}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
- fi
-
- invoke_bfs rainbow -printf "${everything[*]}\n" >/dev/null
-}
-
-function test_fprintf() {
- invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
- sort_output
- diff_output
-}
-
-function test_fprintf_nofile() {
- fail invoke_bfs basic -fprintf
-}
-
-function test_fprintf_noformat() {
- fail invoke_bfs basic -fprintf /dev/null
-}
-
-function test_fstype() {
- fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n')
- bfs_diff basic -fstype "$fstype"
-}
-
-function test_path_flag_expr() {
- bfs_diff links/skip -H -type l
-}
-
-function test_path_expr_flag() {
- bfs_diff links/skip -type l -H
-}
-
-function test_flag_expr_path() {
- bfs_diff -H -type l links/skip
-}
-
-function test_expr_flag_path() {
- bfs_diff -type l -H links/skip
-}
-
-function test_expr_path_flag() {
- bfs_diff -type l links/skip -H
-}
-
-function test_parens() {
- bfs_diff basic \( -name '*f*' \)
-}
-
-function test_bang() {
- bfs_diff basic \! -name foo
-}
-
-function test_not() {
- bfs_diff basic -not -name foo
-}
-
-function test_implicit_and() {
- bfs_diff basic -name foo -type d
-}
-
-function test_a() {
- bfs_diff basic -name foo -a -type d
-}
-
-function test_and() {
- bfs_diff basic -name foo -and -type d
-}
-
-function test_o() {
- bfs_diff basic -name foo -o -type d
-}
-
-function test_or() {
- bfs_diff basic -name foo -or -type d
-}
-
-function test_comma() {
- bfs_diff basic -name '*f*' -print , -print
-}
-
-function test_precedence() {
- bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print
-}
-
-function test_incomplete() {
- fail invoke_bfs basic \(
-}
-
-function test_missing_paren() {
- fail invoke_bfs basic \( -print
-}
-
-function test_extra_paren() {
- fail invoke_bfs basic -print \)
-}
-
-function test_color() {
- bfs_diff rainbow -color
-}
-
-function test_color_L() {
- bfs_diff -L rainbow -color
-}
-
-function test_color_rs_lc_rc_ec() {
- LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color
-}
-
-function test_color_escapes() {
- LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color
-}
-
-function test_color_nul() {
- LS_COLORS="ec=\33[m\0:" bfs_diff rainbow -color -maxdepth 0
-}
-
-function test_color_ln_target() {
- LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_L_ln_target() {
- LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color
-}
-
-function test_color_mh() {
- LS_COLORS="mh=01:" bfs_diff rainbow -color
-}
-
-function test_color_mh0() {
- LS_COLORS="mh=00:" bfs_diff rainbow -color
-}
-
-function test_color_or() {
- LS_COLORS="or=01:" bfs_diff rainbow -color
-}
-
-function test_color_mi() {
- LS_COLORS="mi=01:" bfs_diff rainbow -color
-}
-
-function test_color_or_mi() {
- LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_or_mi0() {
- LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color
-}
-
-function test_color_or0_mi() {
- LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_or0_mi0() {
- LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color
-}
-
-function test_color_su_sg0() {
- LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color
-}
-
-function test_color_su0_sg() {
- LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color
-}
-
-function test_color_su0_sg0() {
- LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw_ow0() {
- LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw0_ow() {
- LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st_tw0_ow0() {
- LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw_ow() {
- LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw_ow0() {
- LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw0_ow() {
- LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color
-}
-
-function test_color_st0_tw0_ow0() {
- LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color
-}
-
-function test_color_ext() {
- LS_COLORS="*.txt=01:" bfs_diff rainbow -color
-}
-
-function test_color_ext0() {
- LS_COLORS="*.txt=00:" bfs_diff rainbow -color
-}
-
-function test_color_ext_override() {
- LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color
-}
-
-function test_color_ext_underride() {
- LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color
-}
-
-function test_color_missing_colon() {
- LS_COLORS="*.txt=01" bfs_diff rainbow -color
-}
-
-function test_color_no_stat() {
- LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color
-}
-
-function test_color_L_no_stat() {
- LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color
-}
-
-function test_color_star() {
- # Regression test: don't segfault on LS_COLORS="*"
- LS_COLORS="*" bfs_diff rainbow -color
-}
-
-function test_color_ls() {
- rm -rf scratch/*
- touchp scratch/foo/bar/baz
- ln -s foo/bar/baz scratch/link
- ln -s foo/bar/nowhere scratch/broken
- ln -s foo/bar/nowhere/nothing scratch/nested
- ln -s foo/bar/baz/qux scratch/notdir
- ln -s scratch/foo/bar scratch/relative
- mkdir scratch/__bfs__
- ln -s /__bfs__/nowhere scratch/absolute
-
- LS_COLORS="or=01;31:" invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \
- | sed 's/.* -> //' \
- | sort >"$OUT"
-
- diff_output
-}
-
-function test_deep() {
- closefrom 4
-
- ulimit -n 16
- bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
-}
-
-function test_deep_strict() {
- closefrom 4
-
- # Not even enough fds to keep the root open
- ulimit -n 7
- bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
-}
-
-function test_exit() {
- invoke_bfs basic -name foo -exit 42
- if [ $? -ne 42 ]; then
- return 1
- fi
-
- invoke_bfs basic -name qux -exit 42
- if [ $? -ne 0 ]; then
- return 1
- fi
-
- bfs_diff basic/g -print -name g -exit
-}
-
-function test_exit_no_implicit_print() {
- bfs_diff basic -not -name foo -o -exit
-}
-
-function test_printx() {
- bfs_diff weirdnames -printx
-}
-
-function test_and_purity() {
- # Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure
- bfs_diff basic -name nonexistent \( -print , -false \)
-}
-
-function test_or_purity() {
- # Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure
- bfs_diff basic -name '*' -o -print
-}
-
-function test_double_negation() {
- bfs_diff basic \! \! -name 'foo'
-}
-
-function test_not_reachability() {
- bfs_diff basic -print \! -quit -print
-}
-
-function test_comma_reachability() {
- bfs_diff basic -print -quit , -print
-}
-
-function test_de_morgan_not() {
- bfs_diff basic \! \( -name 'foo' -o \! -type f \)
-}
-
-function test_de_morgan_and() {
- bfs_diff basic \( \! -name 'foo' -a \! -type f \)
-}
-
-function test_de_morgan_or() {
- bfs_diff basic \( \! -name 'foo' -o \! -type f \)
-}
-
-function test_and_false_or_true() {
- # Test (-a lhs(always_true) -false) <==> (! lhs),
- # (-o lhs(always_false) -true) <==> (! lhs)
- bfs_diff basic -prune -false -o -true
-}
-
-function test_comma_redundant_true() {
- # Test (, lhs(always_true) -true) <==> lhs
- bfs_diff basic -prune , -true
-}
-
-function test_comma_redundant_false() {
- # Test (, lhs(always_false) -false) <==> lhs
- bfs_diff basic -print -not -prune , -false
-}
-
-function test_data_flow_depth() {
- bfs_diff basic -depth +1 -depth -4
-}
-
-function test_data_flow_group() {
- bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \)
-}
-
-function test_data_flow_user() {
- bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \)
-}
-
-function test_data_flow_hidden() {
- bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \)
-}
-
-function test_data_flow_sparse() {
- bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \)
-}
-
-function test_data_flow_type() {
- bfs_diff basic \! \( -type f -o \! -type f \)
-}
-
-function test_data_flow_and_swap() {
- bfs_diff basic \! -type f -a -type d
-}
-
-function test_data_flow_or_swap() {
- bfs_diff basic \! \( -type f -o \! -type d \)
-}
-
-function test_print_error() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic -maxdepth 0 >/dev/full
-}
-
-function test_fprint_error() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic -maxdepth 0 -fprint /dev/full
-}
-
-function test_fprint_noerror() {
- # Regression test: /dev/full should not fail until actually written to
- skip_unless test -e /dev/full
- invoke_bfs basic -false -fprint /dev/full
-}
-
-function test_fprint_error_stdout() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full
-}
-
-function test_fprint_error_stderr() {
- skip_unless test -e /dev/full
- fail invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full
-}
-
-function test_print0() {
- bfs_diff basic/a basic/b -print0
-}
-
-function test_fprint0() {
- invoke_bfs basic/a basic/b -fprint0 "$OUT"
- diff_output
-}
-
-function test_closed_stdin() {
- bfs_diff basic <&-
-}
-
-function test_ok_closed_stdin() {
- bfs_diff basic -ok echo \; <&-
-}
-
-function test_okdir_closed_stdin() {
- bfs_diff basic -okdir echo {} \; <&-
-}
-
-function test_closed_stdout() {
- fail invoke_bfs basic >&-
-}
-
-function test_closed_stderr() {
- fail invoke_bfs basic >&- 2>&-
-}
-
-function test_unique() {
- bfs_diff links/{file,symlink,hardlink} -unique
-}
-
-function test_unique_depth() {
- bfs_diff basic -unique -depth
-}
-
-function test_L_unique() {
- bfs_diff -L links/{file,symlink,hardlink} -unique
-}
-
-function test_L_unique_loops() {
- bfs_diff -L loops/deeply/nested -unique
-}
-
-function test_L_unique_depth() {
- bfs_diff -L loops/deeply/nested -unique -depth
-}
-
-function test_mount() {
- skip_unless test "$SUDO"
- skip_if test "$UNAME" = "Darwin"
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- $TOUCH scratch/foo/bar scratch/mnt/baz
-
- bfs_diff scratch -mount
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_L_mount() {
- skip_unless test "$SUDO"
- skip_if test "$UNAME" = "Darwin"
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- ln -s ../mnt scratch/foo/bar
- $TOUCH scratch/mnt/baz
- ln -s ../mnt/baz scratch/foo/qux
-
- bfs_diff -L scratch -mount
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_xdev() {
- skip_unless test "$SUDO"
- skip_if test "$UNAME" = "Darwin"
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- $TOUCH scratch/foo/bar scratch/mnt/baz
-
- bfs_diff scratch -xdev
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_L_xdev() {
- skip_unless test "$SUDO"
- skip_if test "$UNAME" = "Darwin"
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
- ln -s ../mnt scratch/foo/bar
- $TOUCH scratch/mnt/baz
- ln -s ../mnt/baz scratch/foo/qux
-
- bfs_diff -L scratch -xdev
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_inum_mount() {
- skip_unless test "$SUDO"
- skip_if test "$UNAME" = "Darwin"
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- sudo mount -t tmpfs tmpfs scratch/mnt
-
- bfs_diff scratch -inum "$(inum scratch/mnt)"
- local ret=$?
-
- sudo umount scratch/mnt
- return $ret
-}
-
-function test_inum_bind_mount() {
- skip_unless test "$SUDO"
- skip_unless test "$UNAME" = "Linux"
-
- rm -rf scratch/*
- $TOUCH scratch/{foo,bar}
- sudo mount --bind scratch/{foo,bar}
-
- bfs_diff scratch -inum "$(inum scratch/bar)"
- local ret=$?
-
- sudo umount scratch/bar
- return $ret
-}
-
-function test_type_bind_mount() {
- skip_unless test "$SUDO"
- skip_unless test "$UNAME" = "Linux"
-
- rm -rf scratch/*
- $TOUCH scratch/{file,null}
- sudo mount --bind /dev/null scratch/null
-
- bfs_diff scratch -type c
- local ret=$?
-
- sudo umount scratch/null
- return $ret
-}
-
-function test_xtype_bind_mount() {
- skip_unless test "$SUDO"
- skip_unless test "$UNAME" = "Linux"
-
- rm -rf scratch/*
- $TOUCH scratch/{file,null}
- sudo mount --bind /dev/null scratch/null
- ln -s /dev/null scratch/link
-
- bfs_diff -L scratch -type c
- local ret=$?
-
- sudo umount scratch/null
- return $ret
-}
-
-function test_inum_automount() {
- # bfs shouldn't trigger automounts unless it descends into them
-
- skip_unless test "$SUDO"
- skip_unless command -v systemd-mount &>/dev/null
-
- rm -rf scratch/*
- mkdir scratch/{foo,mnt}
- skip_unless sudo systemd-mount -A -o bind basic scratch/mnt
-
- local before=$(inum scratch/mnt)
- bfs_diff scratch -inum "$before" -prune
- local ret=$?
- local after=$(inum scratch/mnt)
-
- sudo systemd-umount scratch/mnt
-
- ((ret == 0 && before == after))
-}
-
-function set_acl() {
- case "$UNAME" in
- Darwin)
- chmod +a "$(id -un) allow read,write" "$1"
- ;;
- FreeBSD)
- if [ "$(getconf ACL_NFS4 "$1")" -gt 0 ]; then
- setfacl -m "u:$(id -un):rw::allow" "$1"
- else
- setfacl -m "u:$(id -un):rw" "$1"
- fi
- ;;
- *)
- setfacl -m "u:$(id -un):rw" "$1"
- ;;
- esac
-}
-
-function test_acl() {
- rm -rf scratch/*
-
- skip_unless invoke_bfs scratch -quit -acl
-
- $TOUCH scratch/{normal,acl}
- skip_unless set_acl scratch/acl
- ln -s acl scratch/link
-
- bfs_diff scratch -acl
-}
-
-function test_L_acl() {
- rm -rf scratch/*
-
- skip_unless invoke_bfs scratch -quit -acl
-
- $TOUCH scratch/{normal,acl}
- skip_unless set_acl scratch/acl
- ln -s acl scratch/link
-
- bfs_diff -L scratch -acl
-}
-
-function test_capable() {
- skip_unless test "$SUDO"
- skip_unless test "$UNAME" = "Linux"
-
- rm -rf scratch/*
-
- skip_unless invoke_bfs scratch -quit -capable
-
- $TOUCH scratch/{normal,capable}
- sudo setcap all+ep scratch/capable
- ln -s capable scratch/link
-
- bfs_diff scratch -capable
-}
-
-function test_L_capable() {
- skip_unless test "$SUDO"
- skip_unless test "$UNAME" = "Linux"
-
- rm -rf scratch/*
-
- skip_unless invoke_bfs scratch -quit -capable
-
- $TOUCH scratch/{normal,capable}
- sudo setcap all+ep scratch/capable
- ln -s capable scratch/link
-
- bfs_diff -L scratch -capable
-}
-
-function make_xattrs() {
- rm -rf scratch/*
-
- $TOUCH scratch/{normal,xattr,xattr_2}
- ln -s xattr scratch/link
- ln -s normal scratch/xattr_link
-
- case "$UNAME" in
- Darwin)
- xattr -w bfs_test true scratch/xattr \
- && xattr -w bfs_test_2 true scratch/xattr_2 \
- && xattr -s -w bfs_test true scratch/xattr_link
- ;;
- FreeBSD)
- setextattr user bfs_test true scratch/xattr \
- && setextattr user bfs_test_2 true scratch/xattr_2 \
- && setextattr -h user bfs_test true scratch/xattr_link
- ;;
- *)
- # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
- # namespace, which is writable by root and readable by others
- [ "$SUDO" ] \
- && sudo setfattr -n security.bfs_test scratch/xattr \
- && sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \
- && sudo setfattr -h -n security.bfs_test scratch/xattr_link
- ;;
- esac
-}
-
-function test_xattr() {
- skip_unless invoke_bfs scratch -quit -xattr
- skip_unless make_xattrs
- bfs_diff scratch -xattr
-}
-
-function test_L_xattr() {
- skip_unless invoke_bfs scratch -quit -xattr
- skip_unless make_xattrs
- bfs_diff -L scratch -xattr
-}
-
-function test_xattrname() {
- skip_unless invoke_bfs scratch -quit -xattr
- skip_unless make_xattrs
-
- case "$UNAME" in
- Darwin|FreeBSD)
- bfs_diff scratch -xattrname bfs_test
- ;;
- *)
- bfs_diff scratch -xattrname security.bfs_test
- ;;
- esac
-}
-
-function test_L_xattrname() {
- skip_unless invoke_bfs scratch -quit -xattr
- skip_unless make_xattrs
-
- case "$UNAME" in
- Darwin|FreeBSD)
- bfs_diff -L scratch -xattrname bfs_test
- ;;
- *)
- bfs_diff -L scratch -xattrname security.bfs_test
- ;;
- esac
-}
-
-function test_help() {
- invoke_bfs -help | grep -E '\{...?\}' && return 1
- invoke_bfs -D help | grep -E '\{...?\}' && return 1
- invoke_bfs -S help | grep -E '\{...?\}' && return 1
- invoke_bfs -regextype help | grep -E '\{...?\}' && return 1
-
- return 0
-}
-
-function test_version() {
- invoke_bfs -version >/dev/null
-}
-
-function test_warn() {
- local stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null)
- [ -n "$stderr" ]
-}
-
-function test_nowarn() {
- local stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null)
- [ -z "$stderr" ]
-}
-
-function test_typo() {
- invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null
-}
-
-function test_D_multi() {
- bfs_diff -D opt,tree,unknown basic
-}
-
-function test_D_all() {
- bfs_diff -D all basic
-}
-
-function test_O0() {
- bfs_diff -O0 basic -not \( -type f -not -type f \)
-}
-
-function test_O1() {
- bfs_diff -O1 basic -not \( -type f -not -type f \)
-}
-
-function test_O2() {
- bfs_diff -O2 basic -not \( -type f -not -type f \)
-}
-
-function test_O3() {
- bfs_diff -O3 basic -not \( -type f -not -type f \)
-}
-
-function test_Ofast() {
- bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \)
-}
-
-function test_S() {
- invoke_bfs -S "$1" -s basic >"$OUT"
- diff_output
-}
-
-function test_S_bfs() {
- test_S bfs
-}
-
-function test_S_dfs() {
- test_S dfs
-}
-
-function test_S_ids() {
- test_S ids
-}
-
-function test_exclude_name() {
- bfs_diff basic -exclude -name foo
-}
-
-function test_exclude_depth() {
- bfs_diff basic -depth -exclude -name foo
-}
-
-function test_exclude_mindepth() {
- bfs_diff basic -mindepth 3 -exclude -name foo
-}
-
-function test_exclude_print() {
- fail invoke_bfs basic -exclude -print
-}
-
-function test_exclude_exclude() {
- fail invoke_bfs basic -exclude -exclude -name foo
-}
-
-function test_flags() {
- skip_unless invoke_bfs scratch -quit -flags offline
-
- rm -rf scratch/*
-
- $TOUCH scratch/{foo,bar}
- skip_unless chflags offline scratch/bar
-
- bfs_diff scratch -flags -offline,nohidden
-}
-
-function test_files0_from_file() {
- cd weirdnames
- invoke_bfs -mindepth 1 -fprintf ../scratch/files0.in "%P\0"
- bfs_diff -files0-from ../scratch/files0.in
-}
-
-function test_files0_from_stdin() {
- cd weirdnames
- invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from -
-}
-
-function test_files0_from_none() {
- printf "" | fail invoke_bfs -files0-from -
-}
-
-function test_files0_from_empty() {
- printf "\0" | fail invoke_bfs -files0-from -
-}
-
-function test_files0_from_nowhere() {
- fail invoke_bfs -files0-from
-}
-
-function test_files0_from_nothing() {
- fail invoke_bfs -files0-from basic/nonexistent
-}
-
-function test_files0_from_ok() {
- printf "basic\0" | fail invoke_bfs -files0-from - -ok echo {} \;
-}
-
-function test_stderr_fails_silently() {
- skip_unless test -e /dev/full
- bfs_diff -D all basic 2>/dev/full
-}
-
-function test_stderr_fails_loudly() {
- skip_unless test -e /dev/full
- fail invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full
-}
-
-function test_unexpected_operator() {
- fail invoke_bfs \! -o -print
-}
-
-function test_and_incomplete() {
- fail invoke_bfs -print -a
-}
-
-function test_or_incomplete() {
- fail invoke_bfs -print -o
-}
-
-function test_comma_incomplete() {
- fail invoke_bfs -print ,
-}
-
-BOL='\n'
-EOL='\n'
-
-function update_eol() {
- # Put the cursor at the last column, then write a space so the next
- # character will wrap
- EOL="\\033[${COLUMNS}G "
-}
-
-
-if [ "$VERBOSE_TESTS" ]; then
- BOL=''
-elif [ -t 1 ]; then
- BOL='\r\033[K'
-
- # Workaround for bash 4: checkwinsize is off by default. We can turn it on,
- # but we also have to explicitly trigger a foreground job to finish so that
- # it will update the window size before we use $COLUMNS
- shopt -s checkwinsize
- (:)
-
- update_eol
- trap update_eol WINCH
-fi
-
-passed=0
-failed=0
-skipped=0
-
-for TEST in "${enabled_tests[@]}"; do
- if [[ -t 1 || "$VERBOSE_TESTS" ]]; then
- printf "${BOL}${YLW}%s${RST}${EOL}" "$TEST"
- else
- printf "."
- fi
-
- OUT="$TMP/$TEST.out"
-
- if [ "$VERBOSE_ERRORS" ]; then
- ("$TEST")
- else
- ("$TEST") 2>"$TMP/stderr"
- fi
- status=$?
-
- if ((status == 0)); then
- ((++passed))
- elif ((status == EX_SKIP)); then
- ((++skipped))
- if [ "$VERBOSE_SKIPPED" ]; then
- printf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST"
- fi
- else
- ((++failed))
- [ "$VERBOSE_ERRORS" ] || cat "$TMP/stderr" >&2
- printf "${BOL}${RED}%s failed!${RST}\n" "$TEST"
- [ "$STOP" ] && break
- fi
-done
-
-printf "${BOL}"
-
-if ((passed > 0)); then
- printf "${GRN}tests passed: %d${RST}\n" "$passed"
-fi
-if ((skipped > 0)); then
- printf "${CYN}tests skipped: %s${RST}\n" "$skipped"
-fi
-if ((failed > 0)); then
- printf "${RED}tests failed: %s${RST}\n" "$failed"
- exit 1
-fi
+TESTS="$(dirname -- "${BASH_SOURCE[0]}")"
+. "$TESTS/util.sh"
+. "$TESTS/color.sh"
+. "$TESTS/stddirs.sh"
+. "$TESTS/getopts.sh"
+. "$TESTS/run.sh"
+
+stdenv
+drop_root "$@"
+parse_args "$@"
+make_stddirs
+run_tests
diff --git a/tests/trie.c b/tests/trie.c
index 0158fd8..4667322 100644
--- a/tests/trie.c
+++ b/tests/trie.c
@@ -1,23 +1,10 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#undef NDEBUG
-
-#include "../src/trie.h"
-#include <assert.h>
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "trie.h"
+#include "diag.h"
#include <stdlib.h>
#include <string.h>
@@ -50,14 +37,16 @@ const char *keys[] = {
">>>",
};
-const size_t nkeys = sizeof(keys) / sizeof(keys[0]);
+const size_t nkeys = countof(keys);
+
+bool check_trie(void) {
+ bool ret = true;
-int main(void) {
struct trie trie;
trie_init(&trie);
for (size_t i = 0; i < nkeys; ++i) {
- assert(!trie_find_str(&trie, keys[i]));
+ ret &= bfs_check(!trie_find_str(&trie, keys[i]));
const char *prefix = NULL;
for (size_t j = 0; j < i; ++j) {
@@ -70,27 +59,38 @@ int main(void) {
struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]);
if (prefix) {
- assert(leaf);
- assert(strcmp(prefix, leaf->key) == 0);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(prefix, leaf->key) == 0);
} else {
- assert(!leaf);
+ ret &= bfs_check(!leaf);
}
leaf = trie_insert_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(keys[i], leaf->key) == 0);
+ ret &= bfs_check(leaf->length == strlen(keys[i]) + 1);
+ }
+
+ {
+ size_t i = 0;
+ for_trie (leaf, &trie) {
+ ret &= bfs_check(leaf == trie_find_str(&trie, keys[i]));
+ ret &= bfs_check(!leaf->prev || leaf->prev->next == leaf);
+ ret &= bfs_check(!leaf->next || leaf->next->prev == leaf);
+ ++i;
+ }
+ ret &= bfs_check(i == nkeys);
}
for (size_t i = 0; i < nkeys; ++i) {
struct trie_leaf *leaf = trie_find_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(keys[i], leaf->key) == 0);
+ ret &= bfs_check(leaf->length == strlen(keys[i]) + 1);
trie_remove(&trie, leaf);
leaf = trie_find_str(&trie, keys[i]);
- assert(!leaf);
+ ret &= bfs_check(!leaf);
const char *postfix = NULL;
for (size_t j = i + 1; j < nkeys; ++j) {
@@ -103,31 +103,35 @@ int main(void) {
leaf = trie_find_postfix(&trie, keys[i]);
if (postfix) {
- assert(leaf);
- assert(strcmp(postfix, leaf->key) == 0);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(postfix, leaf->key) == 0);
} else {
- assert(!leaf);
+ ret &= bfs_check(!leaf);
}
}
+ for_trie (leaf, &trie) {
+ ret &= bfs_check(false, "trie should be empty");
+ }
+
// This tests the "jump" node handling on 32-bit platforms
size_t longsize = 1 << 20;
char *longstr = malloc(longsize);
- assert(longstr);
+ bfs_verify(longstr);
memset(longstr, 0xAC, longsize);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr + longsize/2, 0xAB, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr + longsize / 2, 0xAB, longsize / 2);
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr, 0xAA, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr, 0xAA, longsize / 2);
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
free(longstr);
trie_destroy(&trie);
- return EXIT_SUCCESS;
+ return ret;
}
diff --git a/tests/util.sh b/tests/util.sh
new file mode 100644
index 0000000..7dba9fb
--- /dev/null
+++ b/tests/util.sh
@@ -0,0 +1,185 @@
+#!/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
+TESTS=$(_realpath "$TESTS")
+if [ "${BUILDDIR-}" ]; then
+ BIN=$(_realpath "$BUILDDIR/bin")
+else
+ BIN=$(_realpath "$TESTS/../bin")
+fi
+MKSOCK="$BIN/tests/mksock"
+XTOUCH="$BIN/tests/xtouch"
+UNAME=$(uname)
+
+# Standardize the environment
+stdenv() {
+ export LC_ALL=C
+ export TZ=UTC0
+
+ local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0"
+ export ASAN_OPTIONS="$SAN_OPTIONS"
+ export LSAN_OPTIONS="$SAN_OPTIONS"
+ export MSAN_OPTIONS="$SAN_OPTIONS"
+ export TSAN_OPTIONS="$SAN_OPTIONS"
+ export UBSAN_OPTIONS="$SAN_OPTIONS"
+
+ export LS_COLORS=""
+ unset BFS_COLORS
+
+ if [ "$UNAME" = Darwin ]; then
+ # ASan on macOS likes to report
+ #
+ # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
+ #
+ # to syslog, which as a side effect opens a socket which might take the
+ # place of one of the standard streams if the process is launched with
+ # it closed. This environment variable avoids the message.
+ export MallocNanoZone=0
+ fi
+
+ # Count the inherited FDs
+ if [ -d /proc/self/fd ]; then
+ local fds=/proc/self/fd
+ else
+ local fds=/dev/fd
+ fi
+ # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting
+ # internal bash fds that are not exposed to spawned processes
+ NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l)
+ NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3))
+
+ # Close stdin so bfs doesn't think we're interactive
+ # dup() the standard fds for logging even when redirected
+ exec </dev/null {DUPOUT}>&1 {DUPERR}>&2
+}
+
+# Drop root priviliges or bail
+drop_root() {
+ if command -v capsh &>/dev/null; then
+ if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
+ if [ -n "${BFS_TRIED_DROP:-}" ]; then
+ color cat >&2 <<EOF
+${RED}error:${RST} Failed to drop capabilities.
+EOF
+
+ exit 1
+ fi
+
+ color cat >&2 <<EOF
+${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
+${BLD}cap_dac_read_search${RST}.
+
+EOF
+
+ BFS_TRIED_DROP=y exec capsh \
+ --drop=cap_dac_override,cap_dac_read_search \
+ --caps=cap_dac_override,cap_dac_read_search-eip \
+ -- "$0" "$@"
+ fi
+ elif ((EUID == 0)); then
+ UNLESS=
+ if [ "$UNAME" = "Linux" ]; then
+ UNLESS=" unless ${GRN}capsh${RST} is installed"
+ fi
+
+ color cat >&2 <<EOF
+${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
+will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
+EOF
+ exit 1
+ fi
+}
+
+## Debugging
+
+# Get the bash call stack
+callers() {
+ local frame=0
+ while caller $frame; do
+ ((++frame))
+ done
+}
+
+# Print a message including path, line number, and command
+debug() {
+ local file="$1"
+ local line="$2"
+ local msg="$3"
+ local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
+ file="${file/#*\/tests\//tests/}"
+
+ color printf "${BLD}%s:%d:${RST} %s\n %s\n" "$file" "$line" "$msg" "$cmd"
+}
+
+## Deferred cleanup
+
+# Quote a command safely for eval
+quote() {
+ printf '%q' "$1"
+ shift
+ if (($# > 0)); then
+ printf ' %q' "$@"
+ fi
+}
+
+DEFER_LEVEL=-1
+
+# Run a command when this (sub)shell exits
+defer() {
+ # Check if the EXIT trap is already set
+ if ((DEFER_LEVEL != BASH_SUBSHELL)); then
+ DEFER_LEVEL=$BASH_SUBSHELL
+ DEFER_CMDS=()
+ DEFER_LINES=()
+ DEFER_FILES=()
+ trap pop_defers EXIT
+ fi
+
+ DEFER_CMDS+=("$(quote "$@")")
+
+ local line file
+ read -r line file < <(caller)
+ DEFER_LINES+=("$line")
+ DEFER_FILES+=("$file")
+}
+
+# Pop a single command from the defer stack and run it
+pop_defer() {
+ local cmd="${DEFER_CMDS[-1]}"
+ local file="${DEFER_FILES[-1]}"
+ local line="${DEFER_LINES[-1]}"
+ unset "DEFER_CMDS[-1]"
+ unset "DEFER_FILES[-1]"
+ unset "DEFER_LINES[-1]"
+
+ local ret=0
+ eval "$cmd" || ret=$?
+
+ if ((ret != 0)); then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ fi
+
+ return $ret
+}
+
+# Run all deferred commands
+pop_defers() {
+ local ret=0
+
+ while ((${#DEFER_CMDS[@]} > 0)); do
+ pop_defer || ret=$?
+ done
+
+ return $ret
+}
diff --git a/tests/xspawn.c b/tests/xspawn.c
new file mode 100644
index 0000000..7362aa5
--- /dev/null
+++ b/tests/xspawn.c
@@ -0,0 +1,206 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "dstring.h"
+#include "xspawn.h"
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+
+/** Duplicate the current environment. */
+static char **envdup(void) {
+ extern char **environ;
+
+ char **envp = NULL;
+ size_t envc = 0;
+
+ for (char **var = environ; ; ++var) {
+ char *copy = NULL;
+ if (*var) {
+ copy = strdup(*var);
+ if (!copy) {
+ goto fail;
+ }
+ }
+
+ char **dest = RESERVE(char *, &envp, &envc);
+ if (!dest) {
+ free(copy);
+ goto fail;
+ }
+ *dest = copy;
+
+ if (!*var) {
+ break;
+ }
+ }
+
+ return envp;
+
+fail:
+ for (size_t i = 0; i < envc; ++i) {
+ free(envp[i]);
+ }
+ free(envp);
+ return NULL;
+}
+
+/** Check that we resolve executables in $PATH correctly. */
+static bool check_use_path(bool use_posix) {
+ bool ret = true;
+
+ struct bfs_spawn spawn;
+ ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ if (!ret) {
+ goto out;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ const char *builddir = getenv("BUILDDIR");
+ dchar *bin = dstrprintf("%s/bin", builddir ? builddir : ".");
+ ret &= bfs_pcheck(bin, "dstrprintf()");
+ if (!ret) {
+ goto destroy;
+ }
+
+ ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, bin, O_RDONLY | O_DIRECTORY, 0) == 0);
+ ret &= bfs_pcheck(bfs_spawn_adddup2(&spawn, 10, 11) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 10) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addfchdir(&spawn, 11) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 11) == 0);
+ if (!ret) {
+ goto bin;
+ }
+
+ // Check that $PATH is resolved in the parent's environment
+ char **envp;
+ ret &= bfs_pcheck(envp = envdup());
+ if (!ret) {
+ goto bin;
+ }
+
+ // Check that $PATH is resolved after the file actions
+ char *old_path = getenv("PATH");
+ dchar *new_path = NULL;
+ if (old_path) {
+ ret &= bfs_pcheck(old_path = strdup(old_path));
+ if (!ret) {
+ goto env;
+ }
+ new_path = dstrprintf("tests:%s", old_path);
+ } else {
+ new_path = dstrdup("tests");
+ }
+ ret &= bfs_check(new_path);
+ if (!ret) {
+ goto path;
+ }
+
+ ret &= bfs_pcheck(setenv("PATH", new_path, true) == 0);
+ if (!ret) {
+ goto path;
+ }
+
+ char *argv[] = {"xspawnee", old_path, NULL};
+ pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp);
+ ret &= bfs_pcheck(pid >= 0, "bfs_spawn()");
+ if (!ret) {
+ goto unset;
+ }
+
+ int wstatus;
+ ret &= bfs_pcheck(xwaitpid(pid, &wstatus, 0) == pid)
+ && bfs_check(WIFEXITED(wstatus));
+ if (ret) {
+ int wexit = WEXITSTATUS(wstatus);
+ ret &= bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit);
+ }
+
+unset:
+ if (old_path) {
+ ret &= bfs_pcheck(setenv("PATH", old_path, true) == 0);
+ } else {
+ ret &= bfs_pcheck(unsetenv("PATH") == 0);
+ }
+path:
+ dstrfree(new_path);
+ free(old_path);
+env:
+ for (char **var = envp; *var; ++var) {
+ free(*var);
+ }
+ free(envp);
+bin:
+ dstrfree(bin);
+destroy:
+ ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+out:
+ return ret;
+}
+
+/** Check path resolution of non-existent executables. */
+static bool check_enoent(bool use_posix) {
+ bool ret = true;
+
+ struct bfs_spawn spawn;
+ ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ if (!ret) {
+ goto out;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ char *argv[] = {"eW6f5RM9Qi", NULL};
+ pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL);
+ ret &= bfs_pcheck(pid < 0 && errno == ENOENT, "bfs_spawn()");
+
+ ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+out:
+ return ret;
+}
+
+static bool check_resolve(void) {
+ bool ret = true;
+ char *exe;
+
+ exe = bfs_spawn_resolve("sh");
+ ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')");
+ free(exe);
+
+ exe = bfs_spawn_resolve("/bin/sh");
+ ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0);
+ free(exe);
+
+ exe = bfs_spawn_resolve("bin/tests/xspawnee");
+ ret &= bfs_pcheck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ free(exe);
+
+ ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+
+ return ret;
+}
+
+bool check_xspawn(void) {
+ bool ret = true;
+
+ ret &= check_use_path(true);
+ ret &= check_use_path(false);
+
+ ret &= check_enoent(true);
+ ret &= check_enoent(false);
+
+ ret &= check_resolve();
+
+ return ret;
+}
diff --git a/tests/xspawnee.c b/tests/xspawnee.c
new file mode 100644
index 0000000..b0a76ca
--- /dev/null
+++ b/tests/xspawnee.c
@@ -0,0 +1,17 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+#include <string.h>
+
+/** Child binary for bfs_spawn() tests. */
+int main(int argc, char *argv[]) {
+ if (argc >= 2) {
+ const char *path = getenv("PATH");
+ if (!path || strcmp(path, argv[1]) != 0) {
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/xtime.c b/tests/xtime.c
new file mode 100644
index 0000000..a7c63d2
--- /dev/null
+++ b/tests/xtime.c
@@ -0,0 +1,191 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "xtime.h"
+#include "bfstd.h"
+#include "diag.h"
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <time.h>
+
+static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
+ return tma->tm_year == tmb->tm_year
+ && tma->tm_mon == tmb->tm_mon
+ && tma->tm_mday == tmb->tm_mday
+ && tma->tm_hour == tmb->tm_hour
+ && tma->tm_min == tmb->tm_min
+ && tma->tm_sec == tmb->tm_sec
+ && tma->tm_wday == tmb->tm_wday
+ && tma->tm_yday == tmb->tm_yday
+ && tma->tm_isdst == tmb->tm_isdst;
+}
+
+/** Check one xgetdate() result. */
+static bool check_one_xgetdate(const char *str, int error, time_t expected) {
+ struct timespec ts;
+ int ret = xgetdate(str, &ts);
+
+ if (error) {
+ return bfs_pcheck(ret == -1 && errno == error, "xgetdate('%s')", str);
+ } else {
+ return bfs_pcheck(ret == 0, "xgetdate('%s')", str)
+ && bfs_check(ts.tv_sec == expected && ts.tv_nsec == 0,
+ "xgetdate('%s'): %jd.%09jd != %jd",
+ str, (intmax_t)ts.tv_sec, (intmax_t)ts.tv_nsec, (intmax_t)expected);
+ }
+}
+
+/** xgetdate() tests. */
+static bool check_xgetdate(void) {
+ bool ret = true;
+
+ ret &= check_one_xgetdate("", EINVAL, 0);
+ ret &= check_one_xgetdate("????", EINVAL, 0);
+ ret &= check_one_xgetdate("1991", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14", 0, 692668800);
+ ret &= check_one_xgetdate("1991-12-14-", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T10:??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10:11", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472);
+ ret &= check_one_xgetdate("1991-12-14T10Z", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T03-07", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472);
+ ret &= check_one_xgetdate("19911214 031112-0700", 0, 692705472);;
+
+ return ret;
+}
+
+#define TM_FORMAT "%04d-%02d-%02d %02d:%02d:%02d (%d/7, %d/365%s)"
+
+#define TM_PRINTF(tm) \
+ (1900 + (tm).tm_year), (tm).tm_mon, (tm).tm_mday, \
+ (tm).tm_hour, (tm).tm_min, (tm).tm_sec, \
+ ((tm).tm_wday + 1), ((tm).tm_yday + 1), \
+ ((tm).tm_isdst ? ((tm).tm_isdst < 0 ? ", DST?" : ", DST") : "")
+
+/** Check one xmktime() result. */
+static bool check_one_xmktime(time_t expected) {
+ struct tm tm;
+ if (!localtime_r(&expected, &tm)) {
+ bfs_diag("localtime_r(%jd): %s", (intmax_t)expected, xstrerror(errno));
+ return false;
+ }
+
+ time_t actual;
+ return bfs_pcheck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm))
+ && bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected);
+}
+
+/** xmktime() tests. */
+static bool check_xmktime(void) {
+ bool ret = true;
+
+ for (time_t time = -10; time <= 10; ++time) {
+ ret &= check_one_xmktime(time);
+ }
+
+ // Attempt to trigger overflow (but don't test for it, since it's not mandatory)
+ struct tm tm = {
+ .tm_year = INT_MAX,
+ .tm_mon = INT_MAX,
+ .tm_mday = INT_MAX,
+ .tm_hour = INT_MAX,
+ .tm_min = INT_MAX,
+ .tm_sec = INT_MAX,
+ .tm_isdst = -1,
+ };
+ time_t time;
+ xmktime(&tm, &time);
+
+ return ret;
+}
+
+/** Check one xtimegm() result. */
+static bool check_one_xtimegm(const struct tm *tm) {
+ struct tm tma = *tm, tmb = *tm;
+ time_t ta, tb;
+ ta = mktime(&tma);
+ if (xtimegm(&tmb, &tb) != 0) {
+ tb = -1;
+ }
+
+ bool ret = true;
+ ret &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb);
+ ret &= bfs_check(ta == -1 || tm_equal(&tma, &tmb));
+
+ if (!ret) {
+ bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma));
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+
+ return ret;
+}
+
+/** Check an overflowing xtimegm() call. */
+static bool check_xtimegm_overflow(const struct tm *tm) {
+ struct tm copy = *tm;
+ time_t time = 123;
+
+ bool ret = true;
+ ret &= bfs_check(xtimegm(&copy, &time) == -1 && errno == EOVERFLOW);
+ ret &= bfs_check(tm_equal(&copy, tm));
+ ret &= bfs_check(time == 123);
+
+ if (!ret) {
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+
+ return ret;
+}
+
+/** xtimegm() tests. */
+static bool check_xtimegm(void) {
+ bool ret = true;
+
+ struct tm tm = {
+ .tm_isdst = -1,
+ };
+
+ // Check equivalence with mktime()
+ for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
+ for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
+ for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
+ for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
+ for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
+ for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
+ ret &= check_one_xtimegm(&tm);
+ }
+
+ // 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 });
+
+ return ret;
+}
+
+bool check_xtime(void) {
+ bool ret = true;
+ ret &= check_xgetdate();
+ ret &= check_xmktime();
+ ret &= check_xtimegm();
+ return ret;
+}
diff --git a/tests/xtimegm.c b/tests/xtimegm.c
deleted file mode 100644
index d774b9e..0000000
--- a/tests/xtimegm.c
+++ /dev/null
@@ -1,107 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "../src/xtime.h"
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <time.h>
-
-static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
- if (tma->tm_year != tmb->tm_year) {
- return false;
- }
- if (tma->tm_mon != tmb->tm_mon) {
- return false;
- }
- if (tma->tm_mday != tmb->tm_mday) {
- return false;
- }
- if (tma->tm_hour != tmb->tm_hour) {
- return false;
- }
- if (tma->tm_min != tmb->tm_min) {
- return false;
- }
- if (tma->tm_sec != tmb->tm_sec) {
- return false;
- }
- if (tma->tm_wday != tmb->tm_wday) {
- return false;
- }
- if (tma->tm_yday != tmb->tm_yday) {
- return false;
- }
- if (tma->tm_isdst != tmb->tm_isdst) {
- return false;
- }
-
- return true;
-}
-
-static void tm_print(FILE *file, const struct tm *tm) {
- fprintf(file, "Y%d M%d D%d h%d m%d s%d wd%d yd%d%s\n",
- tm->tm_year, tm->tm_mon, tm->tm_mday,
- tm->tm_hour, tm->tm_min, tm->tm_sec,
- tm->tm_wday, tm->tm_yday,
- tm->tm_isdst ? (tm->tm_isdst < 0 ? " (DST?)" : " (DST)") : "");
-}
-
-int main(void) {
- if (setenv("TZ", "UTC0", true) != 0) {
- perror("setenv()");
- return EXIT_FAILURE;
- }
-
- struct tm tm = {
- .tm_isdst = -1,
- };
-
- for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
- for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
- for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
- for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
- for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
- for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
- struct tm tma = tm, tmb = tm;
- time_t ta, tb;
- ta = mktime(&tma);
- if (xtimegm(&tmb, &tb) != 0) {
- tb = -1;
- }
-
- bool fail = false;
- if (ta != tb) {
- printf("Mismatch: %jd != %jd\n", (intmax_t)ta, (intmax_t)tb);
- fail = true;
- }
- if (ta != -1 && !tm_equal(&tma, &tmb)) {
- printf("mktime(): ");
- tm_print(stdout, &tma);
- printf("xtimegm(): ");
- tm_print(stdout, &tmb);
- fail = true;
- }
- if (fail) {
- printf("Input: ");
- tm_print(stdout, &tm);
- return EXIT_FAILURE;
- }
- }
-
- return EXIT_SUCCESS;
-}
diff --git a/tests/xtouch.c b/tests/xtouch.c
new file mode 100644
index 0000000..cd41842
--- /dev/null
+++ b/tests/xtouch.c
@@ -0,0 +1,282 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "bfstd.h"
+#include "sanity.h"
+#include "xtime.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#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 != 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) {
+ char *end;
+ long mode = strtol(marg, &end, 8);
+ // https://github.com/llvm/llvm-project/issues/64946
+ sanitize_init(&end);
+ if (*marg && !*end && mode >= 0 && mode < 01000) {
+ args.fmode = args.dmode = mode;
+ } else {
+ 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 (xgettime(&times[0]) != 0) {
+ perror("xgettime()");
+ 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;
+}