summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Kreutz <mail@skreutz.com>2024-03-24 15:04:09 +0100
committerStefan Kreutz <mail@skreutz.com>2024-03-24 15:04:09 +0100
commitc1fa48e9bd617d70e823efef5d6dcea41b1d2087 (patch)
tree421e69c512ac54bf65495ef23fd7d9ec5a5e67d5
downloadbrck-c1fa48e9bd617d70e823efef5d6dcea41b1d2087.tar
Add initial implementationbrck-0.1.0
-rw-r--r--.cargo/audit.toml6
-rw-r--r--.gitignore4
-rw-r--r--CHANGELOG.md17
-rw-r--r--Cargo.lock1101
-rw-r--r--Cargo.toml38
-rw-r--r--IDEAS24
-rw-r--r--LICENSE-APACHE-2.012
-rw-r--r--LICENSE-MIT19
-rw-r--r--README.md113
-rwxr-xr-xscript/check15
-rw-r--r--src/core.rs306
-rw-r--r--src/fs.rs144
-rw-r--r--src/main.rs343
-rw-r--r--tests/cli.rs299
14 files changed, 2441 insertions, 0 deletions
diff --git a/.cargo/audit.toml b/.cargo/audit.toml
new file mode 100644
index 0000000..d0abe57
--- /dev/null
+++ b/.cargo/audit.toml
@@ -0,0 +1,6 @@
+# https://github.com/rustsec/rustsec/blob/main/cargo-audit/audit.toml.example
+
+[advisories]
+ignore = [
+ # "RUSTSEC-2020-0071",
+]
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4b2a36a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/target
+.brck
+.brck.tmp
+.brck.bak
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9ef0612
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep A Changelog][] and this project adheres to
+[Semantic Versioning][].
+
+[Keep A Changelog]: https://keepachangelog.com/en/1.0.0/
+[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
+
+## Unreleased
+
+## [0.1.0] - 2024-03-24
+
+### Added
+
+- Add initial implementation
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..d2ab2c9
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1101 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "assert_fs"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec"
+dependencies = [
+ "anstyle",
+ "doc-comment",
+ "globwalk",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "tempfile",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brck"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "assert_cmd",
+ "assert_fs",
+ "chrono",
+ "clap",
+ "ctrlc",
+ "either",
+ "filetime",
+ "flate2",
+ "hex",
+ "itertools",
+ "parseq",
+ "pledge",
+ "predicates",
+ "serde",
+ "serde_json",
+ "sha2",
+ "testresult",
+ "unveil",
+ "walkdir",
+]
+
+[[package]]
+name = "bstr"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
+
+[[package]]
+name = "cc"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "chrono"
+version = "0.4.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
+dependencies = [
+ "nix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+
+[[package]]
+name = "filetime"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "globset"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
+dependencies = [
+ "bitflags 2.5.0",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "parseq"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96e0928f33f10595e789baf7a10c0fdd288694f9f9866748cf1fb9cb9f84b084"
+dependencies = [
+ "crossbeam-channel",
+]
+
+[[package]]
+name = "pledge"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "252599417b7d9a43b7fdc63dd790b0848666a8910b2ebe1a25118309c3c981e5"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "predicates"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "rustix"
+version = "0.38.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
+[[package]]
+name = "syn"
+version = "2.0.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
+dependencies = [
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "termtree"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
+
+[[package]]
+name = "testresult"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d72e255c0541f86589b0287139b70bd941a197ea4cea8fd8f87afe9c965a99e4"
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unveil"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e7fa867d559102001ec694165ed17d5f82e95213060a65f9c8b6280084bbfec"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.4",
+ "windows_aarch64_msvc 0.52.4",
+ "windows_i686_gnu 0.52.4",
+ "windows_i686_msvc 0.52.4",
+ "windows_x86_64_gnu 0.52.4",
+ "windows_x86_64_gnullvm 0.52.4",
+ "windows_x86_64_msvc 0.52.4",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..f152902
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "brck"
+version = "0.1.0"
+authors = ["Stefan Kreutz <mail@skreutz.com>"]
+edition = "2021"
+description = "A simple bit rot checker"
+readme = "README.md"
+repository = "https://git.skreutz.com/brck.git"
+license = "MIT OR Apache-2.0"
+keywords = ["bitrot", "filesystem", "integrity"]
+categories = ["command-line-utilities", "filesystem"]
+publish = true
+
+[dependencies]
+anyhow = "1.0.81"
+chrono = { version = "0.4.35", features = ["serde"] }
+clap = { version = "4.5.3", features = ["derive", "wrap_help"] }
+ctrlc = "3.4.4"
+either = "1.10.0"
+flate2 = "1.0.28"
+hex = { version = "0.4.3", features = ["serde"] }
+itertools = "0.12.1"
+parseq = "0.1.3"
+serde = { version = "1.0.197", features = ["derive"] }
+serde_json = "1.0.114"
+sha2 = "0.10.8"
+walkdir = "2.5.0"
+
+[target.'cfg(target_os = "openbsd")'.dependencies]
+pledge = "0.4.2"
+unveil = "0.3.2"
+
+[dev-dependencies]
+assert_cmd = "2.0.14"
+assert_fs = "1.1.1"
+filetime = "0.2.23"
+predicates = "3.1.0"
+testresult = "0.4.0"
diff --git a/IDEAS b/IDEAS
new file mode 100644
index 0000000..0c2fc7d
--- /dev/null
+++ b/IDEAS
@@ -0,0 +1,24 @@
+Accept units (k, m, g) for --chunk-size.
+
+Add option -k/--keep-going to continue in case of IO errors such as permission denied.
+
+Add option -e/--errexit to immediately exit >0 on the first denied difference.
+
+Add option to scan ahead and show progress on standard error.
+
+Hash and check symlinks targets?
+ https://doc.rust-lang.org/std/fs/fn.read_link.html
+ https://unix.stackexchange.com/questions/63876/changing-the-timestamp-of-a-symlink
+
+Estimate default number of worker threads based on disk read speed and CPU speed.
+
+Test effect of stdout buffering and/or locking on performance with high verbosity.
+ https://rust-cli.github.io/book/tutorial/output.html#a-note-on-printing-performance
+
+Generate a man page.
+ https://rust-cli.github.io/book/in-depth/docs.html
+ https://lib.rs/crates/clap_mangen
+
+Use common exit codes.
+ https://man.openbsd.org/sysexits
+ https://github.com/dtolnay/anyhow/issues/247
diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0
new file mode 100644
index 0000000..8df1d9e
--- /dev/null
+++ b/LICENSE-APACHE-2.0
@@ -0,0 +1,12 @@
+Copyright 2024 Stefan Kreutz <mail@skreutz.com>
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..452e8f1
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright 2024 Stefan Kreutz <mail@skreutz.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b37513f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,113 @@
+# Brck -- a simple bit rot checker
+
+Brck is a simple bit rot checker for legacy file systems. It records and
+compares the modification times and cryptographic hash sums of regular files.
+When a file's hash changed but it's modification time did not, then Brck
+reports the file as corrupted so you have a chance to restore the file from
+backup. You do have backups, right? 😉
+
+Brck's original source code is hosted [here][brck-repo].
+
+## Installation
+
+Brck is available on [crates.io][brck-cratesio]. You can install it using
+Rust's package manager [Cargo][cargo].
+
+ $ cargo install brck
+
+## Usage
+
+Without any options, Brck will record all regular files in current working
+directory recursively and write those records to a gzipped `.brck` file.
+
+ $ brck
+
+When you run the same command again, Brck will compare the recorded files
+against the current file system and report any corrupted files.
+
+ $ brck
+ corrupted: ./test_file
+ Error: Found 1 denied difference
+
+Brck is quiet by default. Increase the verbosity using `-v` or request a
+summary using `-s`. Alternatively, enable the JSON output using `-J` and
+perform your own post-processing, e.g., using [jq][]:
+
+ $ brck -vvJ | jq -sr 'group_by(.type)[] | [.[0].type, length] | @tsv' 2>/dev/null
+ added 4
+ corrupted 1
+ unchanged 1219
+
+See the built-in help for all supported options:
+
+ $ brck --help
+
+## Features
+
+- Parallel sequential processing: Brck processes files in parallel, yet outputs
+ and records files in-order
+
+- One-pass: Brck reads your files only once
+
+- Constant memory: Brck's memory footprint is independent of the number of
+ processed files
+
+- Relative paths: Brck records relative paths such that you can move the
+ containing directory around
+
+- Quiet by default: Without any options, Brck prints denied differences to
+ standard output, and errors to standard error; nothing else
+
+- Human readable or machine readable, newline-delimited JSON output, at your
+ option
+
+- Graceful shutdown on the first interrupt signal, forceful immediate shutdown
+ on the second
+
+- [Pledged][pledge] and [unveiled][unveil] on OpenBSD
+
+## Limitations
+
+- Brck doesn't follow symlinks.
+
+- Brck doesn't track hardlinks. Performance will be suboptimal in the presence
+ of large hardlinked files because Brck hashes each copy individually.
+
+- Brck doesn't track reflinks. Modern copy-on-write file systems are out of
+ scope because they should check file content integrity themselves, like
+ OpenZFS and Btrfs do.
+
+- Brck may leak file meta data because its `.brck` file may have different
+ permissions than the listed files.
+
+- Brck doesn't respect platform-specific temporary directories such as `TMPDIR`
+ on UNIX. Instead, it creates its temporary files in the current working
+ directory, next to the `.brck` file. This way, Brck can update the files
+ atomically on POSIX-conform systems.
+
+- Brck may fail to remove its temporary files, e.g., in the event of a
+ segfault.
+
+## See also
+
+- Definition of *bit rot* in the [Jargon File][jargon]
+ ([archive][jargon-archive])
+
+- A similar Python program named [Bitrot][python-bitrot]
+
+- A similar Rust program named [Legdur][legdur] ([crates.io][legdur-cratesio])
+
+- A blog post on [Things UNIX can do atomically][rcrowley]
+
+[brck-repo]: https://git.skreutz.com/brck.git/
+[brck-cratesio]: https://crates.io/crates/brck
+[cargo]: https://doc.rust-lang.org/cargo/
+[jq]: https://jqlang.github.io/jq/
+[pledge]: https://man.openbsd.org/pledge
+[unveil]: https://man.openbsd.org/unveil
+[jargon]: http://www.catb.org/jargon/html/B/bit-rot.html
+[jargon-archive]: https://web.archive.org/web/20240312124910/http://www.catb.org/jargon/html/B/bit-rot.html
+[legdur]: https://git.cyplo.dev/cyplo/legdur
+[legdur-cratesio]: https://crates.io/crates/legdur
+[python-bitrot]: https://github.com/ambv/bitrot/
+[rcrowley]: https://web.archive.org/web/20160304224616/http://rcrowley.org/2010/01/06/things-unix-can-do-atomically
diff --git a/script/check b/script/check
new file mode 100755
index 0000000..edf251e
--- /dev/null
+++ b/script/check
@@ -0,0 +1,15 @@
+#! /bin/sh
+
+set -o errexit
+set -o nounset
+set -o xtrace
+
+cargo fmt --all --check
+
+cargo check --workspace --all-targets --all-features
+cargo build --workspace --all-targets --all-features
+cargo test --workspace --all-targets --all-features
+cargo clippy --workspace --all-targets --all-features -- --deny warnings
+env RUSTDOCFLAGS="-Dwarnings" \
+ cargo doc --all-features --no-deps --document-private-items
+cargo audit
diff --git a/src/core.rs b/src/core.rs
new file mode 100644
index 0000000..6ac3ba5
--- /dev/null
+++ b/src/core.rs
@@ -0,0 +1,306 @@
+//! Core functionality.
+//!
+//! This module contains Brck's core types and functions.
+
+use std::{
+ cmp::Ordering,
+ fs::OpenOptions,
+ io::{BufReader, Read},
+ path::{Path, PathBuf},
+ sync::{atomic::AtomicBool, Arc},
+};
+
+use chrono::{DateTime, Utc};
+use either::Either;
+use flate2::bufread::GzDecoder;
+use itertools::{EitherOrBoth, Itertools};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use walkdir::WalkDir;
+
+/// Produces an iterator over the regular files below the given path.
+///
+/// The files are sorted by their full path.
+/// Symlinks are not followed, except for the original `path`.
+pub fn find_files<P: AsRef<Path>>(
+ path: P,
+) -> impl Iterator<Item = Result<PathBuf, walkdir::Error>> {
+ WalkDir::new(path.as_ref())
+ .sort_by_path()
+ .into_iter()
+ .filter_ok(|entry| entry.file_type().is_file())
+ .map_ok(|entry| entry.into_path())
+}
+
+/// Extension for [WalkDir].
+trait WalkDirExt {
+ /// Sort directory entries by full path.
+ ///
+ /// Yiedls "foo bar/buzz" before "foo/buzz".
+ fn sort_by_path(self) -> WalkDir;
+}
+
+impl WalkDirExt for WalkDir {
+ fn sort_by_path(self) -> WalkDir {
+ self.sort_by_key(|a| {
+ if a.file_type().is_dir() {
+ let mut name = a.file_name().to_os_string();
+ name.push(std::path::MAIN_SEPARATOR.to_string());
+ name
+ } else {
+ a.file_name().to_os_string()
+ }
+ })
+ }
+}
+
+/// Returns an iterator over the records of the given database.
+pub fn read_db<P: AsRef<Path>>(
+ path: P,
+) -> Result<impl Iterator<Item = Result<Record, serde_json::Error>>, std::io::Error> {
+ let file = OpenOptions::new().read(true).open(path)?;
+ let buf = BufReader::new(file);
+ let dec = GzDecoder::new(buf);
+ let iter = serde_json::Deserializer::from_reader(dec).into_iter::<Record>();
+ Ok(iter)
+}
+
+/// A record of a file's modification time and hash sum.
+#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
+pub struct Record {
+ pub path: PathBuf,
+ pub modified: DateTime<Utc>,
+ #[serde(with = "hex::serde")]
+ pub sha256: Vec<u8>,
+}
+
+impl Record {
+ /// Constructs a `Record` of the given file.
+ ///
+ /// This function reads the given file in chunks of at most `chunk_size` bytes. It checks the
+ /// `terminate` flag before reading each chunk, returning the `RecordError::Interrupt` error
+ /// when set.
+ pub fn from_path<P: AsRef<Path>>(
+ path: P,
+ chunk_size: u64,
+ terminate: Arc<AtomicBool>,
+ ) -> Result<Record, RecordError> {
+ let modified = std::fs::symlink_metadata(&path)?.modified()?;
+ let mut file = std::fs::File::open(&path)?;
+ let mut hasher = Sha256::new();
+ loop {
+ if terminate.load(std::sync::atomic::Ordering::SeqCst) {
+ return Err(RecordError::Interrupt);
+ }
+ let mut reader = std::io::Read::by_ref(&mut file).take(chunk_size);
+ if std::io::copy(&mut reader, &mut hasher)? == 0 {
+ break;
+ }
+ }
+ let hash = hasher.finalize();
+ Ok(Record {
+ path: path.as_ref().into(),
+ modified: modified.into(),
+ sha256: hash.as_slice().into(),
+ })
+ }
+}
+
+/// The possible errors of [Record::from_path].
+#[derive(Debug)]
+pub enum RecordError {
+ IO(std::io::Error),
+ Interrupt,
+}
+
+impl std::fmt::Display for RecordError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RecordError::IO(err) => write!(f, "{}", err),
+ RecordError::Interrupt => write!(f, "Received interrupt signal"),
+ }
+ }
+}
+
+impl std::error::Error for RecordError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ RecordError::IO(err) => Some(err),
+ RecordError::Interrupt => None,
+ }
+ }
+}
+
+impl From<std::io::Error> for RecordError {
+ fn from(value: std::io::Error) -> Self {
+ RecordError::IO(value)
+ }
+}
+
+/// Returns an iterator over the difference of the given files.
+pub fn diff<I, J, E, F>(cached: I, found: J) -> impl Iterator<Item = Result<Diff, Either<E, F>>>
+where
+ I: Iterator<Item = Result<Record, E>>,
+ J: Iterator<Item = Result<Record, F>>,
+{
+ cached
+ .merge_join_by(found, |cached, found| match (cached, found) {
+ (Err(_), _) => Ordering::Less,
+ (Ok(_), Err(_)) => Ordering::Greater,
+ (Ok(cached), Ok(found)) => cached.path.cmp(&found.path),
+ })
+ .map(|item| match item {
+ EitherOrBoth::Left(Err(err)) => Err(Either::Left(err)),
+ EitherOrBoth::Right(Err(err)) => Err(Either::Right(err)),
+ EitherOrBoth::Left(Ok(old)) => Ok(Diff::Removed { old }),
+ EitherOrBoth::Right(Ok(new)) => Ok(Diff::Added { new }),
+ EitherOrBoth::Both(Ok(old), Ok(new)) => {
+ if old.modified == new.modified {
+ if old.sha256 == new.sha256 {
+ Ok(Diff::Unchanged { old, new })
+ } else {
+ Ok(Diff::Corrupted { old, new })
+ }
+ } else if old.sha256 == new.sha256 {
+ Ok(Diff::Touched { old, new })
+ } else {
+ Ok(Diff::Changed { old, new })
+ }
+ }
+ _ => unreachable!(),
+ })
+}
+
+/// The list of possible types of differences.
+#[derive(Debug, Clone, Hash, Eq, PartialEq, clap::ValueEnum)]
+pub enum DiffKind {
+ Added,
+ Touched,
+ Changed,
+ Unchanged,
+ Removed,
+ Corrupted,
+}
+
+/// The difference between two records of a file.
+#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
+#[serde(tag = "type", rename_all = "lowercase")]
+pub enum Diff {
+ // Modeled variants as structs to improve their JSON representation.
+ Added { new: Record },
+ Touched { old: Record, new: Record },
+ Changed { old: Record, new: Record },
+ Unchanged { old: Record, new: Record },
+ Removed { old: Record },
+ Corrupted { old: Record, new: Record },
+}
+
+impl Diff {
+ pub fn kind(&self) -> DiffKind {
+ match self {
+ Diff::Added { .. } => DiffKind::Added,
+ Diff::Touched { .. } => DiffKind::Touched,
+ Diff::Changed { .. } => DiffKind::Changed,
+ Diff::Unchanged { .. } => DiffKind::Unchanged,
+ Diff::Removed { .. } => DiffKind::Removed,
+ Diff::Corrupted { .. } => DiffKind::Corrupted,
+ }
+ }
+}
+
+impl std::fmt::Display for Diff {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Diff::Added { new, .. } => write!(f, "added: {}", new.path.display()),
+ Diff::Touched { old, .. } => write!(f, "touched: {}", old.path.display()),
+ Diff::Changed { old, .. } => write!(f, "changed: {}", old.path.display()),
+ Diff::Unchanged { old, .. } => write!(f, "unchanged: {}", old.path.display()),
+ Diff::Removed { old, .. } => write!(f, "removed: {}", old.path.display()),
+ Diff::Corrupted { old, .. } => write!(f, "corrupted: {}", old.path.display()),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use testresult::TestResult;
+
+ use super::*;
+
+ fn record(path: &str, modified: &str, sha256: &str) -> Record {
+ Record {
+ path: path.into(),
+ modified: DateTime::parse_from_rfc3339(modified).unwrap().into(),
+ sha256: sha256.into(),
+ }
+ }
+
+ #[test]
+ fn diff_works() -> TestResult {
+ let a = record("a", "2024-03-22T00:00:00Z", "apple");
+ let b = record("b", "2024-03-22T00:00:00Z", "banana");
+ let b_touched = record("b", "2024-03-22T01:00:00Z", "banana");
+ let c = record("c", "2024-03-22T00:00:00Z", "cherry");
+ let c_changed = record("c", "2024-03-22T01:00:00Z", "cashew");
+ let d = record("d", "2024-03-22T00:00:00Z", "date");
+ let e = record("e", "2024-03-22T00:00:00Z", "elderberry");
+ let f = record("f", "2024-03-22T00:00:00Z", "fig");
+ let f_corrupted = record("f", "2024-03-22T00:00:00Z", "feijoa");
+
+ let db = [
+ Err("foo"),
+ Ok(b.clone()),
+ Ok(c.clone()),
+ Ok(d.clone()),
+ Ok(e.clone()),
+ Ok(f.clone()),
+ ];
+
+ let fs = [
+ Ok(a.clone()),
+ Ok(b_touched.clone()),
+ Err("bar"),
+ Ok(c_changed.clone()),
+ Ok(d.clone()),
+ Ok(f_corrupted.clone()),
+ ];
+
+ let mut result = diff(db.into_iter(), fs.into_iter());
+
+ assert_eq!(result.next(), Some(Err(Either::Left("foo"))));
+ assert_eq!(result.next(), Some(Ok(Diff::Added { new: a })));
+ assert_eq!(
+ result.next(),
+ Some(Ok(Diff::Touched {
+ old: b,
+ new: b_touched
+ }))
+ );
+ assert_eq!(result.next(), Some(Err(Either::Right("bar"))));
+ assert_eq!(
+ result.next(),
+ Some(Ok(Diff::Changed {
+ old: c,
+ new: c_changed
+ }))
+ );
+ assert_eq!(
+ result.next(),
+ Some(Ok(Diff::Unchanged {
+ old: d.clone(),
+ new: d
+ }))
+ );
+ assert_eq!(result.next(), Some(Ok(Diff::Removed { old: e })));
+ assert_eq!(
+ result.next(),
+ Some(Ok(Diff::Corrupted {
+ old: f,
+ new: f_corrupted
+ }))
+ );
+ assert_eq!(result.next(), None);
+
+ Ok(())
+ }
+}
diff --git a/src/fs.rs b/src/fs.rs
new file mode 100644
index 0000000..f962e53
--- /dev/null
+++ b/src/fs.rs
@@ -0,0 +1,144 @@
+//! Filesytem functionality.
+//!
+//! This module contains several extensions to [std::fs].
+
+use std::{
+ fs::{File, OpenOptions},
+ path::{Path, PathBuf},
+};
+
+/// A std::fs::File wrapper that removes the file when dropped.
+#[derive(Debug)]
+pub struct TmpFile {
+ path: PathBuf,
+ file: File,
+}
+
+impl TmpFile {
+ /// Opens a new temporary file at the given path.
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<TmpFile, std::io::Error> {
+ let mut options = OpenOptions::new();
+ options.create_new(true).write(true);
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::OpenOptionsExt;
+ options.mode(0o600);
+ }
+ let file = options.open(&path)?;
+ Ok(TmpFile {
+ path: path.as_ref().into(),
+ file,
+ })
+ }
+
+ pub fn file(&self) -> &File {
+ &self.file
+ }
+}
+
+impl Drop for TmpFile {
+ fn drop(&mut self) {
+ if let Err(err) = remove_file_if_exists(&self.path) {
+ eprintln!(
+ "Failed to remove temporary file {}: {}",
+ &self.path.display(),
+ err
+ );
+ }
+ }
+}
+
+/// Removes a file from the filesystem, if it exists.
+pub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
+ std::fs::remove_file(path).or_else(|err| {
+ if err.kind() == std::io::ErrorKind::NotFound {
+ Ok(())
+ } else {
+ Err(err)
+ }
+ })
+}
+
+/// Returns the path without it's final component, if there is one.
+///
+/// In contrast to [Path::parent], this function returns the [CurDir][std::path::Component::CurDir]
+/// instead of the empty string for the current directory.
+pub fn parent_dir<P: AsRef<Path>>(path: &P) -> Option<&Path> {
+ path.as_ref().parent().map(|p| {
+ if p.as_os_str().is_empty() {
+ Path::new(std::path::Component::CurDir.as_os_str())
+ } else {
+ p
+ }
+ })
+}
+
+/// Rename a file.
+///
+/// This function should be atomic on POSIX-conform systems.
+///
+/// Both paths must be located on the same filesystem.
+pub fn rename_file<P, Q>(from: P, to: Q) -> Result<(), std::io::Error>
+where
+ P: AsRef<Path>,
+ Q: AsRef<Path>,
+{
+ File::open(&from)?.sync_all()?;
+ std::fs::rename(&from, &to)?;
+
+ let dest_dir = parent_dir(&to).ok_or_else(|| std::io::Error::other("is root directory"))?;
+ File::open(dest_dir)?.sync_all()?;
+
+ let src_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
+ if src_dir != dest_dir {
+ File::open(src_dir)?.sync_all()?;
+ }
+
+ Ok(())
+}
+
+/// Copy a file.
+///
+/// This functions hard links `from` to `tmp`, and then renames the latter to `to`. The renaming
+/// should be atomic on POSIX-conform systems.
+///
+/// All three paths must be located on the same filesystem.
+// Didn't use https://crates.io/crates/atomic-write-file or https://crates.io/crates/atomicwrites
+// because they create randomly named temporary files, which I wanted to unveil(2) on OpenBSD, and
+// remove when receiving a termination signal.
+pub fn copy_file<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error>
+where
+ P: AsRef<Path>,
+ Q: AsRef<Path>,
+ R: AsRef<Path>,
+{
+ fn inner<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error>
+ where
+ P: AsRef<Path>,
+ Q: AsRef<Path>,
+ R: AsRef<Path>,
+ {
+ let tmp_dir =
+ parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
+ File::open(tmp_dir)?.sync_all()?;
+
+ std::fs::rename(&tmp, &to)?;
+
+ let to_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
+ File::open(to_dir)?.sync_all()?;
+
+ Ok(())
+ }
+
+ File::open(&from)?.sync_all()?;
+ std::fs::hard_link(&from, &tmp)?;
+ inner(&from, &tmp, &to).inspect_err(|_| {
+ if let Err(err) = remove_file_if_exists(&tmp) {
+ eprintln!(
+ "Failed to remove temporary file {}: {}",
+ tmp.as_ref().display(),
+ err
+ );
+ }
+ })
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..2d26eed
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,343 @@
+#![forbid(unsafe_code)]
+
+use std::{
+ collections::HashMap,
+ num::{NonZeroU64, NonZeroUsize},
+ path::PathBuf,
+ sync::{atomic::AtomicBool, Arc},
+};
+
+use anyhow::{anyhow, Context};
+use clap::Parser;
+use either::Either;
+use flate2::{write::GzEncoder, Compression};
+use itertools::Itertools;
+use parseq::ParallelIterator;
+
+mod core;
+use core::*;
+
+mod fs;
+use fs::*;
+
+// std::process::ExitCode::exit_process is unstable
+// Exit codes should fit u8.
+const EXIT_SUCCESS: i32 = 0;
+const EXIT_FAILURE: i32 = 1;
+const EXIT_INTERRUPT: i32 = 130;
+
+/// A simple bit rot checker for legacy file systems.
+///
+/// On the first invocation, Brck records the modification time and a cryptographic hash sum of each regular file in the current working directory to a .brck file.
+/// On subsequent invocations, Brck compares the recorded files against the current file system.
+///
+/// A file can be added, touched, changed, unchanged, removed, or corrupted.
+/// A file is corrupted if it's content changed but it's modification did not.
+///
+/// Without any options, Brck denies corrupted files.
+/// If Brck finds one or more denied differences, it prints them to standard output and exits >0.
+/// Otherwise, Brck updates the .brck file.
+#[derive(Debug, Parser)]
+#[command(about, version, author)]
+struct Args {
+ /// Increase verbosity.
+ ///
+ /// Specify once to print all changed files.
+ /// Specify twice to print unchanged files, too.
+ /// By default, only denied differences are printed.
+ #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
+ verbosity: u8,
+
+ /// Do not actually modify the database.
+ #[arg(short = 'n', long)]
+ dry_run: bool,
+
+ /// Specify the number of worker threads.
+ ///
+ /// Defaults to the available parallelism, typically the number of CPUs.
+ #[arg(short, long)]
+ jobs: Option<NonZeroUsize>,
+
+ /// Deny select differences only.
+ ///
+ /// This option accepts a comma-separated list of differences to deny.
+ /// Without this option, only corrupted files are denied.
+ /// With this option, only the specified differences are denied.
+ #[arg(value_enum, short, long, default_values_t = vec![DiffKind::Corrupted], value_delimiter = ',', action = clap::ArgAction::Append)]
+ deny: Vec<DiffKind>,
+
+ /// Enable structured output.
+ #[arg(short = 'J', long)]
+ json: bool,
+
+ /// Print summary to standard error.
+ #[arg(short, long)]
+ summary: bool,
+
+ /// Specify the maximum number of bytes a worker thread may read without checking for an interrupt signal.
+ ///
+ /// Defaults to 1 GiB.
+ #[arg(long)]
+ chunk_size: Option<NonZeroU64>,
+
+ /// Specify the maximum number of files to read ahead.
+ ///
+ /// This should be greater than the number of worker threads.
+ ///
+ /// Defaults to 1024 files per worker thread.
+ #[arg(long)]
+ queue_size: Option<NonZeroUsize>,
+}
+
+fn main() {
+ std::process::exit(match run() {
+ Ok(_) => EXIT_SUCCESS,
+ Err(err) if err.is::<clap::Error>() => {
+ // Use clap's own error formatter
+ eprintln!("{err}");
+ EXIT_FAILURE
+ }
+ Err(err) => {
+ eprintln!("Error: {:?}", err);
+ match err.downcast_ref::<RecordError>() {
+ Some(RecordError::Interrupt) => EXIT_INTERRUPT,
+ _ => EXIT_FAILURE,
+ }
+ }
+ })
+}
+
+fn run() -> Result<(), anyhow::Error> {
+ let terminate = Arc::new(AtomicBool::new(false));
+ let args = Args::try_parse()?;
+
+ // Paths must begin with ./ for correct filtering.
+ let current_dir: PathBuf = std::path::Component::CurDir.as_os_str().into();
+ let db_path = current_dir.join(".brck");
+ let backup_path = current_dir.join(".brck.bak");
+
+ // Creating temporary files in the same directory instead of std::env_temp_dir() to enable atomic rename(2) on POSIX-conform systems.
+ // The drawback is that the operating system won't remove these files automatically.
+ let tmp_db_path = current_dir.join(".brck.tmp");
+ let tmp_backup_path = current_dir.join(".brck.bak.tmp");
+
+ #[cfg(target_os = "openbsd")]
+ {
+ use std::{os::unix::ffi::OsStrExt, path::Path};
+
+ pledge::pledge![Stdio Rpath Wpath Cpath Unveil, Stdio].context("Failed to pledge(2)")?;
+
+ fn unveil<P: AsRef<Path>>(path: P, permissions: &str) -> Result<(), anyhow::Error> {
+ unveil::unveil(path.as_ref().as_os_str().as_bytes(), permissions).with_context(|| {
+ format!(
+ "Failed to unveil(2) {} with permissions {}",
+ path.as_ref().display(),
+ permissions
+ )
+ })
+ }
+
+ unveil(".", "r")?;
+ unveil(&db_path, "rwc")?;
+ unveil(&tmp_db_path, "rwc")?;
+ unveil(&backup_path, "rwc")?;
+ unveil(&tmp_backup_path, "rwc")?;
+ unveil::unveil("", "").context("Failed to disable unveil(2)")?;
+ }
+
+ let t = terminate.clone();
+ ctrlc::try_set_handler(move || {
+ if t.load(std::sync::atomic::Ordering::SeqCst) {
+ eprintln!("Received second interrupt signal");
+ std::process::exit(EXIT_INTERRUPT);
+ }
+ eprintln!("Received interrupt signal");
+ t.store(true, std::sync::atomic::Ordering::SeqCst);
+ })
+ .context("Failed to register interrupt signal handler")?;
+
+ let (db, first_run) = match read_db(&db_path) {
+ Ok(db) => Ok((Either::Left(db), false)),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
+ Ok((Either::Right(std::iter::empty()), true))
+ }
+ Err(err) => Err(err),
+ }
+ .with_context(|| format!("Failed to read database {}", db_path.display()))?;
+ let db = db.map(|item| {
+ item.map_err(|err| {
+ anyhow::Error::new(err).context(format!(
+ "Failed to decode record from database {}",
+ &db_path.display()
+ ))
+ })
+ });
+
+ let tmp = TmpFile::open(&tmp_db_path).with_context(|| {
+ format!(
+ "Failed to create temporary database {}",
+ tmp_db_path.display()
+ )
+ })?;
+ let mut tmp = GzEncoder::new(tmp.file(), Compression::default());
+
+ let jobs = args
+ .jobs
+ .or_else(|| std::thread::available_parallelism().ok())
+ .map(NonZeroUsize::get)
+ .unwrap_or(1);
+ let queue_size = args
+ .queue_size
+ .map(NonZeroUsize::get)
+ .unwrap_or_else(|| jobs.saturating_mul(1024));
+ let chunk_size = args
+ .chunk_size
+ .map(NonZeroU64::get)
+ .unwrap_or(1024 * 1024 * 1024);
+
+ let fs = find_files(&current_dir)
+ .filter_ok(|path| path != &db_path && path != &tmp_db_path && path != &backup_path)
+ .map(|item| {
+ item.map_err(|err| anyhow::Error::new(err).context("Failed to walk filesystem"))
+ })
+ .map_parallel_limit(jobs, queue_size, move |item| {
+ item.and_then(|path| {
+ Record::from_path(&path, chunk_size, terminate.clone())
+ .with_context(|| format!("Failed to read file {}", path.display()))
+ })
+ });
+
+ let mut counter = HashMap::new();
+ let mut denied = 0;
+
+ diff(db, fs).try_for_each(|item| match item {
+ Err(Either::Left(err)) => Err(err),
+ Err(Either::Right(err)) => Err(err),
+ Ok(diff) => {
+ *counter.entry(diff.kind()).or_insert(0) += 1;
+ record(&diff, &mut tmp).with_context(|| {
+ format!(
+ "Failed to write to temporary database {}",
+ tmp_db_path.display()
+ )
+ })?;
+ if args.deny.contains(&diff.kind()) {
+ if args.json {
+ println!("{}", serde_json::to_string(&diff).unwrap());
+ } else {
+ println!("{diff}");
+ }
+ denied += 1;
+ } else if args.verbosity > 1
+ || (diff.kind() != DiffKind::Unchanged && args.verbosity > 0)
+ {
+ if args.json {
+ println!("{}", serde_json::to_string(&diff).unwrap());
+ } else {
+ println!("{diff}");
+ }
+ }
+ Ok(())
+ }
+ })?;
+
+ if args.summary {
+ let total: usize = counter.values().sum();
+ let width = usize::try_from(total.checked_ilog10().unwrap_or(0) + 1).unwrap_or(0);
+ eprintln!(
+ "Removed: {:>width$}",
+ counter.get(&DiffKind::Removed).unwrap_or(&0)
+ );
+ eprintln!(
+ "Added: {:>width$}",
+ counter.get(&DiffKind::Added).unwrap_or(&0)
+ );
+ eprintln!(
+ "Unchanged: {:>width$}",
+ counter.get(&DiffKind::Unchanged).unwrap_or(&0)
+ );
+ eprintln!(
+ "Corrupted: {:>width$}",
+ counter.get(&DiffKind::Corrupted).unwrap_or(&0)
+ );
+ eprintln!(
+ "Touched: {:>width$}",
+ counter.get(&DiffKind::Touched).unwrap_or(&0)
+ );
+ eprintln!(
+ "Changed: {:>width$}",
+ counter.get(&DiffKind::Changed).unwrap_or(&0)
+ );
+ eprintln!("Total: {:>width$}", total);
+ }
+
+ if denied > 0 {
+ return Err(anyhow!(
+ "Found {} denied {}",
+ denied,
+ if denied == 1 {
+ "difference"
+ } else {
+ "differences"
+ }
+ ));
+ }
+
+ if args.dry_run {
+ eprintln!("Exiting due to dry run");
+ return Ok(());
+ }
+
+ tmp.finish().with_context(|| {
+ format!(
+ "Failed to synchronize temporary database {}",
+ tmp_db_path.display()
+ )
+ })?;
+ if !first_run {
+ copy_file(&db_path, tmp_backup_path, &backup_path).with_context(|| {
+ format!(
+ "Failed to backup database {} to {}",
+ db_path.display(),
+ backup_path.display()
+ )
+ })?;
+ }
+ rename_file(&tmp_db_path, &db_path).with_context(|| {
+ format!(
+ "Failed to persist temporary database {} to {}",
+ tmp_db_path.display(),
+ db_path.display()
+ )
+ })?;
+
+ Ok(())
+}
+
+fn record<T: std::io::Write>(diff: &Diff, mut writer: T) -> Result<(), std::io::Error> {
+ let file = match diff {
+ Diff::Removed { .. } => None,
+ Diff::Added { new, .. } => Some(new),
+ Diff::Unchanged { new, .. } => Some(new),
+ Diff::Corrupted { new, .. } => Some(new),
+ Diff::Touched { new, .. } => Some(new),
+ Diff::Changed { new, .. } => Some(new),
+ };
+ if let Some(file) = file {
+ let s = serde_json::to_string(file).unwrap();
+ writeln!(writer, "{}", s)
+ } else {
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn verify_clap_app() {
+ use clap::CommandFactory;
+ Args::command().debug_assert()
+ }
+}
diff --git a/tests/cli.rs b/tests/cli.rs
new file mode 100644
index 0000000..2846a62
--- /dev/null
+++ b/tests/cli.rs
@@ -0,0 +1,299 @@
+use std::{fs::File, path::Path, process::Command};
+
+use assert_cmd::prelude::*;
+use assert_fs::fixture::{FileWriteStr, PathChild};
+use filetime::FileTime;
+use predicates::prelude::*;
+use serde_json::Value;
+use sha2::{Digest, Sha256};
+use testresult::TestResult;
+
+#[test]
+fn corrupted() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+ dir.child("b").write_str("banana")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ let db = dir.child(".brck");
+ assert!(db.exists());
+ let db_time = db.metadata()?.modified()?;
+ let db_hash = sha256(db.path())?;
+
+ let b = dir.child("b");
+ let b_time = FileTime::from_last_modification_time(&b.metadata()?);
+ b.write_str("cheese")?;
+ filetime::set_file_mtime(b.path(), b_time)?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .failure()
+ .stdout(predicate::str::contains("corrupted"))
+ .stderr(predicate::str::contains("Error"));
+
+ assert_eq!(db_time, db.metadata()?.modified()?);
+ assert_eq!(db_hash, sha256(db.path())?);
+
+ Ok(())
+}
+
+#[test]
+fn quiet_by_default() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ Ok(())
+}
+
+#[test]
+fn verbose() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--verbose")
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("added"))
+ .stderr(predicate::str::is_empty());
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--verbose")
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--verbose")
+ .arg("--verbose")
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("unchanged"))
+ .stderr(predicate::str::is_empty());
+
+ Ok(())
+}
+
+#[test]
+fn json() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ let assert = cmd
+ .current_dir(&dir)
+ .arg("--verbose")
+ .arg("--verbose")
+ .arg("--json")
+ .assert()
+ .success()
+ .stderr(predicate::str::is_empty());
+
+ let out = &assert.get_output().stdout;
+ let mut iter = serde_json::Deserializer::from_slice(out).into_iter::<Value>();
+ let added = iter.next().unwrap()?;
+ assert_eq!(
+ added.get("type"),
+ Some(Value::String("added".into())).as_ref()
+ );
+ assert_eq!(
+ added.get("new").unwrap().get("sha256"),
+ Some(Value::String(
+ "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b".into()
+ ))
+ .as_ref()
+ );
+ assert!(iter.next().is_none());
+
+ Ok(())
+}
+
+#[test]
+fn summary() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+ dir.child("b").write_str("banana")?;
+ dir.child("c").write_str("cherry")?;
+ dir.child("d").write_str("date")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ std::fs::remove_file(dir.child("b"))?;
+ dir.child("c").write_str("cashew")?;
+ dir.child("d").write_str("date")?;
+ dir.child("e").write_str("elderberry")?;
+ dir.child("f").write_str("fig")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ // Matching ^ and $ doesn't really work.
+ let pred = predicate::str::is_match("Removed: *1")?
+ .and(predicate::str::is_match("Added: *2")?) // e, f
+ .and(predicate::str::is_match("Unchanged: *1")?) // a
+ .and(predicate::str::is_match("Corrupted: *0")?) // -
+ .and(predicate::str::is_match("Touched: *1")?) // d
+ .and(predicate::str::is_match("Changed: *1")?) // c
+ .and(predicate::str::is_match("Total: *6")?); // a-f
+ cmd.current_dir(&dir)
+ .arg("--summary")
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(pred);
+
+ Ok(())
+}
+
+#[test]
+fn dry_run_init() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--dry-run")
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::contains("dry run"));
+ assert!(!dir.child(".brck").exists());
+
+ Ok(())
+}
+
+#[test]
+fn dry_run_update() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ let db = dir.child(".brck");
+ assert!(db.exists());
+ let db_time = db.metadata()?.modified()?;
+ let db_hash = sha256(db.path())?;
+
+ dir.child("b").write_str("banana")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--dry-run")
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::contains("dry run"));
+
+ assert_eq!(db_time, db.metadata()?.modified()?);
+ assert_eq!(db_hash, sha256(db.path())?);
+
+ Ok(())
+}
+
+#[test]
+fn deny() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+ dir.child("a").write_str("apple")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ let db = dir.child(".brck");
+ assert!(db.exists());
+ let db_time = db.metadata()?.modified()?;
+ let db_hash = sha256(db.path())?;
+
+ dir.child("b").write_str("banana")?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .arg("--deny")
+ .arg("added")
+ .assert()
+ .failure()
+ .stdout(predicate::str::contains("added"))
+ .stderr(predicate::str::contains("Error"));
+
+ assert_eq!(db_time, db.metadata()?.modified()?);
+ assert_eq!(db_hash, sha256(db.path())?);
+
+ Ok(())
+}
+
+#[test]
+fn empty_dir() -> TestResult {
+ let dir = assert_fs::TempDir::new()?;
+
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.current_dir(&dir)
+ .assert()
+ .success()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::is_empty());
+
+ assert!(dir.child(".brck").exists());
+
+ Ok(())
+}
+
+#[test]
+fn short_help() -> TestResult {
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.arg("-h")
+ .assert()
+ .failure()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::contains("--help"));
+
+ Ok(())
+}
+
+#[test]
+fn long_help() -> TestResult {
+ let mut cmd = Command::cargo_bin("brck")?;
+ cmd.arg("--help")
+ .assert()
+ .failure()
+ .stdout(predicate::str::is_empty())
+ .stderr(predicate::str::contains("Usage"));
+
+ Ok(())
+}
+
+fn sha256<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, std::io::Error> {
+ let mut file = File::open(path)?;
+ let mut hasher = Sha256::new();
+ std::io::copy(&mut file, &mut hasher)?;
+ let hash = hasher.finalize();
+ Ok(hash.as_slice().into())
+}
Generated by cgit. See skreutz.com for my tech blog and contact information.