commit 107ffe6c81712bcd71a0a8158924f9d875793429
parent a33457a58df5ef8da428b277742e7306eee9a6ca
Author: Andy Khramtsov <>
Date: Tue, 14 Jul 2026 01:52:00 +0300
feat: cleanup
Diffstat:
17 files changed, 1083 insertions(+), 3346 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,951 +3,132 @@
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 = "dirs"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users",
- "windows-sys",
-]
-
-[[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 = "1.0.0-dev"
-dependencies = [
- "clap",
- "config",
- "dialoguer",
- "indexmap",
- "log",
- "serde",
- "serde_json",
- "serde_yaml",
- "shellexpand",
- "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 = "libredox"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
-dependencies = [
- "bitflags",
- "libc",
-]
-
-[[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.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
-
-[[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 = "option-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-
-[[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 = "redox_users"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
-dependencies = [
- "getrandom 0.2.16",
- "libredox",
- "thiserror",
-]
-
-[[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 = "ryu"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
-name = "serde-untagged"
-version = "0.1.9"
+name = "cfg-if"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
-dependencies = [
- "erased-serde",
- "serde",
- "serde_core",
- "typeid",
-]
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "dirs"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
- "serde_derive",
+ "dirs-sys",
]
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "dirs-sys"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
]
[[package]]
-name = "serde_json"
-version = "1.0.149"
+name = "getrandom"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
+ "cfg-if",
+ "libc",
+ "wasi",
]
[[package]]
-name = "serde_spanned"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+name = "jannie"
+version = "1.0.0-dev"
dependencies = [
- "serde_core",
+ "log",
+ "shellexpand",
]
[[package]]
-name = "serde_yaml"
-version = "0.9.34+deprecated"
+name = "libc"
+version = "0.2.179"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
-dependencies = [
- "indexmap",
- "itoa",
- "ryu",
- "serde",
- "unsafe-libyaml",
-]
+checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
[[package]]
-name = "sha2"
-version = "0.10.9"
+name = "libredox"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
+ "bitflags",
+ "libc",
]
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
+name = "log"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
-dependencies = [
- "lazy_static",
-]
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
-name = "shell-words"
-version = "1.1.1"
+name = "option-ext"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
-name = "shellexpand"
-version = "3.1.1"
+name = "proc-macro2"
+version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
- "dirs",
+ "unicode-ident",
]
[[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"
+name = "quote"
+version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
- "quote",
- "unicode-ident",
]
[[package]]
-name = "tempfile"
-version = "3.24.0"
+name = "redox_users"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
- "fastrand",
- "getrandom 0.3.4",
- "once_cell",
- "rustix",
- "windows-sys",
+ "getrandom",
+ "libredox",
+ "thiserror",
]
[[package]]
-name = "test-log"
-version = "0.2.19"
+name = "shellexpand"
+version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4"
+checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
dependencies = [
- "env_logger",
- "test-log-macros",
- "tracing-subscriber",
+ "dirs",
]
[[package]]
-name = "test-log-macros"
-version = "0.2.19"
+name = "syn"
+version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "unicode-ident",
]
[[package]]
@@ -971,312 +152,18 @@ dependencies = [
]
[[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 = "unsafe-libyaml"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
-
-[[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"
@@ -1290,41 +177,3 @@ 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
@@ -5,19 +5,5 @@ version = "1.0.0-dev"
edition = "2024"
[dependencies]
-clap = { version = "4.5.54", features = ["derive"] }
-config = "0.15.19"
-dialoguer = "0.12.0"
-indexmap = "2.13.0"
log = "0.4.33"
-serde = { version = "1.0.228", features = ["derive"] }
-serde_json = "1.0.149"
-serde_yaml = "0.9.34"
shellexpand = "3.1.1"
-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
@@ -1,21 +0,0 @@
-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/openscad
- type: whitelist
- - path: my-space/projects/rust/jannie
- type: blacklist
- - path: my-space/projects/rust/jannie/log
- type: whitelist
diff --git a/inventory.yaml b/inventory.yaml
@@ -1,2 +0,0 @@
-items:
- - path: "/home/andy/my-space/projects/openscad/cable-holder"
diff --git a/readme.md b/readme.md
@@ -4,12 +4,9 @@ This is a utility for keeping files on PC organized.
In other words, a way to document workspace.
-## Why some of the things
-
-I used `tokio` and `tracing`, because I just did it this way, no particular reason.
-
It is a thrown-together prototype of a concept I wanted to quickly explore.
-Once I use this garbage for a while I will write a better version.
+I refactored most of the dependencies out now, only `shellexpand` pulls in some
+(about 20).
# License
diff --git a/src/args.rs b/src/args.rs
@@ -1,10 +0,0 @@
-use std::path::PathBuf;
-
-#[derive(clap::Parser, Debug)]
-#[command(version)]
-#[command(about = "Enforce clean workspace", long_about = None)]
-pub struct Args {
- /// Configuration and inventory
- #[arg(short, long, default_value = "~/.config/jannie")]
- pub dir: PathBuf,
-}
diff --git a/src/config/logging.rs b/src/config/logging.rs
@@ -1,103 +0,0 @@
-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
@@ -1,73 +0,0 @@
-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 inventory_format: InventoryFormat,
- #[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, Default, serde::Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum InventoryFormat {
- #[default]
- Yaml,
- Json,
-}
-
-#[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
@@ -1,200 +0,0 @@
-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)
- }
-
- /// Checks if a node with this path exists
- pub fn check_path(&self, path: &Path) -> bool {
- self.paths.get(path).is_some()
- }
-}
-
-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
@@ -1,76 +0,0 @@
-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
- }
-
- pub fn set_meta(&mut self, meta: M) {
- self.meta = 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/inventory.rs b/src/inventory.rs
@@ -1,21 +0,0 @@
-use std::path::PathBuf;
-
-#[derive(serde::Deserialize, Debug)]
-pub struct Inventory {
- pub items: Vec<Item>,
-}
-
-#[derive(serde::Deserialize, Debug)]
-pub struct Item {
- pub path: PathBuf,
- #[serde(default)]
- pub check: Check,
-}
-
-#[derive(serde::Deserialize, Clone, Debug, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum Check {
- #[default]
- None,
- Shell(String),
-}
diff --git a/src/jannie_new.rs b/src/jannie_new.rs
@@ -1,1178 +0,0 @@
-pub mod args {
- use std::path::PathBuf;
-
- #[derive(Debug)]
- pub struct Args {
- pub config_dir: PathBuf,
- }
-
- pub fn parse_args() -> Result<Args, Error> {
- let mut args = std::env::args().skip(1);
- let mut config_dir = None;
- loop {
- let Some(arg) = args.next() else {
- break;
- };
- match arg.as_str() {
- "-d" | "--dir" => {
- let Some(value) = args.next() else {
- return Err(Error::NotEnoughArguments);
- };
- config_dir = Some(value.into());
- }
- _ => {
- return Err(Error::UnknownArgument);
- }
- }
- }
- Ok(Args {
- config_dir: config_dir.unwrap_or_else(|| "~/.config/jannie".into()),
- })
- }
-
- #[derive(Debug)]
- pub enum Error {
- UnknownArgument,
- NotEnoughArguments,
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- None
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::UnknownArgument => write!(f, "Unknown argument"),
- Error::NotEnoughArguments => write!(f, "Not enough arguments"),
- }
- }
- }
-}
-
-pub mod config {
- use std::path::PathBuf;
-
- #[derive(Clone, Debug)]
- pub struct Config {
- pub root: PathBuf,
- pub filters: Vec<Filter>,
- }
-
- #[derive(Clone, Debug)]
- pub struct Filter {
- pub path: PathBuf,
- 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)]
- 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)
- }
- }
-
- pub fn parse_config(file: &str) -> Result<Config, Error> {
- let mut root = None;
- let mut filters = Vec::new();
- let mut state = State::Global;
- let mut lines = file.lines();
- loop {
- let Some(line) = lines.next() else {
- break;
- };
- let line_trimmed = line.trim();
- if line_trimmed.starts_with("#") {
- continue;
- }
- if line_trimmed.is_empty() {
- continue;
- }
- match state {
- State::Global => {
- if line_trimmed == "filters:" {
- state = State::Filters;
- } else {
- let Some((key, value)) = line_trimmed.split_once(": ") else {
- return Err(Error::InvalidInput);
- };
- if key == "root" {
- if root.is_some() {
- return Err(Error::RootDefinedTwice);
- }
- root = Some(value.into());
- } else {
- return Err(Error::InvalidInput);
- }
- }
- }
- State::Filters => {
- let Some((key, value)) = line_trimmed.split_once(": ") else {
- return Err(Error::InvalidInput);
- };
- match key {
- "whitelist" => filters.push(Filter {
- path: value.into(),
- filter_type: FilterType::Whitelist,
- }),
- "blacklist" => filters.push(Filter {
- path: value.into(),
- filter_type: FilterType::Blacklist,
- }),
- _ => return Err(Error::InvalidInput),
- };
- }
- }
- }
- Ok(Config {
- root: root.unwrap_or_else(|| "~".into()),
- filters,
- })
- }
-
- enum State {
- Global,
- Filters,
- }
-
- #[derive(Debug)]
- pub enum Error {
- InvalidInput,
- RootDefinedTwice,
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- None
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::InvalidInput => write!(f, "Invalid config input"),
- Error::RootDefinedTwice => write!(f, "Root path defined twice"),
- }
- }
- }
-}
-
-pub mod filetree {
- pub mod node {
- use std::path::{Path, PathBuf};
- /// Allows any additional data to be provided
- pub struct Node<M> {
- id: usize,
- path: PathBuf,
- /// Any additional data
- meta: M,
- children: Vec<usize>,
- parent: Option<usize>,
- }
-
- impl<M> Node<M> {
- pub fn new(path: PathBuf, id: usize, meta: M) -> Self {
- Self {
- id,
- path,
- meta,
- children: Vec::new(),
- parent: None,
- }
- }
-
- pub fn id(&self) -> usize {
- 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
- }
-
- pub fn set_meta(&mut self, meta: M) {
- self.meta = 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<usize> {
- self.parent
- }
-
- pub fn children(&self) -> &[usize] {
- &self.children
- }
-
- pub fn add_child(&mut self, node_id: usize) {
- self.children.push(node_id);
- }
-
- pub fn set_parent_opt(&mut self, parent_id: Option<usize>) {
- self.parent = parent_id;
- }
-
- pub fn set_parent(&mut self, parent_id: usize) {
- self.parent = Some(parent_id);
- }
-
- pub fn unset_parent(&mut self) {
- self.parent = None;
- }
- }
- }
-
- use std::{
- collections::BTreeMap,
- path::{Path, PathBuf},
- };
-
- use node::Node;
-
- /// The tree currently is insert-only, it's impossible to rearrange it or
- /// remove nodes or alter its structure in other ways.
- pub struct Filetree<M> {
- /// Root is node 0
- nodes: Vec<Node<M>>,
- paths: BTreeMap<PathBuf, usize>,
- }
-
- impl<M> Filetree<M> {
- pub fn new(root_path: impl Into<PathBuf>, root_meta: M) -> Self {
- let root_path = root_path.into();
- let mut paths = BTreeMap::new();
- paths.insert(root_path.clone(), 0);
- let root = Node::new(root_path, 0, root_meta);
- Filetree {
- nodes: vec![root],
- paths,
- }
- }
-
- pub fn root(&self) -> &Node<M> {
- &self.nodes[0]
- }
-
- pub fn nodes(&self) -> &[Node<M>] {
- &self.nodes
- }
-
- pub fn get_node(&self, node_id: usize) -> Option<&Node<M>> {
- self.nodes.get(node_id)
- }
-
- fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node<M>> {
- self.nodes.get_mut(node_id)
- }
-
- pub fn get_node_by_path(&self, path: &Path) -> Option<&Node<M>> {
- self.paths
- .get(path)
- .copied()
- .map(|node_id| &self.nodes[node_id])
- }
-
- pub fn get_node_id_by_path(&self, path: &Path) -> Option<usize> {
- self.paths.get(path).copied()
- }
-
- pub fn get_meta(&self, node_id: usize) -> Option<&M> {
- Some(self.get_node(node_id)?.meta())
- }
-
- pub fn get_meta_mut(&mut self, node_id: usize) -> Option<&mut M> {
- Some(self.get_node_mut(node_id)?.meta_mut())
- }
-
- pub fn get_meta_by_path(&self, path: &Path) -> Option<&M> {
- self.get_meta(self.get_node_id_by_path(path)?)
- }
-
- pub fn get_meta_mut_by_path(&mut self, path: &Path) -> Option<&mut M> {
- self.get_meta_mut(self.get_node_id_by_path(path)?)
- }
-
- /// Checks if a node with this path exists
- pub fn check_path(&self, path: &Path) -> bool {
- self.paths.get(path).is_some()
- }
-
- pub fn insert(&mut self, path: impl Into<PathBuf>, meta: M) -> Result<(), Error> {
- let path = path.into();
- log::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.check_path(&path) {
- return Err(Error::NodeExists);
- }
- let Some(parent) = path.parent() else {
- return Err(Error::MissingPath);
- };
- log::trace!("Searching for node with path {parent:?}");
- if let Some(parent) = self.paths.get(parent).copied() {
- let id = self.nodes.len();
- let mut node = Node::new(path.clone(), id, meta);
- node.set_parent(parent);
- self.nodes.push(node);
- self.paths.insert(path, id);
- self.nodes[parent].add_child(id);
- Ok(())
- } else {
- Err(Error::MissingPath)
- }
- }
- }
-
- impl<M> Filetree<M> {
- pub fn print(&self) -> Result<(), Error> {
- let mut print_buffer = Vec::new();
- print_buffer.push((0, 0));
- while let Some((offset, node_id)) = print_buffer.pop() {
- let node = self.nodes.get(node_id).expect("Shold have the node");
- println!(
- "{}| {}",
- " ".repeat(offset),
- node.path()
- .file_name()
- .map(|name| name.to_str())
- .flatten()
- .unwrap_or("UNKNOWN")
- );
- print_buffer.extend(
- node.children()
- .iter()
- .copied()
- .map(|child_id| (offset + 1, child_id)),
- );
- }
- Ok(())
- }
- }
-
- #[derive(Debug)]
- pub enum Error {
- NodeExists,
- MissingPath,
- CurrentDir,
- ParentDir,
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- None
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::NodeExists => write!(f, "Node with such path already exists"),
- Error::MissingPath => write!(f, "Parent path is missing"),
- Error::CurrentDir => write!(f, "Used current dir (.) in path"),
- Error::ParentDir => write!(f, "Used parent dir (..) in path"),
- }
- }
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
-
- #[test]
- fn cannot_insert_root() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("/", ())).is_err());
- }
-
- #[test]
- fn cannot_insert_relative() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("dir", ())).is_err());
- }
-
- #[test]
- fn cannot_insert_dots() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("/.", ())).is_err());
- assert!(dbg!(tree.insert("/..", ())).is_err());
- }
-
- #[test]
- fn insert_one() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("/dir", ())).is_ok());
- }
-
- #[test]
- fn insert_exists() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("/dir", ())).is_ok());
- assert!(dbg!(tree.insert("/dir", ())).is_err());
- }
-
- #[test]
- fn insert_many() {
- let mut tree = Filetree::new("/", ());
- assert!(dbg!(tree.insert("/dir", ())).is_ok());
- assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_err());
- assert!(dbg!(tree.insert("/dir/dir", ())).is_ok());
- assert!(dbg!(tree.insert("/dir/dir", ())).is_err());
- assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_ok());
- assert!(dbg!(tree.insert("/dir2", ())).is_ok());
- assert!(dbg!(tree.insert("/dir3", ())).is_ok());
- }
- }
-}
-
-pub mod inventory {
- use std::path::PathBuf;
-
- #[derive(Debug)]
- pub struct Inventory {
- pub items: Vec<Item>,
- }
-
- #[derive(Debug)]
- pub struct Item {
- pub path: PathBuf,
- pub check: Check,
- }
-
- #[derive(Clone, Debug, Default)]
- pub enum Check {
- #[default]
- None,
- Shell(String),
- }
-
- pub fn parse_inventory(file: &str) -> Result<Inventory, Error> {
- let mut items = Vec::new();
- let mut state = State::Global;
- let mut lines = file.lines();
- loop {
- let Some(line) = lines.next() else {
- match state {
- State::Global => break,
- State::Item { path, check } => {
- items.push(Item {
- path,
- check: check.unwrap_or_default(),
- });
- break;
- }
- };
- };
- let line_trimmed = line.trim();
- if line_trimmed.starts_with("#") {
- continue;
- }
- if line_trimmed.is_empty() {
- continue;
- }
- match state {
- State::Global => {
- let Some((key, value)) = line_trimmed.split_once(": ") else {
- return Err(Error::InvalidInput);
- };
- if key == "path" {
- state = State::Item {
- path: value.into(),
- check: None,
- };
- } else {
- return Err(Error::InvalidInput);
- }
- }
- State::Item { path, check } => {
- let Some((key, value)) = line_trimmed.split_once(": ") else {
- return Err(Error::InvalidInput);
- };
- match key {
- "path" => {
- items.push(Item {
- path,
- check: check.unwrap_or_default(),
- });
- state = State::Item {
- path: value.into(),
- check: None,
- };
- }
- "check" => {
- if check.is_some() {
- return Err(Error::CheckDefinedTwice);
- }
- let check = if value == "none" {
- Check::None
- } else if let Some(("shell", command)) = value.split_once(" ") {
- Check::Shell(command.into())
- } else {
- return Err(Error::InvalidInput);
- };
- state = State::Item {
- path,
- check: Some(check),
- };
- }
- _ => return Err(Error::InvalidInput),
- };
- }
- }
- }
- Ok(Inventory { items })
- }
-
- enum State {
- Global,
- Item { path: PathBuf, check: Option<Check> },
- }
-
- #[derive(Debug)]
- pub enum Error {
- InvalidInput,
- CheckDefinedTwice,
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- None
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::InvalidInput => write!(f, "Invalid input"),
- Error::CheckDefinedTwice => write!(f, "Check field defined twice"),
- }
- }
- }
-}
-
-pub mod logging {
- //! Logging implementation.
- //! Currently only logs to stderr.
- //!
- //! see [`setup_logging`]
-
- use log::LevelFilter;
- use std::sync::OnceLock;
-
- static LOGGER: OnceLock<Logger> = OnceLock::new();
-
- pub struct Logger {
- level: LevelFilter,
- }
-
- impl log::Log for Logger {
- fn enabled(&self, metadata: &log::Metadata) -> bool {
- metadata.level() <= self.level
- }
-
- fn log(&self, record: &log::Record) {
- if self.enabled(record.metadata()) {
- eprintln!("[{}]: {}", record.level(), record.args());
- }
- }
-
- fn flush(&self) {}
- }
-
- /// Sets up global logger with a given level.
- ///
- /// Note: this function may only be called once.
- pub fn setup_logging(level: LevelFilter) {
- assert!(
- LOGGER.set(Logger { level }).is_ok(),
- "Should initialize logger"
- );
- log::set_logger(LOGGER.get().expect("Logger should be initialized"))
- .expect("Should set logger");
- log::set_max_level(level);
- }
-}
-
-pub mod path_processing {
- use std::{
- env::VarError,
- path::{Path, PathBuf},
- };
-
- pub fn expand_path(path: &Path) -> Result<PathBuf, Error> {
- let path = shellexpand::full(
- path.to_str()
- .ok_or_else(|| Error::Parse(path.to_path_buf()))?,
- )
- .map_err(Error::Expand)?;
- Ok(path.as_ref().into())
- }
-
- pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> {
- let path = path.canonicalize().map_err(Error::Canonicalize)?;
- Ok(path)
- }
-
- /// Expands given path and canonicalizes it
- pub fn process_path(path: &Path) -> Result<PathBuf, Error> {
- let path = expand_path(path)?;
- canonicalize_path(&path)
- }
-
- #[derive(Debug)]
- pub enum Error {
- Parse(PathBuf),
- Expand(shellexpand::LookupError<VarError>),
- Canonicalize(std::io::Error),
- }
-
- impl Error {
- pub fn is_file_not_found(&self) -> bool {
- match *self {
- Error::Canonicalize(ref error) => match error.kind() {
- std::io::ErrorKind::NotFound => true,
- _ => false,
- },
- _ => false,
- }
- }
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Error::Parse(_path) => None,
- Error::Expand(error) => Some(error),
- Error::Canonicalize(error) => Some(error),
- }
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::Parse(path) => write!(f, "Error parsing path {path:?}"),
- Error::Expand(error) => write!(f, "Error expanding path: {error}"),
- Error::Canonicalize(error) => write!(f, "Error canonicalizing path: {error}"),
- }
- }
- }
-}
-
-pub mod state {
- use std::path::{Path, PathBuf};
-
- use super::config::{Config, Filter, FilterType};
- use super::filetree::{self, Filetree};
- use super::inventory::{self, Inventory};
- use super::path_processing;
-
- pub struct State {
- pub config_dir: PathBuf,
- pub config: Config,
- pub inventory: Inventory,
- /// Paths in filters are absolute and processed
- pub filters: Vec<Filter>,
- /// Absolute and processed
- pub root: PathBuf,
- pub blacklist_tree: Filetree<()>,
- pub whitelist_tree: Filetree<()>,
- pub inventory_tree: Filetree<Option<inventory::Check>>,
- }
-
- impl State {
- /// Parses inventory, builds whitelist and blacklist trees, builds an
- /// inventory tree, normalizes root path and filters.
- pub fn new(config_dir: impl Into<PathBuf>, config: Config) -> Result<Self, Error> {
- let config_dir = config_dir.into();
-
- let inventory_path = config_dir.join("inventory.txt");
- log::trace!("Reading inventory from {inventory_path:?}");
- let inventory =
- std::fs::read_to_string(inventory_path).map_err(Error::ReadInventoryFile)?;
- log::trace!("Parsing inventory file contents");
- let inventory =
- inventory::parse_inventory(&inventory).map_err(Error::ParseInventory)?;
-
- log::trace!("Processing root path");
- let root =
- path_processing::process_path(&config.root).map_err(Error::ProcessRootPath)?;
-
- let mut filters = Vec::new();
-
- tracing::debug!("Preprocessing filters");
-
- for filter in &config.filters {
- log::trace!("Preprocessing filter {filter:?}");
- let mut path = config.root.clone();
- path.push(&filter.path);
- filters.push(Filter {
- path: match path_processing::process_path(&path) {
- Ok(path) => path,
- Err(error) if error.is_file_not_found() => {
- log::warn!("Filter path was not found: {filter:?}");
- continue;
- }
- Err(error) => Err(Error::ProcessPath(error))?,
- },
- filter_type: filter.filter_type.clone(),
- });
- }
-
- log::debug!("Building trees from filters");
-
- let mut blacklist_tree = Filetree::new("/", ());
- let mut whitelist_tree = Filetree::new("/", ());
-
- for filter in &filters {
- let path = &filter.path;
- match filter.filter_type {
- FilterType::Whitelist => {
- log::debug!("Adding {:?} to whitelist", path);
- add_to_tree(&mut whitelist_tree, &path)?
- }
- FilterType::Blacklist => {
- if filters
- .iter()
- .filter(|filter| filter.is_whitelist())
- .find(|filter| &filter.path == path)
- .is_some()
- {
- log::debug!("Skipping {:?} blacklist", path);
- } else {
- log::debug!("Adding {:?} to blacklist", path);
- add_to_tree(&mut blacklist_tree, &path)?
- }
- }
- }
- }
-
- log::debug!("Building inventory tree");
-
- let mut inventory_tree = Filetree::new("/", None);
-
- for item in &inventory.items {
- let path = match path_processing::process_path(&item.path) {
- Ok(path) => path,
- Err(error) if error.is_file_not_found() => {
- log::warn!("Inventory item was not found: {item:?}");
- continue;
- }
- Err(error) => Err(Error::ProcessPath(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 inventory_tree.get_node_by_path(&path_buf).is_none() {
- inventory_tree
- .insert(path_buf.clone(), None)
- .map_err(Error::Filetree)?;
- }
- }
- None => {
- *inventory_tree
- .get_meta_mut_by_path(&path)
- .expect("Should have added inventory item") =
- Some(item.check.clone());
- break;
- }
- }
- }
- }
-
- Ok(Self {
- config_dir: config_dir.into(),
- config,
- inventory,
- filters,
- root,
- 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.get_node_by_path(&path_buf).is_none() {
- tree.insert(path_buf.clone(), ()).map_err(Error::Filetree)?;
- }
- }
- None => {
- assert!(tree.get_node_by_path(path).is_some());
- break;
- }
- }
- }
- Ok(())
- }
-
- #[derive(Debug)]
- pub enum Error {
- ReadInventoryFile(std::io::Error),
- ProcessPath(path_processing::Error),
- ProcessRootPath(path_processing::Error),
- ParseInventory(inventory::Error),
- Filetree(filetree::Error),
- }
-
- impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Error::ReadInventoryFile(error) => Some(error),
- Error::ProcessPath(error) => Some(error),
- Error::ProcessRootPath(error) => Some(error),
- Error::ParseInventory(error) => Some(error),
- Error::Filetree(error) => Some(error),
- }
- }
- }
-
- impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::ReadInventoryFile(error) => {
- write!(f, "Error reading inventory file: {error}")
- }
- Error::ProcessPath(error) => write!(f, "Error processing path: {error}"),
- Error::ProcessRootPath(error) => write!(f, "Error processing root path: {error}"),
- Error::ParseInventory(error) => write!(f, "Error parsing inventory: {error}"),
- Error::Filetree(error) => write!(f, "Filetree error: {error}"),
- }
- }
- }
-}
-
-use std::{collections::VecDeque, path::Path};
-
-use filetree::Filetree;
-use state::State;
-
-pub fn oneshot() -> Result<(), Box<dyn std::error::Error>> {
- let args = args::parse_args().map_err(Error::ParseArgs)?;
- let config_dir =
- path_processing::process_path(&args.config_dir).map_err(Error::ProcessDirPath)?;
- let config_path = config_dir.join("config.txt");
- let config_file = std::fs::read_to_string(&config_path).map_err(Error::ReadConfigFile)?;
- let config = config::parse_config(&config_file).map_err(Error::ParseConfig)?;
-
- logging::setup_logging(log::LevelFilter::Trace);
-
- log::debug!("Starting with args: {args:#?}");
- log::debug!("Read config: {config:#?}");
-
- let state = State::new(&config_dir, config)?;
-
- log::debug!("Read inventory: {:#?}", state.inventory);
-
- read(&state)?;
-
- Ok(())
-}
-
-/// This meta is used for display tree.
-#[derive(Clone, Debug)]
-struct Meta {
- inventory: Option<Option<String>>,
- blacklist: Option<bool>,
-}
-
-/// Scan workspace and display its tree.
-fn read(state: &State) -> Result<(), Error> {
- let mut filetree = Filetree::new(
- "/",
- Meta {
- blacklist: None,
- inventory: None,
- },
- );
-
- let mut processing_buffer = Vec::new();
-
- // Add all whitelist to buffer
- for filter in state.filters.iter().filter(|filter| filter.is_whitelist()) {
- processing_buffer.push(filter.path.clone());
- }
-
- // This makes it so every prefix goes first.
- processing_buffer.sort();
-
- // Has some specific ordering so the tree is built in the correct order.
- let mut processing_buffer = VecDeque::from(processing_buffer);
-
- while let Some(item) = processing_buffer.pop_front() {
- tracing::debug!("Adding {:?} to tree", item);
-
- let allowed_to_scan = allowed_to_scan(state, &item)?;
- let inventory_message = (|| -> Result<Option<Option<String>>, Error> {
- let Some(node) = state.inventory_tree.get_node_by_path(&item) else {
- return Ok(None);
- };
- if !node.is_leaf() {
- return Ok(None);
- }
- let Some(check) = node.meta().clone() else {
- return Err(Error::NoInventoryCheckInfo);
- };
- Ok(Some(match check {
- inventory::Check::None => None,
- inventory::Check::Shell(cmd) => Some(
- String::from_utf8(
- std::process::Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .output()
- .map_err(Error::Command)?
- .stdout,
- )
- .map_err(Error::StringUtf8)?,
- ),
- }))
- })()?;
-
- if let Some(meta) = filetree.get_meta_mut_by_path(&item) {
- meta.blacklist = Some(!allowed_to_scan);
- } else {
- // The earliest prefix that already added
- let mut base = item.clone();
- while !filetree.check_path(&base) {
- base.pop();
- }
-
- // What is not yet added
- let mut suffix = item
- .strip_prefix(&base)
- .map_err(Error::BasePrefix)?
- .to_owned();
- suffix.pop();
-
- for component in suffix.components() {
- base.push(component);
- filetree
- .insert(
- base.clone(),
- Meta {
- blacklist: None,
- inventory: None,
- },
- )
- .map_err(Error::Filetree)?;
- }
- filetree
- .insert(
- item.clone(),
- Meta {
- blacklist: Some(!allowed_to_scan),
- inventory: inventory_message,
- },
- )
- .map_err(Error::Filetree)?;
- }
-
- if allowed_to_scan && should_scan(state, &item) {
- tracing::debug!("Scanning {:?}", item.file_name());
- let mut iter = std::fs::read_dir(&item).map_err(Error::ReadFs)?;
-
- let mut dir_buffer = Vec::new();
-
- while let Some(result) = iter.next().transpose().map_err(Error::ReadFs)? {
- let path = result.path();
- dir_buffer.push(path)
- }
-
- dir_buffer.sort();
-
- while let Some(path) = dir_buffer.pop() {
- processing_buffer.push_front(path);
- }
- }
- }
-
- print(&filetree)?;
-
- Ok(())
-}
-
-/// Does not check if allowed.
-/// Please check if allowed before doing this.
-fn should_scan(state: &State, path: &Path) -> bool {
- // 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.get_node_by_path(path) {
- !node.is_leaf()
- } else {
- false
- };
-
- state.blacklist_tree.check_path(path) || inventory
-}
-
-/// If a directory is allowed to be scanned.
-/// Whitelist has more priority than blacklist on the same level of specificity.
-fn allowed_to_scan(state: &State, path: &Path) -> Result<bool, Error> {
- // If there is no not-overridden blacklist
-
- // If no such blacklist filter found that blacklists the path such that
- // for this blacklist filter no such whitelist found that whitelists the
- // path back aka for which the blacklist is the prefix
- Ok(!state
- .filters
- .iter()
- .filter(|filter| filter.is_blacklist())
- .any(|bl_filter| {
- path.starts_with(&bl_filter.path)
- && !state
- .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 = VecDeque::new();
- println!(
- "{}",
- tree.root().path().to_str().unwrap_or("UNKNOWN").to_owned()
- );
- print_buffer.extend(
- tree.root()
- .children()
- .iter()
- .copied()
- .map(|child_id| (1, child_id)),
- );
- while let Some((offset, node_id)) = print_buffer.pop_front() {
- let node = tree.get_node(node_id).expect("Shold have the node");
- let name = node
- .path()
- .file_name()
- .map(|name| name.to_str())
- .flatten()
- .unwrap_or("UNKNOWN")
- .to_owned();
- let offset_text = " ".repeat(offset);
- if let Some(msg) = node.meta().inventory.as_ref() {
- let msg = msg.as_deref().unwrap_or("\x1b[31mno info\x1b[0m");
- println!(
- "{}\x1b[2m|\x1b[0m \x1b[32m{}\x1b[0m \x1b[3m{}\x1b[0m",
- offset_text, name, msg,
- );
- } else if let Some(true) = node.meta().blacklist {
- println!("{}\x1b[2m|\x1b[0m \x1b[2;9m{}\x1b[0m", offset_text, name);
- } else if node.is_leaf() {
- println!("{}\x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", offset_text, name);
- } else {
- println!("{}\x1b[2m|\x1b[0m {}", offset_text, name);
- }
- for child in node
- .children()
- .iter()
- .copied()
- .map(|child_id| (offset + 1, child_id))
- .collect::<Vec<_>>()
- .into_iter()
- .rev()
- {
- print_buffer.push_front(child);
- }
- }
- Ok(())
-}
-
-#[derive(Debug)]
-pub enum Error {
- ParseArgs(args::Error),
- ProcessDirPath(path_processing::Error),
- ReadConfigFile(std::io::Error),
- ParseConfig(config::Error),
- Filetree(filetree::Error),
- BasePrefix(std::path::StripPrefixError),
- NoInventoryCheckInfo,
- Command(std::io::Error),
- StringUtf8(std::string::FromUtf8Error),
- ReadFs(std::io::Error),
-}
-
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Error::ParseArgs(error) => Some(error),
- Error::ProcessDirPath(error) => Some(error),
- Error::ReadConfigFile(error) => Some(error),
- Error::ParseConfig(error) => Some(error),
- Error::Filetree(error) => Some(error),
- Error::BasePrefix(error) => Some(error),
- Error::NoInventoryCheckInfo => None,
- Error::Command(error) => Some(error),
- Error::StringUtf8(error) => Some(error),
- Error::ReadFs(error) => Some(error),
- }
- }
-}
-
-impl std::fmt::Display for Error {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Error::ParseArgs(error) => write!(f, "Error parsing args: {error}"),
- Error::ProcessDirPath(error) => write!(f, "Error processing directory path: {error}"),
- Error::ReadConfigFile(error) => write!(f, "Error reading config file: {error}"),
- Error::ParseConfig(error) => write!(f, "Error parsing config: {error}"),
- Error::Filetree(error) => write!(f, "Filetree error: {error}"),
- Error::BasePrefix(error) => write!(f, "Error stripping prefix: {error}"),
- Error::NoInventoryCheckInfo => write!(f, "No inventory check info in filetree"),
- Error::Command(error) => write!(f, "Error executing command: {error}"),
- Error::StringUtf8(error) => write!(f, "Error building utf8 string: {error}"),
- Error::ReadFs(error) => write!(f, "Error reading from filesystem: {error}"),
- }
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,46 +1,939 @@
-use std::{
- cell::{BorrowError, BorrowMutError},
- collections::VecDeque,
- path::{Path, StripPrefixError},
- string::FromUtf8Error,
-};
+pub mod args {
+ use std::path::PathBuf;
-use clap::Parser;
+ #[derive(Debug)]
+ pub struct Args {
+ pub config_dir: PathBuf,
+ pub debug: bool,
+ pub version: bool,
+ }
+
+ pub fn parse_args() -> Result<Args, Error> {
+ let mut args = std::env::args().skip(1);
+ let mut config_dir = None;
+ let mut debug = None;
+ let mut version = None;
+ loop {
+ let Some(arg) = args.next() else {
+ break;
+ };
+ match arg.as_str() {
+ "-d" | "--dir" => {
+ let Some(value) = args.next() else {
+ return Err(Error::NotEnoughArguments);
+ };
+ config_dir = Some(value.into());
+ }
+ "--debug" => {
+ debug = Some(true);
+ }
+ "-V" | "--version" => {
+ version = Some(true);
+ }
+ _ => {
+ return Err(Error::UnknownArgument);
+ }
+ }
+ }
+ Ok(Args {
+ config_dir: config_dir.unwrap_or_else(|| "~/.config/jannie".into()),
+ debug: debug.unwrap_or(false),
+ version: version.unwrap_or(false),
+ })
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ UnknownArgument,
+ NotEnoughArguments,
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ None
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::UnknownArgument => write!(f, "Unknown argument"),
+ Error::NotEnoughArguments => write!(f, "Not enough arguments"),
+ }
+ }
+ }
+}
+
+pub mod config {
+ use std::path::PathBuf;
+
+ #[derive(Clone, Debug)]
+ pub struct Config {
+ pub root: PathBuf,
+ pub filters: Vec<Filter>,
+ }
+
+ #[derive(Clone, Debug)]
+ pub struct Filter {
+ pub path: PathBuf,
+ 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)]
+ 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)
+ }
+ }
+
+ pub fn parse_config(file: &str) -> Result<Config, Error> {
+ let mut root = None;
+ let mut filters = Vec::new();
+ let mut state = State::Global;
+ let mut lines = file.lines();
+ loop {
+ let Some(line) = lines.next() else {
+ break;
+ };
+ let line_trimmed = line.trim();
+ if line_trimmed.starts_with("#") {
+ continue;
+ }
+ if line_trimmed.is_empty() {
+ continue;
+ }
+ match state {
+ State::Global => {
+ if line_trimmed == "filters:" {
+ state = State::Filters;
+ } else {
+ let Some((key, value)) = line_trimmed.split_once(": ") else {
+ return Err(Error::InvalidInput);
+ };
+ if key == "root" {
+ if root.is_some() {
+ return Err(Error::RootDefinedTwice);
+ }
+ root = Some(value.into());
+ } else {
+ return Err(Error::InvalidInput);
+ }
+ }
+ }
+ State::Filters => {
+ let Some((key, value)) = line_trimmed.split_once(": ") else {
+ return Err(Error::InvalidInput);
+ };
+ match key {
+ "whitelist" => filters.push(Filter {
+ path: value.into(),
+ filter_type: FilterType::Whitelist,
+ }),
+ "blacklist" => filters.push(Filter {
+ path: value.into(),
+ filter_type: FilterType::Blacklist,
+ }),
+ _ => return Err(Error::InvalidInput),
+ };
+ }
+ }
+ }
+ Ok(Config {
+ root: root.unwrap_or_else(|| "~".into()),
+ filters,
+ })
+ }
+
+ enum State {
+ Global,
+ Filters,
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ InvalidInput,
+ RootDefinedTwice,
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ None
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::InvalidInput => write!(f, "Invalid config input"),
+ Error::RootDefinedTwice => write!(f, "Root path defined twice"),
+ }
+ }
+ }
+}
+
+pub mod filetree {
+ pub mod node {
+ use std::path::{Path, PathBuf};
+ /// Allows any additional data to be provided
+ pub struct Node<M> {
+ id: usize,
+ path: PathBuf,
+ /// Any additional data
+ meta: M,
+ children: Vec<usize>,
+ parent: Option<usize>,
+ }
+
+ impl<M> Node<M> {
+ pub fn new(path: PathBuf, id: usize, meta: M) -> Self {
+ Self {
+ id,
+ path,
+ meta,
+ children: Vec::new(),
+ parent: None,
+ }
+ }
+
+ pub fn id(&self) -> usize {
+ 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
+ }
+
+ pub fn set_meta(&mut self, meta: M) {
+ self.meta = 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<usize> {
+ self.parent
+ }
+
+ pub fn children(&self) -> &[usize] {
+ &self.children
+ }
+
+ pub fn add_child(&mut self, node_id: usize) {
+ self.children.push(node_id);
+ }
+
+ pub fn set_parent_opt(&mut self, parent_id: Option<usize>) {
+ self.parent = parent_id;
+ }
+
+ pub fn set_parent(&mut self, parent_id: usize) {
+ self.parent = Some(parent_id);
+ }
+
+ pub fn unset_parent(&mut self) {
+ self.parent = None;
+ }
+ }
+ }
+
+ use std::{
+ collections::BTreeMap,
+ path::{Path, PathBuf},
+ };
+
+ use node::Node;
+
+ /// The tree currently is insert-only, it's impossible to rearrange it or
+ /// remove nodes or alter its structure in other ways.
+ pub struct Filetree<M> {
+ /// Root is node 0
+ nodes: Vec<Node<M>>,
+ paths: BTreeMap<PathBuf, usize>,
+ }
+
+ impl<M> Filetree<M> {
+ pub fn new(root_path: impl Into<PathBuf>, root_meta: M) -> Self {
+ let root_path = root_path.into();
+ let mut paths = BTreeMap::new();
+ paths.insert(root_path.clone(), 0);
+ let root = Node::new(root_path, 0, root_meta);
+ Filetree {
+ nodes: vec![root],
+ paths,
+ }
+ }
+
+ pub fn root(&self) -> &Node<M> {
+ &self.nodes[0]
+ }
+
+ pub fn nodes(&self) -> &[Node<M>] {
+ &self.nodes
+ }
+
+ pub fn get_node(&self, node_id: usize) -> Option<&Node<M>> {
+ self.nodes.get(node_id)
+ }
+
+ fn get_node_mut(&mut self, node_id: usize) -> Option<&mut Node<M>> {
+ self.nodes.get_mut(node_id)
+ }
+
+ pub fn get_node_by_path(&self, path: &Path) -> Option<&Node<M>> {
+ self.paths
+ .get(path)
+ .copied()
+ .map(|node_id| &self.nodes[node_id])
+ }
+
+ pub fn get_node_id_by_path(&self, path: &Path) -> Option<usize> {
+ self.paths.get(path).copied()
+ }
+
+ pub fn get_meta(&self, node_id: usize) -> Option<&M> {
+ Some(self.get_node(node_id)?.meta())
+ }
+
+ pub fn get_meta_mut(&mut self, node_id: usize) -> Option<&mut M> {
+ Some(self.get_node_mut(node_id)?.meta_mut())
+ }
+
+ pub fn get_meta_by_path(&self, path: &Path) -> Option<&M> {
+ self.get_meta(self.get_node_id_by_path(path)?)
+ }
+
+ pub fn get_meta_mut_by_path(&mut self, path: &Path) -> Option<&mut M> {
+ self.get_meta_mut(self.get_node_id_by_path(path)?)
+ }
+
+ /// Checks if a node with this path exists
+ pub fn check_path(&self, path: &Path) -> bool {
+ self.paths.get(path).is_some()
+ }
+
+ pub fn insert(&mut self, path: impl Into<PathBuf>, meta: M) -> Result<(), Error> {
+ let path = path.into();
+ log::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.check_path(&path) {
+ return Err(Error::NodeExists);
+ }
+ let Some(parent) = path.parent() else {
+ return Err(Error::MissingPath);
+ };
+ log::trace!("Searching for node with path {parent:?}");
+ if let Some(parent) = self.paths.get(parent).copied() {
+ let id = self.nodes.len();
+ let mut node = Node::new(path.clone(), id, meta);
+ node.set_parent(parent);
+ self.nodes.push(node);
+ self.paths.insert(path, id);
+ self.nodes[parent].add_child(id);
+ Ok(())
+ } else {
+ Err(Error::MissingPath)
+ }
+ }
+ }
+
+ impl<M> Filetree<M> {
+ pub fn print(&self) -> Result<(), Error> {
+ let mut print_buffer = Vec::new();
+ print_buffer.push((0, 0));
+ while let Some((offset, node_id)) = print_buffer.pop() {
+ let node = self.nodes.get(node_id).expect("Shold have the node");
+ println!(
+ "{}| {}",
+ " ".repeat(offset),
+ node.path()
+ .file_name()
+ .map(|name| name.to_str())
+ .flatten()
+ .unwrap_or("UNKNOWN")
+ );
+ print_buffer.extend(
+ node.children()
+ .iter()
+ .copied()
+ .map(|child_id| (offset + 1, child_id)),
+ );
+ }
+ Ok(())
+ }
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ NodeExists,
+ MissingPath,
+ CurrentDir,
+ ParentDir,
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ None
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::NodeExists => write!(f, "Node with such path already exists"),
+ Error::MissingPath => write!(f, "Parent path is missing"),
+ Error::CurrentDir => write!(f, "Used current dir (.) in path"),
+ Error::ParentDir => write!(f, "Used parent dir (..) in path"),
+ }
+ }
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
-use crate::{
- args::Args,
- config::Config,
- filetree::{Filetree, node::Node},
- state::State,
-};
+ #[test]
+ fn cannot_insert_root() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("/", ())).is_err());
+ }
+
+ #[test]
+ fn cannot_insert_relative() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("dir", ())).is_err());
+ }
+
+ #[test]
+ fn cannot_insert_dots() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("/.", ())).is_err());
+ assert!(dbg!(tree.insert("/..", ())).is_err());
+ }
+
+ #[test]
+ fn insert_one() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("/dir", ())).is_ok());
+ }
+
+ #[test]
+ fn insert_exists() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("/dir", ())).is_ok());
+ assert!(dbg!(tree.insert("/dir", ())).is_err());
+ }
+
+ #[test]
+ fn insert_many() {
+ let mut tree = Filetree::new("/", ());
+ assert!(dbg!(tree.insert("/dir", ())).is_ok());
+ assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_err());
+ assert!(dbg!(tree.insert("/dir/dir", ())).is_ok());
+ assert!(dbg!(tree.insert("/dir/dir", ())).is_err());
+ assert!(dbg!(tree.insert("/dir/dir/dir", ())).is_ok());
+ assert!(dbg!(tree.insert("/dir2", ())).is_ok());
+ assert!(dbg!(tree.insert("/dir3", ())).is_ok());
+ }
+ }
+}
+
+pub mod inventory {
+ use std::path::PathBuf;
+
+ #[derive(Debug)]
+ pub struct Inventory {
+ pub items: Vec<Item>,
+ }
+
+ #[derive(Debug)]
+ pub struct Item {
+ pub path: PathBuf,
+ pub check: Check,
+ }
+
+ #[derive(Clone, Debug, Default)]
+ pub enum Check {
+ #[default]
+ None,
+ Shell(String),
+ }
+
+ pub fn parse_inventory(file: &str) -> Result<Inventory, Error> {
+ let mut items = Vec::new();
+ let mut state = State::Global;
+ let mut lines = file.lines();
+ loop {
+ let Some(line) = lines.next() else {
+ match state {
+ State::Global => break,
+ State::Item { path, check } => {
+ items.push(Item {
+ path,
+ check: check.unwrap_or_default(),
+ });
+ break;
+ }
+ };
+ };
+ let line_trimmed = line.trim();
+ if line_trimmed.starts_with("#") {
+ continue;
+ }
+ if line_trimmed.is_empty() {
+ continue;
+ }
+ match state {
+ State::Global => {
+ let Some((key, value)) = line_trimmed.split_once(": ") else {
+ return Err(Error::InvalidInput);
+ };
+ if key == "path" {
+ state = State::Item {
+ path: value.into(),
+ check: None,
+ };
+ } else {
+ return Err(Error::InvalidInput);
+ }
+ }
+ State::Item { path, check } => {
+ let Some((key, value)) = line_trimmed.split_once(": ") else {
+ return Err(Error::InvalidInput);
+ };
+ match key {
+ "path" => {
+ items.push(Item {
+ path,
+ check: check.unwrap_or_default(),
+ });
+ state = State::Item {
+ path: value.into(),
+ check: None,
+ };
+ }
+ "check" => {
+ if check.is_some() {
+ return Err(Error::CheckDefinedTwice);
+ }
+ let check = if value == "none" {
+ Check::None
+ } else if let Some(("shell", command)) = value.split_once(" ") {
+ Check::Shell(command.into())
+ } else {
+ return Err(Error::InvalidInput);
+ };
+ state = State::Item {
+ path,
+ check: Some(check),
+ };
+ }
+ _ => return Err(Error::InvalidInput),
+ };
+ }
+ }
+ }
+ Ok(Inventory { items })
+ }
+
+ enum State {
+ Global,
+ Item { path: PathBuf, check: Option<Check> },
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ InvalidInput,
+ CheckDefinedTwice,
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ None
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::InvalidInput => write!(f, "Invalid input"),
+ Error::CheckDefinedTwice => write!(f, "Check field defined twice"),
+ }
+ }
+ }
+}
+
+pub mod logging {
+ //! Logging implementation.
+ //! Currently only logs to stderr.
+ //!
+ //! see [`setup_logging`]
+
+ use log::LevelFilter;
+ use std::sync::OnceLock;
+
+ static LOGGER: OnceLock<Logger> = OnceLock::new();
+
+ pub struct Logger {
+ level: LevelFilter,
+ }
+
+ impl log::Log for Logger {
+ fn enabled(&self, metadata: &log::Metadata) -> bool {
+ metadata.level() <= self.level
+ }
+
+ fn log(&self, record: &log::Record) {
+ if self.enabled(record.metadata()) {
+ eprintln!("[{}]: {}", record.level(), record.args());
+ }
+ }
+
+ fn flush(&self) {}
+ }
+
+ /// Sets up global logger with a given level.
+ ///
+ /// Note: this function may only be called once.
+ pub fn setup_logging(level: LevelFilter) {
+ assert!(
+ LOGGER.set(Logger { level }).is_ok(),
+ "Should initialize logger"
+ );
+ log::set_logger(LOGGER.get().expect("Logger should be initialized"))
+ .expect("Should set logger");
+ log::set_max_level(level);
+ }
+}
+
+pub mod path_processing {
+ use std::{
+ env::VarError,
+ path::{Path, PathBuf},
+ };
+
+ pub fn expand_path(path: &Path) -> Result<PathBuf, Error> {
+ let path = shellexpand::full(
+ path.to_str()
+ .ok_or_else(|| Error::Parse(path.to_path_buf()))?,
+ )
+ .map_err(Error::Expand)?;
+ Ok(path.as_ref().into())
+ }
+
+ pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> {
+ let path = path.canonicalize().map_err(Error::Canonicalize)?;
+ Ok(path)
+ }
+
+ /// Expands given path and canonicalizes it
+ pub fn process_path(path: &Path) -> Result<PathBuf, Error> {
+ let path = expand_path(path)?;
+ canonicalize_path(&path)
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ Parse(PathBuf),
+ Expand(shellexpand::LookupError<VarError>),
+ Canonicalize(std::io::Error),
+ }
+
+ impl Error {
+ pub fn is_file_not_found(&self) -> bool {
+ match *self {
+ Error::Canonicalize(ref error) => match error.kind() {
+ std::io::ErrorKind::NotFound => true,
+ _ => false,
+ },
+ _ => false,
+ }
+ }
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::Parse(_path) => None,
+ Error::Expand(error) => Some(error),
+ Error::Canonicalize(error) => Some(error),
+ }
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::Parse(path) => write!(f, "Error parsing path {path:?}"),
+ Error::Expand(error) => write!(f, "Error expanding path: {error}"),
+ Error::Canonicalize(error) => write!(f, "Error canonicalizing path: {error}"),
+ }
+ }
+ }
+}
+
+pub mod state {
+ use std::path::{Path, PathBuf};
+
+ use super::config::{Config, Filter, FilterType};
+ use super::filetree::{self, Filetree};
+ use super::inventory::{self, Inventory};
+ use super::path_processing;
+
+ pub struct State {
+ pub config_dir: PathBuf,
+ pub config: Config,
+ pub inventory: Inventory,
+ /// Paths in filters are absolute and processed
+ pub filters: Vec<Filter>,
+ /// Absolute and processed
+ pub root: PathBuf,
+ pub blacklist_tree: Filetree<()>,
+ pub whitelist_tree: Filetree<()>,
+ pub inventory_tree: Filetree<Option<inventory::Check>>,
+ }
+
+ impl State {
+ /// Parses inventory, builds whitelist and blacklist trees, builds an
+ /// inventory tree, normalizes root path and filters.
+ pub fn new(config_dir: impl Into<PathBuf>, config: Config) -> Result<Self, Error> {
+ let config_dir = config_dir.into();
+
+ let inventory_path = config_dir.join("inventory.txt");
+ log::trace!("Reading inventory from {inventory_path:?}");
+ let inventory =
+ std::fs::read_to_string(inventory_path).map_err(Error::ReadInventoryFile)?;
+ log::trace!("Parsing inventory file contents");
+ let inventory =
+ inventory::parse_inventory(&inventory).map_err(Error::ParseInventory)?;
-pub mod args;
-pub mod config;
-pub mod filetree;
-pub mod inventory;
-pub mod jannie_new;
-pub mod logging;
-pub mod path_processing;
-pub mod state;
+ log::trace!("Processing root path");
+ let root =
+ path_processing::process_path(&config.root).map_err(Error::ProcessRootPath)?;
-/// `main`, but returns [`Result`].
-pub fn result_main() -> Result<(), Error> {
- let args = Args::parse();
- let dir = path_processing::process_path(&args.dir).map_err(Error::ProcessDirPath)?;
+ let mut filters = Vec::new();
- let config_path = dir.join("config.yaml");
- let config = Config::new(&config_path)?;
+ log::debug!("Preprocessing filters");
- logging::setup_logging(&config.logging)?;
+ for filter in &config.filters {
+ log::trace!("Preprocessing filter {filter:?}");
+ let mut path = config.root.clone();
+ path.push(&filter.path);
+ filters.push(Filter {
+ path: match path_processing::process_path(&path) {
+ Ok(path) => path,
+ Err(error) if error.is_file_not_found() => {
+ log::warn!("Filter path was not found: {filter:?}");
+ continue;
+ }
+ Err(error) => Err(Error::ProcessPath(error))?,
+ },
+ filter_type: filter.filter_type.clone(),
+ });
+ }
+
+ log::debug!("Building trees from filters");
- tracing::debug!("Starting with args: {args:#?}");
- tracing::debug!("Read config: {config:#?}");
+ let mut blacklist_tree = Filetree::new("/", ());
+ let mut whitelist_tree = Filetree::new("/", ());
+
+ for filter in &filters {
+ let path = &filter.path;
+ match filter.filter_type {
+ FilterType::Whitelist => {
+ log::debug!("Adding {:?} to whitelist", path);
+ add_to_tree(&mut whitelist_tree, &path)?
+ }
+ FilterType::Blacklist => {
+ if filters
+ .iter()
+ .filter(|filter| filter.is_whitelist())
+ .find(|filter| &filter.path == path)
+ .is_some()
+ {
+ log::debug!("Skipping {:?} blacklist", path);
+ } else {
+ log::debug!("Adding {:?} to blacklist", path);
+ add_to_tree(&mut blacklist_tree, &path)?
+ }
+ }
+ }
+ }
- let state = State::new(&dir, config)?;
+ log::debug!("Building inventory tree");
- tracing::debug!("Read inventory: {:#?}", state.inventory);
+ let mut inventory_tree = Filetree::new("/", None);
- state.runtime.block_on(read(&state))?;
+ for item in &inventory.items {
+ let path = match path_processing::process_path(&item.path) {
+ Ok(path) => path,
+ Err(error) if error.is_file_not_found() => {
+ log::warn!("Inventory item was not found: {item:?}");
+ continue;
+ }
+ Err(error) => Err(Error::ProcessPath(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 inventory_tree.get_node_by_path(&path_buf).is_none() {
+ inventory_tree
+ .insert(path_buf.clone(), None)
+ .map_err(Error::Filetree)?;
+ }
+ }
+ None => {
+ *inventory_tree
+ .get_meta_mut_by_path(&path)
+ .expect("Should have added inventory item") =
+ Some(item.check.clone());
+ break;
+ }
+ }
+ }
+ }
+
+ Ok(Self {
+ config_dir: config_dir.into(),
+ config,
+ inventory,
+ filters,
+ root,
+ 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.get_node_by_path(&path_buf).is_none() {
+ tree.insert(path_buf.clone(), ()).map_err(Error::Filetree)?;
+ }
+ }
+ None => {
+ assert!(tree.get_node_by_path(path).is_some());
+ break;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ #[derive(Debug)]
+ pub enum Error {
+ ReadInventoryFile(std::io::Error),
+ ProcessPath(path_processing::Error),
+ ProcessRootPath(path_processing::Error),
+ ParseInventory(inventory::Error),
+ Filetree(filetree::Error),
+ }
+
+ impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::ReadInventoryFile(error) => Some(error),
+ Error::ProcessPath(error) => Some(error),
+ Error::ProcessRootPath(error) => Some(error),
+ Error::ParseInventory(error) => Some(error),
+ Error::Filetree(error) => Some(error),
+ }
+ }
+ }
+
+ impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::ReadInventoryFile(error) => {
+ write!(f, "Error reading inventory file: {error}")
+ }
+ Error::ProcessPath(error) => write!(f, "Error processing path: {error}"),
+ Error::ProcessRootPath(error) => write!(f, "Error processing root path: {error}"),
+ Error::ParseInventory(error) => write!(f, "Error parsing inventory: {error}"),
+ Error::Filetree(error) => write!(f, "Filetree error: {error}"),
+ }
+ }
+ }
+}
+
+use std::{collections::VecDeque, path::Path};
+
+use filetree::Filetree;
+use state::State;
+
+pub fn oneshot() -> Result<(), Box<dyn std::error::Error>> {
+ let args = args::parse_args().map_err(Error::ParseArgs)?;
+
+ if args.version {
+ println!("jannie {}", env!("CARGO_PKG_VERSION"));
+ return Ok(());
+ }
+
+ let config_dir =
+ path_processing::process_path(&args.config_dir).map_err(Error::ProcessDirPath)?;
+ let config_path = config_dir.join("config.txt");
+ let config_file = std::fs::read_to_string(&config_path).map_err(Error::ReadConfigFile)?;
+ let config = config::parse_config(&config_file).map_err(Error::ParseConfig)?;
+
+ logging::setup_logging(if args.debug {
+ log::LevelFilter::Trace
+ } else {
+ log::LevelFilter::Warn
+ });
+
+ log::debug!("Starting with args: {args:#?}");
+ log::debug!("Read config: {config:#?}");
+
+ let state = State::new(&config_dir, config)?;
+
+ log::debug!("Read inventory: {:#?}", state.inventory);
+
+ read(&state)?;
Ok(())
}
@@ -53,15 +946,14 @@ struct Meta {
}
/// Scan workspace and display its tree.
-async fn read(state: &State) -> Result<(), Error> {
- let root = Node::new(
- "/".into(),
+fn read(state: &State) -> Result<(), Error> {
+ let mut filetree = Filetree::new(
+ "/",
Meta {
blacklist: None,
inventory: None,
},
);
- let mut filetree = Filetree::new(root);
let mut processing_buffer = Vec::new();
@@ -77,14 +969,13 @@ async fn read(state: &State) -> Result<(), Error> {
let mut processing_buffer = VecDeque::from(processing_buffer);
while let Some(item) = processing_buffer.pop_front() {
- tracing::debug!("Adding {:?} to tree", item);
+ log::debug!("Adding {:?} to tree", item);
let allowed_to_scan = allowed_to_scan(state, &item)?;
let inventory_message = (|| -> Result<Option<Option<String>>, Error> {
- let Some(node) = state.inventory_tree.node_by_path(&item) else {
+ let Some(node) = state.inventory_tree.get_node_by_path(&item) else {
return Ok(None);
};
- let node = node.try_borrow()?;
if !node.is_leaf() {
return Ok(None);
}
@@ -93,19 +984,22 @@ async fn read(state: &State) -> Result<(), Error> {
};
Ok(Some(match check {
inventory::Check::None => None,
- inventory::Check::Shell(cmd) => Some(String::from_utf8(
- std::process::Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .output()
- .map_err(Error::Command)?
- .stdout,
- )?),
+ inventory::Check::Shell(cmd) => Some(
+ String::from_utf8(
+ std::process::Command::new("sh")
+ .arg("-c")
+ .arg(cmd)
+ .output()
+ .map_err(Error::Command)?
+ .stdout,
+ )
+ .map_err(Error::StringUtf8)?,
+ ),
}))
})()?;
- if let Some(node) = filetree.node_by_path(&item) {
- node.try_borrow_mut()?.meta_mut().blacklist = Some(!allowed_to_scan);
+ if let Some(meta) = filetree.get_meta_mut_by_path(&item) {
+ meta.blacklist = Some(!allowed_to_scan);
} else {
// The earliest prefix that already added
let mut base = item.clone();
@@ -122,30 +1016,34 @@ async fn read(state: &State) -> Result<(), Error> {
for component in suffix.components() {
base.push(component);
- filetree.insert(Node::new(
- base.clone(),
+ filetree
+ .insert(
+ base.clone(),
+ Meta {
+ blacklist: None,
+ inventory: None,
+ },
+ )
+ .map_err(Error::Filetree)?;
+ }
+ filetree
+ .insert(
+ item.clone(),
Meta {
- blacklist: None,
- inventory: None,
+ blacklist: Some(!allowed_to_scan),
+ inventory: inventory_message,
},
- ))?;
- }
- filetree.insert(Node::new(
- item.clone(),
- Meta {
- blacklist: Some(!allowed_to_scan),
- inventory: inventory_message,
- },
- ))?;
+ )
+ .map_err(Error::Filetree)?;
}
- if allowed_to_scan && should_scan(state, &item)? {
- tracing::debug!("Scanning {:?}", item.file_name());
- let mut iter = tokio::fs::read_dir(&item).await.map_err(Error::ReadFs)?;
+ if allowed_to_scan && should_scan(state, &item) {
+ log::debug!("Scanning {:?}", item.file_name());
+ let mut iter = std::fs::read_dir(&item).map_err(Error::ReadFs)?;
let mut dir_buffer = Vec::new();
- while let Some(result) = iter.next_entry().await.map_err(Error::ReadFs)? {
+ while let Some(result) = iter.next().transpose().map_err(Error::ReadFs)? {
let path = result.path();
dir_buffer.push(path)
}
@@ -165,16 +1063,16 @@ async fn read(state: &State) -> Result<(), Error> {
/// Does not check if allowed.
/// Please check if allowed before doing this.
-fn should_scan(state: &State, path: &Path) -> Result<bool, Error> {
+fn should_scan(state: &State, path: &Path) -> bool {
// 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()
+ let inventory = if let Some(node) = state.inventory_tree.get_node_by_path(path) {
+ !node.is_leaf()
} else {
false
};
- Ok(state.blacklist_tree.check_path(path) || inventory)
+ state.blacklist_tree.check_path(path) || inventory
}
/// If a directory is allowed to be scanned.
@@ -206,23 +1104,18 @@ fn print(tree: &Filetree<Meta>) -> Result<(), Error> {
let mut print_buffer = VecDeque::new();
println!(
"{}",
- tree.root()
- .try_borrow()?
- .path()
- .to_str()
- .unwrap_or("UNKNOWN")
- .to_owned()
+ tree.root().path().to_str().unwrap_or("UNKNOWN").to_owned()
);
print_buffer.extend(
tree.root()
- .try_borrow()?
.children()
+ .iter()
+ .copied()
.map(|child_id| (1, child_id)),
);
while let Some((offset, node_id)) = print_buffer.pop_front() {
- let node = tree.node(node_id).expect("Shold have the node");
+ let node = tree.get_node(node_id).expect("Shold have the node");
let name = node
- .try_borrow()?
.path()
.file_name()
.map(|name| name.to_str())
@@ -230,22 +1123,23 @@ fn print(tree: &Filetree<Meta>) -> Result<(), Error> {
.unwrap_or("UNKNOWN")
.to_owned();
let offset_text = " ".repeat(offset);
- if let Some(msg) = node.try_borrow()?.meta().inventory.as_ref() {
+ if let Some(msg) = node.meta().inventory.as_ref() {
let msg = msg.as_deref().unwrap_or("\x1b[31mno info\x1b[0m");
println!(
"{}\x1b[2m|\x1b[0m \x1b[32m{}\x1b[0m \x1b[3m{}\x1b[0m",
offset_text, name, msg,
);
- } else if let Some(true) = node.try_borrow()?.meta().blacklist {
+ } else if let Some(true) = node.meta().blacklist {
println!("{}\x1b[2m|\x1b[0m \x1b[2;9m{}\x1b[0m", offset_text, name);
- } else if node.try_borrow()?.is_leaf() {
+ } else if node.is_leaf() {
println!("{}\x1b[2m|\x1b[0m \x1b[31m{}\x1b[0m", offset_text, name);
} else {
println!("{}\x1b[2m|\x1b[0m {}", offset_text, name);
}
for child in node
- .try_borrow()?
.children()
+ .iter()
+ .copied()
.map(|child_id| (offset + 1, child_id))
.collect::<Vec<_>>()
.into_iter()
@@ -257,30 +1151,50 @@ fn print(tree: &Filetree<Meta>) -> Result<(), Error> {
Ok(())
}
-#[derive(thiserror::Error, Debug)]
+#[derive(Debug)]
pub enum Error {
- #[error(transparent)]
- Config(#[from] config::Error),
- #[error(transparent)]
- State(#[from] state::Error),
- #[error("Error processing dir path: {0}")]
+ ParseArgs(args::Error),
ProcessDirPath(path_processing::Error),
- #[error("Error setting up logging: {0}")]
- Logging(#[from] logging::Error),
- #[error("Error building filetree: {0}")]
- Filetree(#[from] filetree::Error),
- #[error("Borrow error: {0}")]
- Borrow(#[from] BorrowError),
- #[error("Borrow mut error: {0}")]
- BorrowMut(#[from] BorrowMutError),
- #[error("Error stripping base from path: {0}")]
- BasePrefix(StripPrefixError),
- #[error("No inventory check info on tree node")]
+ ReadConfigFile(std::io::Error),
+ ParseConfig(config::Error),
+ Filetree(filetree::Error),
+ BasePrefix(std::path::StripPrefixError),
NoInventoryCheckInfo,
- #[error("Error executing command: {0}")]
Command(std::io::Error),
- #[error("String returned by command is not utf8: {0}")]
- StringUtf8(#[from] FromUtf8Error),
- #[error("Error reading filesystem: {0}")]
- ReadFs(tokio::io::Error),
+ StringUtf8(std::string::FromUtf8Error),
+ ReadFs(std::io::Error),
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::ParseArgs(error) => Some(error),
+ Error::ProcessDirPath(error) => Some(error),
+ Error::ReadConfigFile(error) => Some(error),
+ Error::ParseConfig(error) => Some(error),
+ Error::Filetree(error) => Some(error),
+ Error::BasePrefix(error) => Some(error),
+ Error::NoInventoryCheckInfo => None,
+ Error::Command(error) => Some(error),
+ Error::StringUtf8(error) => Some(error),
+ Error::ReadFs(error) => Some(error),
+ }
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::ParseArgs(error) => write!(f, "Error parsing args: {error}"),
+ Error::ProcessDirPath(error) => write!(f, "Error processing directory path: {error}"),
+ Error::ReadConfigFile(error) => write!(f, "Error reading config file: {error}"),
+ Error::ParseConfig(error) => write!(f, "Error parsing config: {error}"),
+ Error::Filetree(error) => write!(f, "Filetree error: {error}"),
+ Error::BasePrefix(error) => write!(f, "Error stripping prefix: {error}"),
+ Error::NoInventoryCheckInfo => write!(f, "No inventory check info in filetree"),
+ Error::Command(error) => write!(f, "Error executing command: {error}"),
+ Error::StringUtf8(error) => write!(f, "Error building utf8 string: {error}"),
+ Error::ReadFs(error) => write!(f, "Error reading from filesystem: {error}"),
+ }
+ }
}
diff --git a/src/logging.rs b/src/logging.rs
@@ -1,86 +0,0 @@
-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
@@ -1,5 +1,5 @@
fn main() {
- if let Err(error) = jannie::jannie_new::oneshot() {
+ if let Err(error) = jannie::oneshot() {
eprintln!("{error}");
}
}
diff --git a/src/path_processing.rs b/src/path_processing.rs
@@ -1,45 +0,0 @@
-use std::{
- env::VarError,
- path::{Path, PathBuf},
-};
-
-pub fn expand_path(path: &Path) -> Result<PathBuf, Error> {
- let path = shellexpand::full(
- path.to_str()
- .ok_or_else(|| Error::Parse(path.to_path_buf()))?,
- )?;
- Ok(path.as_ref().into())
-}
-
-pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> {
- let path = path.canonicalize().map_err(Error::Canonicalize)?;
- Ok(path)
-}
-
-/// Expands given path and canonicalizes it
-pub fn process_path(path: &Path) -> Result<PathBuf, Error> {
- let path = expand_path(path)?;
- canonicalize_path(&path)
-}
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
- #[error("Error parsing path: {0:?}")]
- Parse(PathBuf),
- #[error("Error expanding path: {0}")]
- Expand(#[from] shellexpand::LookupError<VarError>),
- #[error("Error canonicalizing path: {0}")]
- Canonicalize(std::io::Error),
-}
-
-impl Error {
- pub fn is_file_not_found(&self) -> bool {
- match *self {
- Error::Canonicalize(ref error) => match error.kind() {
- std::io::ErrorKind::NotFound => true,
- _ => false,
- },
- _ => false,
- }
- }
-}
diff --git a/src/state.rs b/src/state.rs
@@ -1,194 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use tokio::runtime::Runtime;
-
-use crate::{
- config::{Config, Filter, InventoryFormat},
- filetree::{self, Filetree, node::Node},
- inventory::{self, Inventory},
- path_processing,
-};
-
-pub struct State {
- pub config_dir: PathBuf,
- pub config: Config,
- pub runtime: Runtime,
- pub inventory: Inventory,
- /// Paths are absolute and processed
- pub filters: Vec<Filter>,
- /// Absolute processed
- pub root: PathBuf,
- pub blacklist_tree: Filetree<()>,
- pub whitelist_tree: Filetree<()>,
- pub inventory_tree: Filetree<Option<inventory::Check>>,
-}
-
-impl State {
- pub fn new(config_dir: &Path, config: Config) -> Result<Self, Error> {
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()
- .map_err(Error::Runtime)?;
-
- let inventory_path = match config.inventory_format {
- InventoryFormat::Yaml => config_dir.join("inventory.yaml"),
- InventoryFormat::Json => config_dir.join("inventory.json"),
- };
-
- tracing::trace!("Reading inventory from {inventory_path:?}");
-
- let inventory = runtime
- .block_on(tokio::fs::read(inventory_path))
- .map_err(Error::ReadInventoryFile)?;
-
- tracing::trace!("Parsing inventory file contents");
-
- let inventory: Inventory =
- match config.inventory_format {
- InventoryFormat::Yaml => serde_yaml::from_slice(inventory.as_slice())
- .map_err(Error::ReadInventoryYaml)?,
- InventoryFormat::Json => serde_json::from_slice(inventory.as_slice())
- .map_err(Error::ReadInventoryJson)?,
- };
-
- tracing::trace!("Processing root path");
-
- let root = path_processing::process_path(&config.root).map_err(Error::ProcessRootPath)?;
-
- let mut filters = Vec::new();
-
- tracing::debug!("Preprocessing filters");
-
- for filter in &config.filters {
- tracing::trace!("Preprocessing filter {filter:?}");
- let mut path = config.root.clone();
- path.push(&filter.path);
- filters.push(Filter {
- path: match path_processing::process_path(&path) {
- Ok(path) => path,
- Err(error) if error.is_file_not_found() => {
- tracing::warn!("Filter path was not found: {filter:?}");
- continue;
- }
- Err(error) => Err(Error::ProcessPath(error))?,
- },
- filter_type: filter.filter_type.clone(),
- });
- }
-
- tracing::debug!("Building trees from filters");
-
- let mut blacklist_tree = Filetree::new(Node::new("/".into(), ()));
- let mut whitelist_tree = Filetree::new(Node::new("/".into(), ()));
-
- for filter in &filters {
- let path = &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 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)?
- }
- }
- }
- }
-
- tracing::debug!("Building inventory tree");
-
- let mut inventory_tree = Filetree::new(Node::new("/".into(), None));
- for item in &inventory.items {
- let path = match path_processing::process_path(&item.path) {
- Ok(path) => path,
- Err(error) if error.is_file_not_found() => {
- tracing::warn!("Inventory item was not found: {item:?}");
- continue;
- }
- Err(error) => Err(Error::ProcessPath(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 inventory_tree.node_by_path(&path_buf).is_none() {
- inventory_tree.insert(Node::new(path_buf.clone(), None))?;
- }
- }
- None => {
- inventory_tree
- .node_by_path(&path)
- .expect("Should have added inventory item")
- .borrow_mut()
- .meta_mut()
- .replace(item.check.clone());
- break;
- }
- }
- }
- }
-
- Ok(Self {
- config_dir: config_dir.into(),
- config,
- runtime,
- inventory,
- filters,
- root,
- 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 reading inventory file: {0}")]
- ReadInventoryFile(std::io::Error),
- #[error("Error processing path: {0}")]
- ProcessPath(path_processing::Error),
- #[error("Error processing root path: {0}")]
- ProcessRootPath(path_processing::Error),
- #[error("Error parsing yaml inventory: {0}")]
- ReadInventoryYaml(serde_yaml::Error),
- #[error("Error parsing json inventory: {0}")]
- ReadInventoryJson(serde_json::Error),
- #[error("Error building filetree: {0}")]
- Filetree(#[from] filetree::Error),
-}