This works so far

This commit is contained in:
Russell 2025-07-28 16:47:52 +00:00
commit e7dedf3e02
10 changed files with 1623 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

769
Cargo.lock generated Normal file
View File

@ -0,0 +1,769 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[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.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "inotify"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [
"bitflags 1.3.2",
"futures-core",
"inotify-sys",
"libc",
"tokio",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "io-uring"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys",
]
[[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.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
dependencies = [
"backtrace",
"io-uring",
"libc",
"mio",
"pin-project-lite",
"slab",
"socket2",
"windows-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "where-cmd"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"inotify",
"memmap2",
"regex",
"serde",
"walkdir",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "where-cmd"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.0", features = ["derive"] }
memmap2 = "0.9"
inotify = "0.10"
serde = { version = "1.0", features = ["derive"] }
walkdir = "2.0"
regex = "1.0"
chrono = "0.4"

16
justfile Normal file
View File

@ -0,0 +1,16 @@
default:
just -l
run mode="quiet":
#! /bin/bash
case "{{ mode }}" in
quiet)
RUSTFLAGS="-A warnings" cargo run --quiet
;;
*)
cargo run
;;
esac
build:
cargo build --release

59
src/cli/mod.rs Normal file
View File

@ -0,0 +1,59 @@
pub mod tree;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "where", about = "Universal system file and package mapper")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Scan system and build/update database
Scan {
#[arg(short, long, help = "Force full rescan")]
force: bool,
},
/// Show file information
File {
path: String,
},
/// List files in tree format for packages
Tree {
#[arg(long, help = "Show tree for specific package")]
package: Option<String>,
#[arg(short, long, help = "Maximum depth")]
depth: Option<usize>,
#[arg(short, long, help = "Filter by source type")]
source: Option<String>,
},
/// Search for files
Find {
#[arg(short, long, help = "File name pattern")]
name: Option<String>,
#[arg(short, long, help = "Package name")]
package: Option<String>,
#[arg(short, long, help = "Files larger than size")]
size: Option<String>,
},
/// Show package information
Package {
name: String,
},
/// List all packages
Packages {
#[arg(short, long, help = "Filter by source")]
source: Option<String>,
},
}

100
src/cli/tree.rs Normal file
View File

@ -0,0 +1,100 @@
use crate::{PackageManager, PackageSource, WhereError};
use std::collections::HashMap;
use std::path::Path;
pub fn print_package_tree(manager: &dyn PackageManager, package_name: &str) -> Result<(), WhereError> {
// Get package info first
let package_info = match manager.get_package_info(package_name)? {
Some(info) => info,
None => {
println!("Package '{}' not found", package_name);
return Ok(());
}
};
// Print package header
println!("{}{} {}\x1b[0m",
package_info.source.color_code(),
package_info.name,
package_info.version);
// Get all files for this package
let files = manager.get_package_files(package_name)?;
if files.is_empty() {
println!(" (no files found)");
return Ok(());
}
// Build directory tree structure
let tree = build_tree_structure(&files);
// Print the tree
print_tree_node(&tree, "", true);
Ok(())
}
#[derive(Debug)]
struct TreeNode {
name: String,
is_file: bool,
children: HashMap<String, TreeNode>,
}
impl TreeNode {
fn new(name: String, is_file: bool) -> Self {
TreeNode {
name,
is_file,
children: HashMap::new(),
}
}
}
fn build_tree_structure(files: &[std::path::PathBuf]) -> TreeNode {
let mut root = TreeNode::new("".to_string(), false);
for file_path in files {
let path_str = file_path.to_string_lossy();
let components: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect();
let mut current = &mut root;
for (i, component) in components.iter().enumerate() {
let is_last = i == components.len() - 1;
let is_file = is_last && file_path.is_file();
current.children
.entry(component.to_string())
.or_insert_with(|| TreeNode::new(component.to_string(), is_file));
current = current.children.get_mut(*component).unwrap();
}
}
root
}
fn print_tree_node(node: &TreeNode, prefix: &str, is_last: bool) {
if !node.name.is_empty() {
let connector = if is_last { "└── " } else { "├── " };
let file_indicator = if node.is_file { "" } else { "/" };
println!("{}{}{}{}", prefix, connector, node.name, file_indicator);
}
let mut children: Vec<_> = node.children.iter().collect();
children.sort_by_key(|(name, child)| (!child.is_file, name.to_lowercase()));
for (i, (_, child)) in children.iter().enumerate() {
let is_child_last = i == children.len() - 1;
let child_prefix = if node.name.is_empty() {
prefix.to_string()
} else {
format!("{}{} ", prefix, if is_last { " " } else { "" })
};
print_tree_node(child, &child_prefix, is_child_last);
}
}

85
src/database/format.rs Normal file
View File

@ -0,0 +1,85 @@
use memmap2::Mmap;
use std::fs::File;
#[repr(C, packed)]
pub struct DatabaseHeader {
magic: [u8; 8], // "WHEREDB\0"
version: u32,
created: u64, // Unix timestamp
file_count: u64,
package_count: u32,
files_offset: u64,
packages_offset: u64,
strings_offset: u64,
index_offset: u64,
}
#[repr(C, packed)]
pub struct FileRecord {
path_hash: u64, // FNV-1a hash for quick lookups
package_id: u32,
source: u8,
permissions: u16,
size: u64,
mtime: u64,
path_offset: u32, // offset into string pool
}
#[repr(C, packed)]
pub struct PackageRecord {
id: u32,
source: u8,
install_time: u64,
name_offset: u32,
version_offset: u32,
file_count: u32,
first_file_idx: u32,
}
pub struct WhereDatabase {
_file: File,
mmap: Mmap,
header: &'static DatabaseHeader,
}
impl WhereDatabase {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, WhereError> {
let file = File::open(path)?;
let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
let header = unsafe {
&*(mmap.as_ptr() as *const DatabaseHeader)
};
// Validate magic number
if &header.magic != b"WHEREDB\0" {
return Err(WhereError::InvalidDatabase);
}
Ok(WhereDatabase {
_file: file,
mmap,
header,
})
}
pub fn find_file(&self, path: &str) -> Option<FileInfo> {
let hash = fnv1a_hash(path.as_bytes());
// Binary search through sorted file records
let files = self.get_file_records();
match files.binary_search_by_key(&hash, |record| record.path_hash) {
Ok(idx) => {
let record = &files[idx];
Some(FileInfo {
path: self.get_string(record.path_offset),
package: self.get_package_name(record.package_id),
source: PackageSource::from_u8(record.source),
size: record.size,
modified: record.mtime,
})
}
Err(_) => None,
}
}
}

198
src/main.rs Normal file
View File

@ -0,0 +1,198 @@
#![allow(dead_code)]
#![allow(unused_imports)]
pub mod package_managers;
pub mod cli;
use std::path::PathBuf;
use package_managers::{detect_available_managers, PackageManager, PackageSource};
use cli::{Cli, Commands};
use clap::Parser;
// Define the types that your modules need
#[derive(Debug, Clone)]
pub struct PackageInfo {
pub name: String,
pub version: String,
pub source: PackageSource,
pub install_time: u64,
pub size: u64,
}
#[derive(Debug)]
pub enum WhereError {
Io(std::io::Error),
Utf8(std::string::FromUtf8Error),
CommandFailed(String),
PackageNotFound(String),
ManagerNotAvailable(String),
InvalidDatabase,
}
// Implement From traits for error conversion
impl From<std::io::Error> for WhereError {
fn from(err: std::io::Error) -> Self {
WhereError::Io(err)
}
}
impl From<std::string::FromUtf8Error> for WhereError {
fn from(err: std::string::FromUtf8Error) -> Self {
WhereError::Utf8(err)
}
}
impl std::fmt::Display for WhereError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WhereError::Io(e) => write!(f, "IO error: {}", e),
WhereError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
WhereError::CommandFailed(s) => write!(f, "Command failed: {}", s),
WhereError::PackageNotFound(s) => write!(f, "Package not found: {}", s),
WhereError::ManagerNotAvailable(s) => write!(f, "Manager not available: {}", s),
WhereError::InvalidDatabase => write!(f, "Invalid database format"),
}
}
}
impl std::error::Error for WhereError {}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let managers = detect_available_managers();
match cli.command {
Commands::Tree { depth, package, source } => {
handle_tree_command(&managers, depth, package, source)?;
}
Commands::Package { name } => {
handle_package_command(&managers, &name)?;
}
Commands::Packages { source } => {
handle_packages_command(&managers, source)?;
}
Commands::File { path } => {
handle_file_command(&managers, &path)?;
}
Commands::Find { name, package, size } => {
handle_find_command(&managers, name, package, size)?;
}
Commands::Scan { force } => {
handle_scan_command(&managers, force)?;
}
}
Ok(())
}
fn handle_tree_command(
managers: &[Box<dyn PackageManager>],
_depth: Option<usize>,
package_filter: Option<String>,
_source_filter: Option<String>
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
if let Some(package_name) = package_filter {
// Show tree for specific package
cli::tree::print_package_tree(dnf.as_ref(), &package_name)?;
} else {
// Show tree for first few packages as demo
println!("Package file trees (first 5 packages):");
let packages = dnf.get_installed_packages()?;
for package in packages.iter().take(5) {
cli::tree::print_package_tree(dnf.as_ref(), &package.name)?;
println!(); // Empty line between packages
}
}
} else {
println!("No DNF package manager found");
}
Ok(())
}
fn handle_package_command(
managers: &[Box<dyn PackageManager>],
name: &str
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
match dnf.get_package_info(name) {
Ok(Some(info)) => {
println!("Package: {}", info.name);
println!("Version: {}", info.version);
println!("Source: {}", info.source.name());
println!("Install time: {}", info.install_time);
println!("Size: {} bytes", info.size);
// Show file tree for this package
println!("\nFiles:");
cli::tree::print_package_tree(dnf.as_ref(), name)?;
}
Ok(None) => println!("Package '{}' not found", name),
Err(e) => println!("Error: {}", e),
}
}
Ok(())
}
fn handle_packages_command(
managers: &[Box<dyn PackageManager>],
_source_filter: Option<String>
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
let packages = dnf.get_installed_packages()?;
println!("Installed packages ({} total):", packages.len());
for package in packages.iter() {
println!(" {} {} ({})",
package.name,
package.version,
package.source.name());
}
}
Ok(())
}
fn handle_file_command(
managers: &[Box<dyn PackageManager>],
path: &str
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
match dnf.get_file_owner(std::path::Path::new(path)) {
Ok(Some(owner)) => println!("{} is owned by package: {}", path, owner),
Ok(None) => println!("{} is not owned by any package", path),
Err(e) => println!("Error checking {}: {}", path, e),
}
}
Ok(())
}
fn handle_find_command(
managers: &[Box<dyn PackageManager>],
_name: Option<String>,
package: Option<String>,
_size: Option<String>
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(package_name) = package {
if let Some(dnf) = managers.iter().find(|m| m.name() == "dnf") {
match dnf.get_package_files(&package_name) {
Ok(files) => {
println!("Files in package '{}':", package_name);
for file in files {
println!(" {}", file.display());
}
}
Err(e) => println!("Error: {}", e),
}
}
} else {
println!("Find command needs more specific criteria");
}
Ok(())
}
fn handle_scan_command(
_managers: &[Box<dyn PackageManager>],
_force: bool
) -> Result<(), Box<dyn std::error::Error>> {
println!("Scan functionality not yet implemented");
Ok(())
}

159
src/package_managers/dnf.rs Normal file
View File

@ -0,0 +1,159 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::{PackageInfo, WhereError};
use super::{PackageManager, PackageSource};
pub struct DnfManager;
impl PackageManager for DnfManager {
fn name(&self) -> &'static str { "dnf" }
fn source_type(&self) -> PackageSource {
PackageSource::Dnf
}
fn is_available(&self) -> bool {
Path::new("/usr/bin/dnf").exists() || Path::new("/usr/bin/rpm").exists()
}
fn get_installed_packages(&self) -> Result<Vec<PackageInfo>, WhereError> {
// Use rpm directly for better performance and parsing
let output = Command::new("rpm")
.args(["-qa", "--queryformat", "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{SIZE}\n"])
.output()?;
if !output.status.success() {
return Err(WhereError::CommandFailed("rpm query failed".to_string()));
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() >= 4 {
Some(PackageInfo {
name: parts[0].to_string(),
version: parts[1].to_string(),
source: PackageSource::Dnf,
install_time: parts[2].parse().unwrap_or(0),
size: parts[3].parse().unwrap_or(0),
})
} else {
None
}
})
.collect())
}
fn get_package_files(&self, package: &str) -> Result<Vec<PathBuf>, WhereError> {
let output = Command::new("rpm")
.args(["-ql", package])
.output()?;
if !output.status.success() {
return Err(WhereError::PackageNotFound(package.to_string()));
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout.lines()
.filter(|line| !line.is_empty())
.map(|line| PathBuf::from(line.trim()))
.filter(|path| path.exists()) // Only include files that actually exist
.collect())
}
fn get_file_owner(&self, path: &Path) -> Result<Option<String>, WhereError> {
let output = Command::new("rpm")
.args(["-qf", &path.to_string_lossy()])
.output()?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
let package_name = stdout.trim();
// RPM returns full package name with version, extract just the name
if let Some(base_name) = package_name.split('-').next() {
return Ok(Some(base_name.to_string()));
}
Ok(Some(package_name.to_string()))
} else {
// Check if it's a "file not owned by any package" error vs other errors
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("is not owned by any package") {
Ok(None)
} else {
Err(WhereError::CommandFailed(format!("rpm query failed: {}", stderr)))
}
}
}
fn get_package_info(&self, package: &str) -> Result<Option<PackageInfo>, WhereError> {
let output = Command::new("rpm")
.args(["-qi", package])
.output()?;
if !output.status.success() {
return Ok(None);
}
let stdout = String::from_utf8(output.stdout)?;
let mut name = String::new();
let mut version = String::new();
let mut install_time = 0u64;
let mut size = 0u64;
for line in stdout.lines() {
if line.starts_with("Name : ") {
name = line.strip_prefix("Name : ").unwrap_or("").to_string();
} else if line.starts_with("Version : ") {
let version_part = line.strip_prefix("Version : ").unwrap_or("");
// Get release info too
if let Some(release_line) = stdout.lines()
.find(|l| l.starts_with("Release : ")) {
let release = release_line.strip_prefix("Release : ").unwrap_or("");
version = format!("{}-{}", version_part, release);
} else {
version = version_part.to_string();
}
} else if line.starts_with("Install Date: ") {
let date_str = line.strip_prefix("Install Date: ").unwrap_or("");
install_time = self.parse_rpm_date(date_str).unwrap_or(0);
} else if line.starts_with("Size : ") {
let size_str = line.strip_prefix("Size : ").unwrap_or("");
size = size_str.split_whitespace().next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
}
if !name.is_empty() {
Ok(Some(PackageInfo {
name,
version,
source: PackageSource::Dnf,
install_time,
size,
}))
} else {
Ok(None)
}
}
}
impl DnfManager {
fn parse_rpm_date(&self, date_str: &str) -> Option<u64> {
// RPM date format: "Wed 28 Jul 2025 10:30:00 AM UTC"
// Convert to Unix timestamp
use chrono::{DateTime, Utc};
// Try parsing the RPM date format
if let Ok(dt) = DateTime::parse_from_str(date_str, "%a %d %b %Y %I:%M:%S %p %Z") {
Some(dt.timestamp() as u64)
} else if let Ok(dt) = DateTime::parse_from_str(date_str, "%a %b %d %H:%M:%S %Y") {
Some(dt.timestamp() as u64)
} else {
None
}
}
}

223
src/package_managers/mod.rs Normal file
View File

@ -0,0 +1,223 @@
use std::path::{Path, PathBuf};
pub mod dnf;
// pub mod flatpak;
// pub mod snap;
use self::dnf::DnfManager;
// Uncomment these as we implement the managers
// use self::flatpak::FlatpakManager;
// use self::snap::SnapManager;
use crate::{PackageInfo, WhereError};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PackageSource {
Apt = 1,
Dnf = 2,
Pacman = 3,
Flatpak = 4,
Snap = 5,
Homebrew = 6,
Manual = 7,
}
impl PackageSource {
pub fn from_u8(value: u8) -> Self {
match value {
1 => PackageSource::Apt,
2 => PackageSource::Dnf,
3 => PackageSource::Pacman,
4 => PackageSource::Flatpak,
5 => PackageSource::Snap,
6 => PackageSource::Homebrew,
_ => PackageSource::Manual,
}
}
pub fn name(&self) -> &'static str {
match self {
PackageSource::Apt => "apt",
PackageSource::Dnf => "dnf",
PackageSource::Pacman => "pacman",
PackageSource::Flatpak => "flatpak",
PackageSource::Snap => "snap",
PackageSource::Homebrew => "homebrew",
PackageSource::Manual => "manual",
}
}
pub fn color_code(&self) -> &'static str {
match self {
PackageSource::Apt => "\x1b[32m", // Green
PackageSource::Dnf => "\x1b[31m", // Red
PackageSource::Pacman => "\x1b[36m", // Cyan
PackageSource::Flatpak => "\x1b[34m", // Blue
PackageSource::Snap => "\x1b[35m", // Magenta
PackageSource::Homebrew => "\x1b[33m", // Yellow
PackageSource::Manual => "\x1b[37m", // White
}
}
}
pub trait PackageManager {
fn name(&self) -> &'static str;
fn source_type(&self) -> PackageSource;
fn is_available(&self) -> bool;
fn get_installed_packages(&self) -> Result<Vec<PackageInfo>, WhereError>;
fn get_package_files(&self, package: &str) -> Result<Vec<PathBuf>, WhereError>;
fn get_file_owner(&self, path: &Path) -> Result<Option<String>, WhereError>;
fn get_package_info(&self, package: &str) -> Result<Option<PackageInfo>, WhereError>;
/// Priority for file ownership resolution (higher = preferred)
fn priority(&self) -> u8 {
match self.source_type() {
PackageSource::Dnf => 10, // System package manager gets highest priority
PackageSource::Apt => 10,
PackageSource::Pacman => 10,
PackageSource::Flatpak => 5, // User applications
PackageSource::Snap => 5,
PackageSource::Homebrew => 3, // Usually supplementary
PackageSource::Manual => 1, // Lowest priority
}
}
}
/// Detect all available package managers on the system
pub fn detect_available_managers() -> Vec<Box<dyn PackageManager>> {
let mut managers: Vec<Box<dyn PackageManager>> = Vec::new();
// Check system package managers first
let dnf = DnfManager;
if dnf.is_available() {
managers.push(Box::new(dnf));
}
// Add other managers as we implement them
// let flatpak = FlatpakManager;
// if flatpak.is_available() {
// managers.push(Box::new(flatpak));
// }
// let snap = SnapManager;
// if snap.is_available() {
// managers.push(Box::new(snap));
// }
// Sort by priority (highest first) for consistent ordering
managers.sort_by(|a, b| b.priority().cmp(&a.priority()));
managers
}
/// Find which package owns a file, checking all available managers
pub fn find_file_owner(path: &Path) -> Result<Option<(String, PackageSource)>, WhereError> {
let managers = detect_available_managers();
for manager in managers {
if let Ok(Some(package)) = manager.get_file_owner(path) {
return Ok(Some((package, manager.source_type())));
}
}
Ok(None)
}
/// Get all installed packages from all available managers
pub fn get_all_packages() -> Result<Vec<PackageInfo>, WhereError> {
let managers = detect_available_managers();
let mut all_packages = Vec::new();
for manager in managers {
match manager.get_installed_packages() {
Ok(mut packages) => all_packages.append(&mut packages),
Err(e) => {
eprintln!("Warning: Failed to get packages from {}: {}", manager.name(), e);
// Continue with other managers
}
}
}
// Sort by name for consistent output
all_packages.sort_by(|a, b| a.name.cmp(&b.name));
Ok(all_packages)
}
/// Get package information by name, searching all managers
pub fn find_package(name: &str) -> Result<Option<PackageInfo>, WhereError> {
let managers = detect_available_managers();
for manager in managers {
if let Ok(Some(package)) = manager.get_package_info(name) {
return Ok(Some(package));
}
}
Ok(None)
}
/// Get packages from a specific source type
pub fn get_packages_by_source(source: PackageSource) -> Result<Vec<PackageInfo>, WhereError> {
let managers = detect_available_managers();
for manager in managers {
if manager.source_type() == source {
return manager.get_installed_packages();
}
}
Err(WhereError::ManagerNotAvailable(source.name().to_string()))
}
/// Summary of available package managers
#[derive(Debug)]
pub struct ManagerSummary {
pub name: String,
pub source_type: PackageSource,
pub available: bool,
pub package_count: Option<usize>,
}
pub fn get_manager_summary() -> Vec<ManagerSummary> {
let available_managers = detect_available_managers();
let mut summaries = Vec::new();
for manager in available_managers {
let package_count = manager.get_installed_packages()
.map(|packages| packages.len())
.ok();
summaries.push(ManagerSummary {
name: manager.name().to_string(),
source_type: manager.source_type(),
available: true,
package_count,
});
}
summaries
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_source_conversion() {
assert_eq!(PackageSource::from_u8(2), PackageSource::Dnf);
assert_eq!(PackageSource::Dnf.name(), "dnf");
}
#[test]
fn test_manager_detection() {
let managers = detect_available_managers();
assert!(!managers.is_empty(), "Should detect at least one package manager");
// On Fedora, should detect DNF
let has_dnf = managers.iter().any(|m| m.name() == "dnf");
if Path::new("/usr/bin/dnf").exists() {
assert!(has_dnf, "Should detect DNF on systems where it's installed");
}
}
}