commit 6d684d1c56f5ff14f07ff3dc0c2c9e8dafad0098
Author: Andy Khramtsov <>
Date: Sat, 7 Feb 2026 01:53:13 +0300
feat: init
Diffstat:
15 files changed, 2117 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+/target
+log
diff --git a/Cargo.lock b/Cargo.lock
@@ -0,0 +1,1244 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+dependencies = [
+ "serde_core",
+]
+
+[[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 = "bumpalo"
+version = "3.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.5.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "config"
+version = "0.15.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6"
+dependencies = [
+ "async-trait",
+ "convert_case",
+ "json5",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde-untagged",
+ "serde_core",
+ "serde_json",
+ "toml",
+ "winnow",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "console"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys",
+]
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "dialoguer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
+dependencies = [
+ "console",
+ "shell-words",
+ "tempfile",
+ "zeroize",
+]
+
+[[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 = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[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 = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "jannie"
+version = "0.0.0"
+dependencies = [
+ "clap",
+ "config",
+ "dialoguer",
+ "indexmap",
+ "serde",
+ "test-log",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "uuid",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.179"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "pest"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
+
+[[package]]
+name = "ron"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
+dependencies = [
+ "bitflags",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "typeid",
+ "unicode-ident",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "test-log"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4"
+dependencies = [
+ "env_logger",
+ "test-log-macros",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "test-log-macros"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tokio"
+version = "1.49.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.11+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
+dependencies = [
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
+dependencies = [
+ "getrandom 0.3.4",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "yaml-rust2"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zmij"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
diff --git a/Cargo.toml b/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "jannie"
+version = "0.0.0"
+edition = "2024"
+
+[dependencies]
+clap = { version = "4.5.54", features = ["derive"] }
+config = "0.15.19"
+dialoguer = "0.12.0"
+indexmap = "2.13.0"
+serde = { version = "1.0.228", features = ["derive"] }
+test-log = { version = "0.2.19", features = ["trace"] }
+thiserror = "2.0.17"
+tokio = { version = "1.49.0", features = ["rt-multi-thread", "fs"] }
+tracing = "0.1.44"
+tracing-appender = "0.2.4"
+tracing-subscriber = { version = "0.3.22", features = ["json"] }
+uuid = { version = "1.20.0", features = ["v4"] }
diff --git a/config.yaml b/config.yaml
@@ -0,0 +1,19 @@
+logging:
+ file:
+ level: trace
+ directory: log
+ prefix: jannie
+
+root: /home/andy
+
+filters:
+ - path: my-space/projects/python
+ type: whitelist
+ - path: my-space/projects/python/sound
+ type: blacklist
+ - path: my-space/projects/rust
+ type: whitelist
+ - path: my-space/projects/rust/jannie
+ type: blacklist
+ - path: my-space/projects/rust/jannie/log
+ type: whitelist
diff --git a/inventory.json b/inventory.json
diff --git a/justfile b/justfile
@@ -0,0 +1,8 @@
+set quiet := true
+
+default:
+ just --list
+
+# Nextest all workspace packages
+test:
+ RUST_LOG=trace cargo nextest run --workspace --no-fail-fast
diff --git a/src/args.rs b/src/args.rs
@@ -0,0 +1,8 @@
+use std::path::PathBuf;
+
+#[derive(clap::Parser, Debug)]
+pub struct Args {
+ /// Configuration and inventory
+ #[arg(short, long, default_value = ".")]
+ pub dir: PathBuf,
+}
diff --git a/src/config/logging.rs b/src/config/logging.rs
@@ -0,0 +1,103 @@
+use std::path::PathBuf;
+
+use tracing::Level;
+
+#[derive(Clone, Debug, Default, serde::Deserialize)]
+pub struct LoggingConfig {
+ #[serde(default)]
+ pub file: Option<FileConfig>,
+}
+
+#[derive(Clone, Debug, serde::Deserialize)]
+pub struct FileConfig {
+ pub directory: PathBuf,
+ pub prefix: String,
+ #[serde(flatten)]
+ pub subscriber: SubscriberConfig,
+}
+
+#[derive(Clone, Debug, serde::Deserialize)]
+pub struct SubscriberConfig {
+ #[serde(default)]
+ pub format: LogFormat,
+ #[serde(default)]
+ pub level: LogLevel,
+ #[serde(default)]
+ pub target_filters: Vec<TargetFilter>,
+ #[serde(default = "SubscriberConfig::default_ansi")]
+ pub ansi: bool,
+ #[serde(default = "SubscriberConfig::default_target")]
+ pub target: bool,
+ #[serde(default = "SubscriberConfig::default_file_path")]
+ pub file_path: bool,
+ #[serde(default = "SubscriberConfig::default_line_number")]
+ pub line_number: bool,
+}
+
+impl SubscriberConfig {
+ pub fn default_ansi() -> bool {
+ false
+ }
+
+ pub fn default_target() -> bool {
+ true
+ }
+
+ pub fn default_file_path() -> bool {
+ false
+ }
+
+ pub fn default_line_number() -> bool {
+ false
+ }
+}
+
+#[derive(Clone, Debug, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub struct TargetFilter {
+ #[serde(default)]
+ #[serde(rename = "type")]
+ pub filter_type: TargetFilterType,
+ pub pattern: String,
+}
+
+#[derive(Clone, Debug, Copy, Default, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum TargetFilterType {
+ #[default]
+ Whitelist,
+ Blacklist,
+}
+
+#[derive(Clone, Debug, Copy, Default, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum LogFormat {
+ #[default]
+ Plain,
+ Json,
+}
+
+#[derive(Clone, Debug, Copy, Default, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum LogLevel {
+ #[default]
+ Off,
+ Error,
+ Warn,
+ Info,
+ Debug,
+ Trace,
+}
+
+impl From<LogLevel> for Option<Level> {
+ fn from(value: LogLevel) -> Self {
+ match value {
+ LogLevel::Off => None,
+ LogLevel::Error => Some(Level::ERROR),
+ LogLevel::Warn => Some(Level::WARN),
+ LogLevel::Info => Some(Level::INFO),
+ LogLevel::Debug => Some(Level::DEBUG),
+ LogLevel::Trace => Some(Level::TRACE),
+ }
+ }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
@@ -0,0 +1,63 @@
+use std::path::{Path, PathBuf};
+
+pub mod logging;
+
+#[derive(Clone, Debug, serde::Deserialize)]
+pub struct Config {
+ #[serde(default)]
+ pub logging: logging::LoggingConfig,
+ pub root: PathBuf,
+ #[serde(default)]
+ pub filters: Vec<Filter>,
+}
+
+impl Config {
+ pub fn new(config_path: &Path) -> Result<Self, Error> {
+ let config = config::Config::builder()
+ .add_source(config::File::from(config_path))
+ .build()?;
+ Ok(config.try_deserialize()?)
+ }
+}
+
+#[derive(Clone, Debug, serde::Deserialize)]
+pub struct Filter {
+ pub path: PathBuf,
+ #[serde(default)]
+ #[serde(rename = "type")]
+ pub filter_type: FilterType,
+}
+
+impl Filter {
+ pub fn is_whitelist(&self) -> bool {
+ self.filter_type.is_whitelist()
+ }
+
+ pub fn is_blacklist(&self) -> bool {
+ self.filter_type.is_blacklist()
+ }
+}
+
+#[derive(Clone, Debug, Default, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum FilterType {
+ #[default]
+ Whitelist,
+ Blacklist,
+}
+
+impl FilterType {
+ pub fn is_whitelist(&self) -> bool {
+ matches!(self, FilterType::Whitelist)
+ }
+
+ pub fn is_blacklist(&self) -> bool {
+ matches!(self, FilterType::Blacklist)
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Error building config: {0}")]
+ Build(#[from] config::ConfigError),
+}
diff --git a/src/filetree/mod.rs b/src/filetree/mod.rs
@@ -0,0 +1,195 @@
+use std::{
+ cell::{BorrowError, BorrowMutError, RefCell},
+ collections::BTreeMap,
+ path::{Path, PathBuf},
+ rc::Rc,
+};
+
+use indexmap::IndexMap;
+use uuid::Uuid;
+
+use crate::filetree::node::Node;
+
+pub mod node;
+
+pub type NodeRef<M> = Rc<RefCell<Node<M>>>;
+
+pub struct Filetree<M> {
+ nodes: IndexMap<Uuid, NodeRef<M>>,
+ paths: BTreeMap<PathBuf, Uuid>,
+ root: Uuid,
+}
+
+impl<M> Filetree<M> {
+ pub fn new(root: Node<M>) -> Self {
+ // todo: check root better
+ assert!(root.is_leaf());
+ let mut filetree = Filetree {
+ nodes: IndexMap::new(),
+ paths: BTreeMap::new(),
+ root: root.id(),
+ };
+ filetree.insert_node(root, None).unwrap();
+ filetree
+ }
+
+ pub fn root(&self) -> NodeRef<M> {
+ Rc::clone(
+ self.nodes
+ .get(&self.root)
+ .expect("Filetree should have root"),
+ )
+ }
+
+ pub fn nodes(&self) -> impl Iterator<Item = NodeRef<M>> {
+ self.nodes.values().map(Rc::clone)
+ }
+
+ pub fn node(&self, node_id: Uuid) -> Option<NodeRef<M>> {
+ self.nodes.get(&node_id).map(Rc::clone)
+ }
+
+ pub fn node_by_path(&self, path: &Path) -> Option<NodeRef<M>> {
+ self.paths
+ .get(path)
+ .map(|node_id| self.nodes.get(node_id).expect("Filetree should have node"))
+ .map(Rc::clone)
+ }
+}
+
+impl<M> Filetree<M> {
+ pub fn insert(&mut self, node: Node<M>) -> Result<(), Error> {
+ let mut path = node.path().to_owned();
+ tracing::debug!("Trying to add node with path {path:?}");
+ if path.ends_with("..") {
+ return Err(Error::ParentDir);
+ }
+ if path.ends_with(".") {
+ return Err(Error::CurrentDir);
+ }
+ if self.paths.get(&path).is_some() {
+ return Err(Error::NodeExists);
+ }
+ path.pop();
+ tracing::trace!("Searching for node with path {path:?}");
+ if let Some(parent) = self.paths.get(&path).copied() {
+ self.insert_node(node, Some(parent))?;
+ Ok(())
+ } else {
+ Err(Error::MissingPath)
+ }
+ }
+
+ fn insert_node(&mut self, mut node: Node<M>, parent: Option<Uuid>) -> Result<(), Error> {
+ let node_id = node.id();
+ if let Some(parent) = parent {
+ self.nodes
+ .get(&parent)
+ .expect("Should have parent node")
+ .try_borrow_mut()?
+ .add_child(node_id);
+ node.set_parent(parent);
+ } else {
+ node.unset_parent();
+ }
+ self.paths.insert(node.path().to_owned(), node_id);
+ self.nodes.insert(node_id, Rc::new(RefCell::new(node)));
+ Ok(())
+ }
+}
+
+impl<M> Filetree<M> {
+ pub fn print(&self) -> Result<(), Error> {
+ let mut print_buffer = Vec::new();
+ print_buffer.push((0, self.root));
+ while let Some((offset, node_id)) = print_buffer.pop() {
+ let node = self
+ .nodes
+ .get(&node_id)
+ .expect("Shold have the node")
+ .try_borrow()?;
+ println!(
+ "{}| {}",
+ " ".repeat(offset),
+ node.path()
+ .file_name()
+ .map(|name| name.to_str())
+ .flatten()
+ .unwrap_or("UNKNOWN")
+ );
+ print_buffer.extend(node.children().map(|child_id| (offset + 1, child_id)));
+ }
+ Ok(())
+ }
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Error borrowing mutably: {0}")]
+ BorrowMutError(#[from] BorrowMutError),
+ #[error("Error borrowing: {0}")]
+ BorrowError(#[from] BorrowError),
+ #[error("Node already exists in the tree")]
+ NodeExists,
+ #[error("Node extends the path too much, directory does not exist")]
+ MissingPath,
+ #[error("Use of current dir is disallowed")]
+ CurrentDir,
+ #[error("Use of parent dir is disallowed")]
+ ParentDir,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test_log::test(test)]
+ fn cannot_insert_root() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("/".into(), ()))).is_err());
+ }
+
+ #[test_log::test(test)]
+ fn cannot_insert_relative() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("dir".into(), ()))).is_err());
+ }
+
+ #[test_log::test(test)]
+ fn cannot_insert_dots() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("/.".into(), ()))).is_err());
+ assert!(dbg!(tree.insert(Node::new("/..".into(), ()))).is_err());
+ }
+
+ #[test_log::test(test)]
+ fn insert_one() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("/dir".into(), ()))).is_ok());
+ }
+
+ #[test_log::test(test)]
+ fn insert_exists() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("/dir".into(), ()))).is_ok());
+ assert!(dbg!(tree.insert(Node::new("/dir".into(), ()))).is_err());
+ }
+
+ #[test_log::test(test)]
+ fn insert_many() {
+ let root = Node::new("/".into(), ());
+ let mut tree = Filetree::new(root);
+ assert!(dbg!(tree.insert(Node::new("/dir".into(), ()))).is_ok());
+ assert!(dbg!(tree.insert(Node::new("/dir/dir/dir".into(), ()))).is_err());
+ assert!(dbg!(tree.insert(Node::new("/dir/dir".into(), ()))).is_ok());
+ assert!(dbg!(tree.insert(Node::new("/dir/dir".into(), ()))).is_err());
+ assert!(dbg!(tree.insert(Node::new("/dir/dir/dir".into(), ()))).is_ok());
+ assert!(dbg!(tree.insert(Node::new("/dir2".into(), ()))).is_ok());
+ assert!(dbg!(tree.insert(Node::new("/dir3".into(), ()))).is_ok());
+ }
+}
diff --git a/src/filetree/node.rs b/src/filetree/node.rs
@@ -0,0 +1,72 @@
+use std::path::{Path, PathBuf};
+
+use indexmap::IndexSet;
+use uuid::Uuid;
+
+pub struct Node<M> {
+ id: Uuid,
+ path: PathBuf,
+ meta: M,
+ children: IndexSet<Uuid>,
+ parent: Option<Uuid>,
+}
+
+impl<M> Node<M> {
+ pub fn new(path: PathBuf, meta: M) -> Self {
+ Self {
+ id: Uuid::new_v4(),
+ path,
+ meta,
+ children: IndexSet::new(),
+ parent: None,
+ }
+ }
+
+ pub fn id(&self) -> Uuid {
+ self.id
+ }
+
+ pub fn path(&self) -> &Path {
+ &self.path
+ }
+
+ pub fn meta(&self) -> &M {
+ &self.meta
+ }
+
+ pub fn meta_mut(&mut self) -> &mut M {
+ &mut self.meta
+ }
+
+ /// A node is a leaf if it has no children
+ pub fn is_leaf(&self) -> bool {
+ self.children.is_empty()
+ }
+
+ pub fn parent(&self) -> Option<Uuid> {
+ self.parent
+ }
+
+ pub fn children(&self) -> impl Iterator<Item = Uuid> {
+ self.children.iter().copied()
+ }
+
+ pub fn children_vec(&self) -> Vec<Uuid> {
+ self.children.iter().copied().collect()
+ }
+
+ pub fn add_child(&mut self, node_id: Uuid) {
+ self.children.insert(node_id);
+ }
+
+ pub fn set_parent_opt(&mut self, parent_id: Option<Uuid>) {
+ self.parent = parent_id;
+ }
+ pub fn set_parent(&mut self, parent_id: Uuid) {
+ self.parent = Some(parent_id);
+ }
+
+ pub fn unset_parent(&mut self) {
+ self.parent = None;
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -0,0 +1,202 @@
+use std::{cell::BorrowError, collections::VecDeque, path::Path};
+
+use clap::Parser;
+
+use crate::{
+ args::Args,
+ config::Config,
+ filetree::{Filetree, node::Node},
+ state::State,
+};
+
+pub mod args;
+pub mod config;
+pub mod filetree;
+pub mod logging;
+pub mod state;
+
+pub fn result_main() -> Result<(), Error> {
+ let args = Args::parse();
+ let config_path = args.dir.join("config.yaml");
+ let config = Config::new(&config_path)?;
+
+ logging::setup_logging(&config.logging)?;
+
+ tracing::debug!("Starting with args: {args:#?}");
+ tracing::debug!("Read config: {config:#?}");
+
+ let state = State::new(&args.dir, config)?;
+
+ state.runtime.block_on(read(&state));
+
+ Ok(())
+}
+
+#[derive(Clone, Debug)]
+struct Meta {
+ blacklist: Option<bool>,
+}
+
+async fn read(state: &State) {
+ let root = Node::new(state.config.root.clone(), Meta { blacklist: None });
+ let mut filetree = Filetree::new(root);
+ let mut buffer = Vec::new();
+
+ buffer.extend(
+ state
+ .config
+ .filters
+ .iter()
+ .filter(|filter| filter.is_whitelist())
+ .map(|filter| {
+ let mut path = state.config.root.clone();
+ path.push(&filter.path);
+ path
+ }),
+ );
+
+ // This makes it so every prefix goes first.
+ buffer.sort();
+
+ let mut buffer = VecDeque::from(buffer);
+
+ while let Some(dir) = buffer.pop_front() {
+ tracing::debug!("Adding {:?} to tree", dir);
+ let mut base = dir.clone();
+ while filetree.node_by_path(&base).is_none() {
+ base.pop();
+ }
+
+ let allowed = allowed(state, &dir);
+
+ if base != dir {
+ let mut suffix = dir.strip_prefix(&base).unwrap().to_owned();
+ suffix.pop();
+
+ for component in suffix.components() {
+ base.push(component);
+ filetree
+ .insert(Node::new(base.clone(), Meta { blacklist: None }))
+ .unwrap();
+ }
+ filetree
+ .insert(Node::new(
+ dir.clone(),
+ Meta {
+ blacklist: Some(!allowed),
+ },
+ ))
+ .unwrap();
+ } else {
+ filetree
+ .node_by_path(&dir)
+ .unwrap()
+ .borrow_mut()
+ .meta_mut()
+ .blacklist = Some(!allowed);
+ }
+
+ if allowed && should_scan(state, &dir).unwrap() {
+ tracing::debug!("Scanning {:?}", dir.file_name());
+ let mut iter = tokio::fs::read_dir(&dir).await.unwrap();
+
+ while let Some(result) = iter.next_entry().await.unwrap() {
+ let path = result.path();
+ buffer.push_back(path)
+ }
+ }
+ }
+
+ print(&filetree).unwrap();
+}
+
+/// Does not check if allowed.
+/// Please check if allowed before doing this.
+fn should_scan(state: &State, path: &Path) -> Result<bool, Error> {
+ // is allowed
+ // and is a prefix of a blacklist that is not overridden or is a prefix of an inventory item
+ let inventory = if let Some(node) = state.inventory_tree.node_by_path(path) {
+ !node.try_borrow()?.is_leaf()
+ } else {
+ false
+ };
+
+ Ok(state.blacklist_tree.node_by_path(path).is_some() || inventory)
+}
+
+/// If a directory is allowed to be scanned.
+/// Whitelist has more priority than blacklist on the same level of specificity.
+fn allowed(state: &State, path: &Path) -> bool {
+ // If there is no not-overridden blacklist
+
+ let path = path.strip_prefix(&state.config.root).unwrap();
+
+ // If no such blacklist filter found such that
+ // for this blacklist filter no such whitelist found
+ // for which the blacklist is the prefix
+ !state
+ .config
+ .filters
+ .iter()
+ .filter(|filter| filter.is_blacklist())
+ .any(|bl_filter| {
+ path.starts_with(&bl_filter.path)
+ && !state
+ .config
+ .filters
+ .iter()
+ .filter(|filter| filter.is_whitelist())
+ .any(|wl_filter| {
+ path.starts_with(&wl_filter.path)
+ && wl_filter.path.starts_with(&bl_filter.path)
+ })
+ })
+}
+
+fn print(tree: &Filetree<Meta>) -> Result<(), Error> {
+ let mut print_buffer = Vec::new();
+ print_buffer.push((0, tree.root().borrow().id()));
+ while let Some((offset, node_id)) = print_buffer.pop() {
+ let node = tree.node(node_id).expect("Shold have the node");
+ match node.borrow().meta().blacklist {
+ Some(true) => println!(
+ "{}| {}, BL",
+ " ".repeat(offset),
+ node.borrow()
+ .path()
+ .file_name()
+ .map(|name| name.to_str())
+ .flatten()
+ .unwrap_or("UNKNOWN")
+ ),
+ _ => println!(
+ "{}| {}",
+ " ".repeat(offset),
+ node.borrow()
+ .path()
+ .file_name()
+ .map(|name| name.to_str())
+ .flatten()
+ .unwrap_or("UNKNOWN")
+ ),
+ };
+ print_buffer.extend(
+ node.borrow()
+ .children()
+ .map(|child_id| (offset + 1, child_id)),
+ );
+ }
+ Ok(())
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error(transparent)]
+ Config(#[from] config::Error),
+ #[error(transparent)]
+ State(#[from] state::Error),
+ #[error("Error setting up logging: {0}")]
+ Logging(#[from] logging::Error),
+ #[error("Borrow error: {0}")]
+ Borrow(#[from] BorrowError),
+}
diff --git a/src/logging.rs b/src/logging.rs
@@ -0,0 +1,86 @@
+use tracing::Level;
+use tracing_subscriber::{
+ Layer,
+ fmt::writer::BoxMakeWriter,
+ layer::{Filter, SubscriberExt},
+ util::SubscriberInitExt,
+};
+
+use crate::config::logging::{LogFormat, LoggingConfig, SubscriberConfig, TargetFilterType};
+
+/// Sets up a log implementation that logs to stdout.
+pub fn setup_logging(config: &LoggingConfig) -> Result<(), Error> {
+ let file_layer = config.file.as_ref().map(|config| {
+ let writer = BoxMakeWriter::new(tracing_appender::rolling::RollingFileAppender::new(
+ tracing_appender::rolling::Rotation::NEVER,
+ &config.directory,
+ &config.prefix,
+ ));
+ fmt_subscriber(writer, &config.subscriber)
+ });
+ let mut subscribers = Vec::new();
+ if let Some(file_layer) = file_layer {
+ subscribers.push(file_layer.boxed());
+ }
+ tracing_subscriber::registry()
+ .with(subscribers)
+ .try_init()
+ .map_err(|error| Error::TracingSubscriber(error.to_string()))?;
+ Ok(())
+}
+
+fn fmt_subscriber<S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>>(
+ writer: BoxMakeWriter,
+ config: &SubscriberConfig,
+) -> Option<impl Layer<S> + 'static> {
+ let fmt = tracing_subscriber::fmt::layer()
+ .with_writer(writer)
+ .with_ansi(config.ansi)
+ .with_target(config.target)
+ .with_file(config.file_path)
+ .with_line_number(config.line_number);
+ let fmt = match config.format {
+ LogFormat::Plain => fmt.boxed(),
+ LogFormat::Json => fmt.json().boxed(),
+ };
+ let fmt = fmt
+ .with_filter(tracing_subscriber::filter::LevelFilter::from_level(
+ Option::<Level>::from(config.level)?,
+ ))
+ .with_filter(subscriber_target_filter(config));
+ Some(fmt.boxed())
+}
+
+fn subscriber_target_filter<S: tracing::Subscriber>(
+ config: &SubscriberConfig,
+) -> impl Filter<S> + 'static {
+ let whitelist_filters = config
+ .target_filters
+ .clone()
+ .into_iter()
+ .filter(|filter| matches!(filter.filter_type, TargetFilterType::Whitelist))
+ .collect::<Vec<_>>();
+ let blacklist_filters = config
+ .target_filters
+ .clone()
+ .into_iter()
+ .filter(|filter| matches!(filter.filter_type, TargetFilterType::Blacklist))
+ .collect::<Vec<_>>();
+ tracing_subscriber::filter::FilterFn::new(move |log| {
+ let whitelisted = whitelist_filters.is_empty()
+ || whitelist_filters
+ .iter()
+ .any(|filter| log.target().contains(&filter.pattern));
+ let blacklisted = !blacklist_filters.is_empty()
+ && blacklist_filters
+ .iter()
+ .any(|filter| log.target().contains(&filter.pattern));
+ whitelisted && !blacklisted
+ })
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Error setting global subscriber: {0}")]
+ TracingSubscriber(String),
+}
diff --git a/src/main.rs b/src/main.rs
@@ -0,0 +1,5 @@
+fn main() {
+ if let Err(error) = jannie::result_main() {
+ eprintln!("{error}");
+ }
+}
diff --git a/src/state.rs b/src/state.rs
@@ -0,0 +1,92 @@
+use std::path::{Path, PathBuf};
+
+use tokio::runtime::Runtime;
+
+use crate::{
+ config::Config,
+ filetree::{self, Filetree, node::Node},
+};
+
+pub struct State {
+ pub config_dir: PathBuf,
+ pub config: Config,
+ pub runtime: Runtime,
+ pub blacklist_tree: Filetree<()>,
+ pub whitelist_tree: Filetree<()>,
+ pub inventory_tree: Filetree<()>,
+}
+
+impl State {
+ pub fn new(config_dir: &Path, config: Config) -> Result<Self, Error> {
+ // todo: read inventory
+ let mut blacklist_tree = Filetree::new(Node::new("/".into(), ()));
+ let mut whitelist_tree = Filetree::new(Node::new("/".into(), ()));
+
+ for filter in &config.filters {
+ let mut path = config.root.clone();
+ path.push(&filter.path);
+ match filter.filter_type {
+ crate::config::FilterType::Whitelist => {
+ tracing::debug!("Adding {:?} to whitelist", path);
+ add_to_tree(&mut whitelist_tree, &path)?
+ }
+ crate::config::FilterType::Blacklist => {
+ if config
+ .filters
+ .iter()
+ .filter(|filter| filter.is_whitelist())
+ .find(|filter| filter.path == path)
+ .is_some()
+ {
+ tracing::debug!("Skipping {:?} blacklist", path);
+ } else {
+ tracing::debug!("Adding {:?} to blacklist", path);
+ add_to_tree(&mut blacklist_tree, &path)?
+ }
+ }
+ }
+ }
+
+ let inventory_tree = Filetree::new(Node::new("/".into(), ()));
+ Ok(Self {
+ config_dir: config_dir.into(),
+ config,
+ runtime: tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .map_err(Error::Runtime)?,
+ blacklist_tree,
+ whitelist_tree,
+ inventory_tree,
+ })
+ }
+}
+
+fn add_to_tree(tree: &mut Filetree<()>, path: &Path) -> Result<(), Error> {
+ let mut components = path.components();
+ let mut path_buf = PathBuf::new();
+ loop {
+ let component = components.next();
+ match component {
+ Some(component) => {
+ path_buf.push(component);
+ if tree.node_by_path(&path_buf).is_none() {
+ tree.insert(Node::new(path_buf.clone(), ()))?;
+ }
+ }
+ None => {
+ assert!(tree.node_by_path(path).is_some());
+ break;
+ }
+ }
+ }
+ Ok(())
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Error building runtime: {0}")]
+ Runtime(tokio::io::Error),
+ #[error("Error building filetree: {0}")]
+ Filetree(#[from] filetree::Error),
+}