Added foundational modules for core functionalities:

- Introduced `walk.rs` as a parallel directory walker for search operations.
- Implemented basic index handling in `commands/index.rs`.
- Created `utils/config.rs` for configuration management with placeholders for future enhancements.
This commit is contained in:
elipeter 2025-06-16 16:46:22 +02:00
commit ab5558f537
16 changed files with 2187 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

813
Cargo.lock generated Normal file
View file

@ -0,0 +1,813 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "Nano"
version = "0.1.0"
dependencies = [
"clap",
"directories",
"serde",
"time",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[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 = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clap"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
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 = "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 = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "directories"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
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 = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[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 = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[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.173"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[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.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 = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom",
"libredox",
"thiserror 2.0.12",
]
[[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 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[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 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[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 = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "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.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror 1.0.69",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
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.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"time",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[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 = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[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"
[[package]]
name = "winnow"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
dependencies = [
"memchr",
]

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "Nano"
version = "0.1.0"
edition = "2024"
[dependencies]
directories = "6.0.0"
clap = { version = "4.5.40", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
toml = "0.8.23"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json", "ansi","time"] }
tracing-appender = "0.2.3"
tracing = "0.1.41"
time = "0.3.41"

88
src/cli.rs Normal file
View file

@ -0,0 +1,88 @@
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "nano")]
#[command(about = "A fast vulnerability scanner with project indexing")]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub(crate) command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Scan project for vulnerabilities
Scan {
/// Path to scan (defaults to current directory)
#[arg(default_value = ".")]
path: String,
/// Skip using/building index, scan directly
#[arg(long)]
no_index: bool,
/// Force rebuild index before scanning
#[arg(long)]
rebuild_index: bool,
/// Output format
#[arg(short, long, value_enum, default_value = "table")]
format: OutputFormat,
/// Show only high severity issues
#[arg(long)]
high_only: bool,
},
/// Manage project indexes
Index {
#[command(subcommand)]
action: IndexAction,
},
/// List all indexed projects
List {
/// Show detailed information
#[arg(short, long)]
verbose: bool,
},
/// Remove project from index
Clean {
/// Project name or path to clean
project: Option<String>,
/// Clean all projects
#[arg(long)]
all: bool,
},
}
#[derive(Subcommand)]
pub enum IndexAction {
/// Build or update index for current project
Build {
/// Path to index (defaults to current directory)
#[arg(default_value = ".")]
path: String,
/// Force full rebuild
#[arg(short, long)]
force: bool,
},
/// Show index status and statistics
Status {
/// Project path to check
#[arg(default_value = ".")]
path: String,
},
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum OutputFormat {
Table,
Json,
Csv,
Sarif,
}

37
src/commands/clean.rs Normal file
View file

@ -0,0 +1,37 @@
use std::{env, fs};
use crate::utils::get_project_info;
pub fn handle(
project: Option<String>,
all: bool,
config_dir: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
if all {
println!("Cleaning all indexes...");
if config_dir.exists() {
fs::remove_dir_all(config_dir)?;
fs::create_dir_all(config_dir)?;
}
println!("All indexes cleaned.");
} else if let Some(proj_name) = project {
let db_path = config_dir.join(format!("{}.sqlite", proj_name));
if db_path.exists() {
fs::remove_file(&db_path)?;
println!("Cleaned index for: {}", proj_name);
} else {
println!("No index found for: {}", proj_name);
}
} else {
let current_dir = env::current_dir()?;
let (project_name, db_path) = get_project_info(&current_dir, config_dir)?;
if db_path.exists() {
fs::remove_file(&db_path)?;
println!("Cleaned index for: {}", project_name);
} else {
println!("No index found for current project: {}", project_name);
}
}
Ok(())
}

48
src/commands/index.rs Normal file
View file

@ -0,0 +1,48 @@
use std::fs;
use crate::cli::IndexAction;
use crate::utils::project::get_project_info;
pub fn handle(
action: IndexAction,
database_dir: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
match action {
IndexAction::Build { path, force } => {
let build_path = std::path::Path::new(&path).canonicalize()?;
let (project_name, db_path) = get_project_info(&build_path, database_dir)?;
if force || !db_path.exists() {
println!("Building index for: {}", project_name);
build_index(&build_path, &db_path)?;
println!("Index built: {}", db_path.display());
} else {
println!("Index already exists. Use --force to rebuild.");
}
}
IndexAction::Status { path } => {
let status_path = std::path::Path::new(&path).canonicalize()?;
let (project_name, db_path) = get_project_info(&status_path, database_dir)?;
println!("Project: {}", project_name);
println!("Index path: {}", db_path.display());
println!("Index exists: {}", db_path.exists());
if db_path.exists() {
let metadata = fs::metadata(&db_path)?;
println!("Index size: {} bytes", metadata.len());
println!("Last modified: {:?}", metadata.modified()?);
}
}
}
Ok(())
}
pub fn build_index(
_project_path: &std::path::Path,
db_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
// TODO: Implement actual index building
fs::File::create(db_path)?;
println!("Index building logic goes here...");
Ok(())
}

35
src/commands/list.rs Normal file
View file

@ -0,0 +1,35 @@
use std::fs;
pub fn handle(
verbose: bool,
database_dir: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Indexed projects:");
if database_dir.exists() {
for entry in fs::read_dir(database_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("sqlite") {
let project_name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
println!(" {}", project_name);
if verbose {
let metadata = fs::metadata(&path)?;
println!(" Path: {}", path.display());
println!(" Size: {} bytes", metadata.len());
println!(" Modified: {:?}", metadata.modified()?);
}
}
}
} else {
println!(" No indexed projects found.");
}
Ok(())
}

29
src/commands/mod.rs Normal file
View file

@ -0,0 +1,29 @@
pub mod scan;
pub mod index;
pub mod list;
pub mod clean;
use crate::cli::Commands;
use std::path::Path;
use crate::utils::config::Config;
pub fn handle_command(
command: Commands,
database_dir: &Path,
config: &Config
) -> Result<(), Box<dyn std::error::Error>> {
match command {
Commands::Scan { path, no_index, rebuild_index, format, high_only } => {
scan::handle(&path, no_index, rebuild_index, format, high_only, database_dir, config)
}
Commands::Index { action } => {
index::handle(action, database_dir)
}
Commands::List { verbose } => {
list::handle(verbose, database_dir)
}
Commands::Clean { project, all } => {
clean::handle(project, all, database_dir)
}
}
}

53
src/commands/scan.rs Normal file
View file

@ -0,0 +1,53 @@
use crate::cli::OutputFormat;
use crate::utils::project::get_project_info;
use std::path::Path;
use crate::utils::config::Config;
pub fn handle(
path: &str,
no_index: bool,
rebuild_index: bool,
format: OutputFormat,
high_only: bool,
database_dir: &Path,
config: &Config,
) -> Result<(), Box<dyn std::error::Error>> {
let scan_path = Path::new(path).canonicalize()?;
let (project_name, db_path) = get_project_info(&scan_path, database_dir)?;
tracing::info!("Config: {:?}", config);
tracing::info!("Scanning project: {}", project_name);
tracing::info!("Scan path: {}", scan_path.display());
if no_index {
tracing::info!("Scanning without index...");
scan_filesystem(&scan_path)?;
} else {
if rebuild_index || !db_path.exists() {
tracing::info!("Building/updating index...");
crate::commands::index::build_index(&scan_path, &db_path)?;
}
tracing::info!("Using index: {}", db_path.display());
scan_with_index(&db_path)?;
}
tracing::info!("Output format: {:?}", format);
if high_only {
tracing::info!("Filtering: High severity only");
}
Ok(())
}
fn scan_filesystem(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// TODO: Implement direct filesystem scanning
tracing::info!("Direct filesystem scan of: {}", path.display());
Ok(())
}
fn scan_with_index(db_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
// TODO: Implement index-based scanning
tracing::info!("Index-based scan using: {}", db_path.display());
Ok(())
}

0
src/exit_codes.rs Normal file
View file

43
src/filetypes.rs Normal file
View file

@ -0,0 +1,43 @@
// use crate::dir_entry;
// use crate::filesystem;
//
// use faccess::PathExt;
//
// /// Whether or not to show
// #[derive(Default)]
// pub struct FileTypes {
// pub files: bool,
// pub directories: bool,
// pub symlinks: bool,
// pub block_devices: bool,
// pub char_devices: bool,
// pub sockets: bool,
// pub pipes: bool,
// pub executables_only: bool,
// pub empty_only: bool,
// }
//
// impl FileTypes {
// pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {
// if let Some(ref entry_type) = entry.file_type() {
// (!self.files && entry_type.is_file())
// || (!self.directories && entry_type.is_dir())
// || (!self.symlinks && entry_type.is_symlink())
// || (!self.block_devices && filesystem::is_block_device(*entry_type))
// || (!self.char_devices && filesystem::is_char_device(*entry_type))
// || (!self.sockets && filesystem::is_socket(*entry_type))
// || (!self.pipes && filesystem::is_pipe(*entry_type))
// || (self.executables_only && !entry.path().executable())
// || (self.empty_only && !filesystem::is_empty(entry))
// || !(entry_type.is_file()
// || entry_type.is_dir()
// || entry_type.is_symlink()
// || filesystem::is_block_device(*entry_type)
// || filesystem::is_char_device(*entry_type)
// || filesystem::is_socket(*entry_type)
// || filesystem::is_pipe(*entry_type))
// } else {
// true
// }
// }
// }

59
src/main.rs Normal file
View file

@ -0,0 +1,59 @@
mod cli;
mod commands;
mod utils;
use crate::utils::Config;
use cli::Cli;
use clap::Parser;
use directories::ProjectDirs;
use std::fs;
use tracing_subscriber::{fmt, EnvFilter, Registry};
use tracing_subscriber::prelude::*;
use tracing_subscriber::fmt::time;
// use tracing_appender::rolling::{RollingFileAppender, Rotation};
// use tracing_appender::non_blocking;
fn init_tracing() {
// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "logs", "nano-scanner.log");
// let (file_writer, guard) = non_blocking(file_appender);
let fmt_layer = fmt::layer()
.pretty()
.with_thread_ids(true)
.with_timer(time::UtcTime::rfc_3339());
// let file_layer = fmt::layer()
// .with_writer(file_writer)
// .without_time()
// .json();
Registry::default()
.with(EnvFilter::from_default_env()) // obey RUST_LOG
.with(fmt_layer)
.init(); // install as the global subscriber
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
init_tracing();
tracing::debug!("CLI starting up");
let cli = Cli::parse();
let proj_dirs = ProjectDirs::from("dev", "ecpeter23", "nano")
.ok_or("Unable to determine project directories")?;
let config_dir = proj_dirs.config_dir();
fs::create_dir_all(config_dir)?;
let database_dir = proj_dirs.data_local_dir();
fs::create_dir_all(database_dir)?;
let config = Config::load(config_dir)?;
commands::handle_command(cli.command, database_dir, &config)?;
Ok(())
}

260
src/utils/config.rs Normal file
View file

@ -0,0 +1,260 @@
use serde::{Deserialize, Serialize};
use std::path::{Path};
use std::fs;
use toml;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct ScannerConfig {
/// The maximum file size to scan, in megabytes. TODO: IMPLEMENT
pub max_file_size_mb: u64,
/// File extensions to exclude from scanning. TODO: IMPLEMENT
pub excluded_extensions: Vec<String>,
/// Directories to exclude from scanning. TODO: IMPLEMENT
pub excluded_directories: Vec<String>,
/// Whether to respect the global ignore file or not. TODO: IMPLEMENT
pub read_global_ignore: bool,
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not. TODO: IMPLEMENT
pub read_vcsignore: bool,
/// Whether to require a `.git` directory to respect gitignore files. TODO: IMPLEMENT
pub require_git_to_read_vcsignore: bool,
/// Whether to limit the search to starting file system or not. TODO: IMPLEMENT
pub one_file_system: bool,
/// Whether to follow symlinks or not. TODO: IMPLEMENT
pub follow_symlinks: bool,
/// Whether to scan hidden files or not. TODO: IMPLEMENT
pub scan_hidden_files: bool,
}
impl Default for ScannerConfig {
fn default() -> Self {
Self {
max_file_size_mb: 100,
excluded_extensions: vec![
"jpg", "png", "gif", "mp4", "avi", "mkv",
"zip", "tar", "gz", "exe", "dll", "so",
]
.into_iter()
.map(str::to_owned)
.collect(),
excluded_directories: vec![
"node_modules", ".git", "target", ".vscode", ".idea", "build", "dist",
]
.into_iter()
.map(str::to_owned)
.collect(),
read_global_ignore: false,
read_vcsignore: true,
require_git_to_read_vcsignore: true,
one_file_system: false,
follow_symlinks: false,
scan_hidden_files: false,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct DatabaseConfig {
/// The number of days to keep database files for. TODO: IMPLEMENT
pub auto_cleanup_days: u32,
/// The maximum size of the database, in megabytes. TODO: IMPLEMENT
pub max_db_size_mb: u64,
/// Whether to run a VACUUM on startup or not. TODO: IMPLEMENT
pub vacuum_on_startup: bool,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
auto_cleanup_days: 30,
max_db_size_mb: 1024,
vacuum_on_startup: false,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct OutputConfig {
/// The default output format. TODO: IMPLEMENT
pub default_format: String,
/// Whether to show progress or not. TODO: IMPLEMENT
pub show_progress: bool,
/// Whether to colorize output or not. TODO: IMPLEMENT
pub color_output: bool,
/// The maximum number of results to show. TODO: IMPLEMENT
pub max_results: Option<u32>,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
default_format: "table".into(),
show_progress: true,
color_output: true,
max_results: None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct PerformanceConfig {
/// The maximum search depth, or `None` if no maximum search depth should be set.
///
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
/// all files under subdirectories of the current directory, etc.
pub max_depth: Option<usize>, // TODO: IMPLEMENT
/// The minimum depth for reported entries, or `None`.
pub min_depth: Option<usize>, // TODO: IMPLEMENT
/// Whether to stop traversing into matching directories.
pub prune: bool, // TODO: IMPLEMENT
/// The maximum number of worker threads to use., or `None` to auto-detect.
pub worker_threads: Option<u32>, // TODO: IMPLEMENT
/// The maximum number of entries to index in a single chunk.
pub index_chunk_size: u32, // TODO: IMPLEMENT
/// The maximum amount of memory to use, in megabytes.
pub memory_limit_mb: u64, // TODO: IMPLEMENT
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
max_depth: None,
min_depth: None,
prune: false,
worker_threads: None,
index_chunk_size: 1_000,
memory_limit_mb: 512,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Config {
pub scanner: ScannerConfig,
pub database: DatabaseConfig,
pub output: OutputConfig,
pub performance: PerformanceConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
scanner: ScannerConfig::default(),
database: DatabaseConfig::default(),
output: OutputConfig::default(),
performance: PerformanceConfig::default(),
}
}
}
impl Config {
pub fn load(
config_dir: &Path,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut config = Config::default();
let default_config_path = config_dir.join("nano.conf");
if !default_config_path.exists() {
create_example_config(config_dir)?;
}
let user_config_path = config_dir.join("nano.local");
if user_config_path.exists() {
let user_config_content = fs::read_to_string(&user_config_path)?;
let user_config: Config = toml::from_str(&user_config_content)?;
config = merge_configs(config, user_config);
println!("Loaded user config from: {}", user_config_path.display());
} else {
println!("Using default configuration. Create {} to customize.", user_config_path.display());
}
Ok(config)
}
}
fn create_example_config(
config_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let example_path = config_dir.join("nano.conf");
let default_config = Config::default();
let toml_content = toml::to_string_pretty(&default_config)?;
// Add comments to make it user-friendly
let commented_content = format!(
"# Nano Vulnerability Scanner Configuration\n\
# YOU SHOULD NOT MODIFY THIS FILE.\n\
# Create/modify 'nano.local' to set configs\n\
# Only include the sections you want to override\n\n{}",
toml_content
);
fs::write(&example_path, commented_content)?;
println!("Example config created at: {}", example_path.display());
Ok(())
}
/// Merge user config into default config, preserving defaults where the user didn't
/// supply new exclusions and overriding everything else.
fn merge_configs(mut default: Config, user: Config) -> Config {
// --- ScannerConfig ---
default.scanner.max_file_size_mb = user.scanner.max_file_size_mb;
default.scanner.read_global_ignore = user.scanner.read_global_ignore;
default.scanner.read_vcsignore = user.scanner.read_vcsignore;
default.scanner.require_git_to_read_vcsignore = user.scanner.require_git_to_read_vcsignore;
default.scanner.one_file_system = user.scanner.one_file_system;
default.scanner.follow_symlinks = user.scanner.follow_symlinks;
default.scanner.scan_hidden_files = user.scanner.scan_hidden_files;
// Merge exclusion lists (default ⊔ user), then sort & dedupe
default.scanner.excluded_extensions.extend(user.scanner.excluded_extensions);
default.scanner.excluded_directories.extend(user.scanner.excluded_directories);
default.scanner.excluded_extensions.sort_unstable();
default.scanner.excluded_extensions.dedup();
default.scanner.excluded_directories.sort_unstable();
default.scanner.excluded_directories.dedup();
// --- DatabaseConfig ---
default.database.auto_cleanup_days = user.database.auto_cleanup_days;
default.database.max_db_size_mb = user.database.max_db_size_mb;
default.database.vacuum_on_startup = user.database.vacuum_on_startup;
// --- OutputConfig ---
default.output.default_format = user.output.default_format;
default.output.show_progress = user.output.show_progress;
default.output.color_output = user.output.color_output;
default.output.max_results = user.output.max_results;
// --- PerformanceConfig ---
default.performance.max_depth = user.performance.max_depth;
default.performance.min_depth = user.performance.min_depth;
default.performance.prune = user.performance.prune;
default.performance.worker_threads = user.performance.worker_threads;
default.performance.index_chunk_size = user.performance.index_chunk_size;
default.performance.memory_limit_mb = user.performance.memory_limit_mb;
default
}

6
src/utils/mod.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod project;
pub mod config;
// Re-export commonly used functions for convenience
pub use project::{get_project_info, sanitize_project_name};
pub use config::Config;

31
src/utils/project.rs Normal file
View file

@ -0,0 +1,31 @@
use std::path::{Path, PathBuf};
pub fn get_project_info(
project_path: &Path,
config_dir: &Path,
) -> Result<(String, PathBuf), Box<dyn std::error::Error>> {
let project_name = project_path
.file_name()
.and_then(|name| name.to_str())
.ok_or("Unable to determine project name")?;
let db_name = sanitize_project_name(project_name);
let db_path = config_dir.join(format!("{}.sqlite", db_name));
Ok((project_name.to_string(), db_path))
}
pub fn sanitize_project_name(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| match c {
' ' | '\t' | '\n' | '\r' => '_',
c if c.is_alphanumeric() || c == '_' || c == '-' => c,
_ => '_'
})
.collect::<String>()
.split('_')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("_")
}

670
src/walk.rs Normal file
View file

@ -0,0 +1,670 @@
// use std::borrow::Cow;
// use std::ffi::OsStr;
// use std::io::{self, Write};
// use std::mem;
// use std::path::PathBuf;
// use std::sync::atomic::{AtomicBool, Ordering};
// use std::sync::{Arc, Mutex, MutexGuard};
// use std::thread;
// use std::time::{Duration, Instant};
//
// use anyhow::{anyhow, Result};
// use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, SendError, Sender};
// use etcetera::BaseStrategy;
// use ignore::overrides::{Override, OverrideBuilder};
// use ignore::{WalkBuilder, WalkParallel, WalkState};
// use regex::bytes::Regex;
//
// use crate::config::Config;
// use crate::dir_entry::DirEntry;
// use crate::error::print_error;
// use crate::exec;
// use crate::exit_codes::{merge_exitcodes, ExitCode};
// use crate::filesystem;
// use crate::output;
//
// /// The receiver thread can either be buffering results or directly streaming to the console.
// #[derive(PartialEq)]
// enum ReceiverMode {
// /// Receiver is still buffering in order to sort the results, if the search finishes fast
// /// enough.
// Buffering,
//
// /// Receiver is directly printing results to the output.
// Streaming,
// }
//
// /// The Worker threads can result in a valid entry having PathBuf or an error.
// #[allow(clippy::large_enum_variant)]
// #[derive(Debug)]
// pub enum WorkerResult {
// // Errors should be rare, so it's probably better to allow large_enum_variant than
// // to box the Entry variant
// Entry(ignore::DirEntry), // TODO: CHECK IF ERRORS
// Error(ignore::Error),
// }
//
// /// A batch of WorkerResults to send over a channel.
// #[derive(Clone)]
// struct Batch {
// items: Arc<Mutex<Option<Vec<WorkerResult>>>>,
// }
//
// impl Batch {
// fn new() -> Self {
// Self {
// items: Arc::new(Mutex::new(Some(vec![]))),
// }
// }
//
// fn lock(&self) -> MutexGuard<'_, Option<Vec<WorkerResult>>> {
// self.items.lock().unwrap()
// }
// }
//
// impl IntoIterator for Batch {
// type Item = WorkerResult;
// type IntoIter = std::vec::IntoIter<WorkerResult>;
//
// fn into_iter(self) -> Self::IntoIter {
// self.lock().take().unwrap().into_iter()
// }
// }
//
// /// Wrapper that sends batches of items at once over a channel.
// struct BatchSender {
// batch: Batch,
// tx: Sender<Batch>,
// limit: usize,
// }
//
// impl BatchSender {
// fn new(tx: Sender<Batch>, limit: usize) -> Self {
// Self {
// batch: Batch::new(),
// tx,
// limit,
// }
// }
//
// /// Check if we need to flush a batch.
// fn needs_flush(&self, batch: Option<&Vec<WorkerResult>>) -> bool {
// match batch {
// // Limit the batch size to provide some backpressure
// Some(vec) => vec.len() >= self.limit,
// // Batch was already taken by the receiver, so make a new one
// None => true,
// }
// }
//
// /// Add an item to a batch.
// fn send(&mut self, item: WorkerResult) -> Result<(), SendError<()>> {
// let mut batch = self.batch.lock();
//
// if self.needs_flush(batch.as_ref()) {
// drop(batch);
// self.batch = Batch::new();
// batch = self.batch.lock();
// }
//
// let items = batch.as_mut().unwrap();
// items.push(item);
//
// if items.len() == 1 {
// // New batch, send it over the channel
// self.tx
// .send(self.batch.clone())
// .map_err(|_| SendError(()))?;
// }
//
// Ok(())
// }
// }
//
// /// Maximum size of the output buffer before flushing results to the console
// const MAX_BUFFER_LENGTH: usize = 1000;
// /// Default duration until output buffering switches to streaming.
// const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
//
// /// Wrapper for the receiver thread's buffering behavior.
// struct ReceiverBuffer<'a, W> {
// /// The configuration.
// config: &'a Config,
// /// For shutting down the senders.
// quit_flag: &'a AtomicBool,
// /// The ^C notifier.
// interrupt_flag: &'a AtomicBool,
// /// Receiver for worker results.
// rx: Receiver<Batch>,
// /// Standard output.
// stdout: W,
// /// The current buffer mode.
// mode: ReceiverMode,
// /// The deadline to switch to streaming mode.
// deadline: Instant,
// /// The buffer of quickly received paths.
// buffer: Vec<ignore::DirEntry>,
// /// Result count.
// num_results: usize,
// }
//
// impl<'a, W: Write> ReceiverBuffer<'a, W> {
// /// Create a new receiver buffer.
// fn new(state: &'a WorkerState, rx: Receiver<Batch>, stdout: W) -> Self {
// let config = &state.config;
// let quit_flag = state.quit_flag.as_ref();
// let interrupt_flag = state.interrupt_flag.as_ref();
// let max_buffer_time = config.max_buffer_time.unwrap_or(DEFAULT_MAX_BUFFER_TIME);
// let deadline = Instant::now() + max_buffer_time;
//
// Self {
// config,
// quit_flag,
// interrupt_flag,
// rx,
// stdout,
// mode: ReceiverMode::Buffering,
// deadline,
// buffer: Vec::with_capacity(MAX_BUFFER_LENGTH),
// num_results: 0,
// }
// }
//
// /// Process results until finished.
// fn process(&mut self) -> ExitCode {
// loop {
// if let Err(ec) = self.poll() {
// self.quit_flag.store(true, Ordering::Relaxed);
// return ec;
// }
// }
// }
//
// /// Receive the next worker result.
// fn recv(&self) -> Result<Batch, RecvTimeoutError> {
// match self.mode {
// ReceiverMode::Buffering => {
// // Wait at most until we should switch to streaming
// self.rx.recv_deadline(self.deadline)
// }
// ReceiverMode::Streaming => {
// // Wait however long it takes for a result
// Ok(self.rx.recv()?)
// }
// }
// }
//
// /// Wait for a result or state change.
// fn poll(&mut self) -> Result<(), ExitCode> {
// match self.recv() {
// Ok(batch) => {
// for result in batch {
// match result {
// WorkerResult::Entry(dir_entry) => {
// if self.config.quiet {
// return Err(ExitCode::HasResults(true));
// }
//
// match self.mode {
// ReceiverMode::Buffering => {
// self.buffer.push(dir_entry);
// if self.buffer.len() > MAX_BUFFER_LENGTH {
// self.stream()?;
// }
// }
// ReceiverMode::Streaming => {
// self.print(&dir_entry)?;
// }
// }
//
// self.num_results += 1;
// if let Some(max_results) = self.config.max_results {
// if self.num_results >= max_results {
// return self.stop();
// }
// }
// }
// WorkerResult::Error(err) => {
// if self.config.show_filesystem_errors {
// print_error(err.to_string());
// }
// }
// }
// }
//
// // If we don't have another batch ready, flush before waiting
// if self.mode == ReceiverMode::Streaming && self.rx.is_empty() {
// self.flush()?;
// }
// }
// Err(RecvTimeoutError::Timeout) => {
// self.stream()?;
// }
// Err(RecvTimeoutError::Disconnected) => {
// return self.stop();
// }
// }
//
// Ok(())
// }
//
// /// Output a path.
// fn print(&mut self, entry: &DirEntry) -> Result<(), ExitCode> {
// if let Err(e) = output::print_entry(&mut self.stdout, entry, self.config) {
// if e.kind() != ::std::io::ErrorKind::BrokenPipe {
// print_error(format!("Could not write to output: {e}"));
// return Err(ExitCode::GeneralError);
// }
// }
//
// if self.interrupt_flag.load(Ordering::Relaxed) {
// // Ignore any errors on flush, because we're about to exit anyway
// let _ = self.flush();
// return Err(ExitCode::KilledBySigint);
// }
//
// Ok(())
// }
//
// /// Switch ourselves into streaming mode.
// fn stream(&mut self) -> Result<(), ExitCode> {
// self.mode = ReceiverMode::Streaming;
//
// let buffer = mem::take(&mut self.buffer);
// for path in buffer {
// self.print(&path)?;
// }
//
// self.flush()
// }
//
// /// Stop looping.
// fn stop(&mut self) -> Result<(), ExitCode> {
// if self.mode == ReceiverMode::Buffering {
// self.buffer.sort();
// self.stream()?;
// }
//
// if self.config.quiet {
// Err(ExitCode::HasResults(self.num_results > 0))
// } else {
// Err(ExitCode::Success)
// }
// }
//
// /// Flush stdout if necessary.
// fn flush(&mut self) -> Result<(), ExitCode> {
// if self.stdout.flush().is_err() {
// // Probably a broken pipe. Exit gracefully.
// return Err(ExitCode::GeneralError);
// }
// Ok(())
// }
// }
//
// /// State shared by the sender and receiver threads.
// struct WorkerState {
// /// The search patterns.
// patterns: Vec<Regex>,
// /// The command line configuration.
// config: Config,
// /// Flag for cleanly shutting down the parallel walk
// quit_flag: Arc<AtomicBool>,
// /// Flag specifically for quitting due to ^C
// interrupt_flag: Arc<AtomicBool>,
// }
//
// impl WorkerState {
// fn new(patterns: Vec<Regex>, config: Config) -> Self {
// let quit_flag = Arc::new(AtomicBool::new(false));
// let interrupt_flag = Arc::new(AtomicBool::new(false));
//
// Self {
// patterns,
// config,
// quit_flag,
// interrupt_flag,
// }
// }
//
// fn build_overrides(&self, paths: &[PathBuf]) -> Result<Override> {
// let first_path = &paths[0];
// let config = &self.config;
//
// let mut builder = OverrideBuilder::new(first_path);
//
// for pattern in &config.exclude_patterns {
// builder
// .add(pattern)
// .map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
// }
//
// builder
// .build()
// .map_err(|_| anyhow!("Mismatch in exclude patterns"))
// }
//
// fn build_walker(&self, paths: &[PathBuf]) -> Result<WalkParallel> {
// let first_path = &paths[0];
// let config = &self.config;
// let overrides = self.build_overrides(paths)?;
//
// let mut builder = WalkBuilder::new(first_path);
// builder
// .hidden(config.ignore_hidden)
// .ignore(config.read_fdignore)
// .parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
// .git_ignore(config.read_vcsignore)
// .git_global(config.read_vcsignore)
// .git_exclude(config.read_vcsignore)
// .require_git(config.require_git_to_read_vcsignore)
// .overrides(overrides)
// .follow_links(config.follow_links)
// // No need to check for supported platforms, option is unavailable on unsupported ones
// .same_file_system(config.one_file_system)
// .max_depth(config.max_depth);
//
// if config.read_fdignore {
// builder.add_custom_ignore_filename(".fdignore");
// }
//
// if config.read_global_ignore {
// if let Ok(basedirs) = etcetera::choose_base_strategy() {
// let global_ignore_file = basedirs.config_dir().join("fd").join("ignore");
// if global_ignore_file.is_file() {
// let result = builder.add_ignore(global_ignore_file);
// match result {
// Some(ignore::Error::Partial(_)) => (),
// Some(err) => {
// print_error(format!("Malformed pattern in global ignore file. {err}."));
// }
// None => (),
// }
// }
// }
// }
//
// for ignore_file in &config.ignore_files {
// let result = builder.add_ignore(ignore_file);
// match result {
// Some(ignore::Error::Partial(_)) => (),
// Some(err) => {
// print_error(format!("Malformed pattern in custom ignore file. {err}."));
// }
// None => (),
// }
// }
//
// for path in &paths[1..] {
// builder.add(path);
// }
//
// let walker = builder.threads(config.threads).build_parallel();
// Ok(walker)
// }
//
// /// Run the receiver work, either on this thread or a pool of background
// /// threads (for --exec).
// fn receive(&self, rx: Receiver<Batch>) -> ExitCode {
// let config = &self.config;
//
// // This will be set to `Some` if the `--exec` argument was supplied.
// if let Some(ref cmd) = config.command {
// if cmd.in_batch_mode() {
// exec::batch(rx.into_iter().flatten(), cmd, config)
// } else {
// let out_perm = Mutex::new(());
//
// thread::scope(|scope| {
// // Each spawned job will store its thread handle in here.
// let threads = config.threads;
// let mut handles = Vec::with_capacity(threads);
// for _ in 0..threads {
// let rx = rx.clone();
//
// // Spawn a job thread that will listen for and execute inputs.
// let handle = scope
// .spawn(|| exec::job(rx.into_iter().flatten(), cmd, &out_perm, config));
//
// // Push the handle of the spawned thread into the vector for later joining.
// handles.push(handle);
// }
// let exit_codes = handles.into_iter().map(|handle| handle.join().unwrap());
// merge_exitcodes(exit_codes)
// })
// }
// } else {
// let stdout = io::stdout().lock();
// let stdout = io::BufWriter::new(stdout);
//
// ReceiverBuffer::new(self, rx, stdout).process()
// }
// }
//
// /// Spawn the sender threads.
// fn spawn_senders(&self, walker: WalkParallel, tx: Sender<Batch>) {
// walker.run(|| {
// let patterns = &self.patterns;
// let config = &self.config;
// let quit_flag = self.quit_flag.as_ref();
//
// let mut limit = 0x100;
// if let Some(cmd) = &config.command {
// if !cmd.in_batch_mode() && config.threads > 1 {
// // Evenly distribute work between multiple receivers
// limit = 1;
// }
// }
// let mut tx = BatchSender::new(tx.clone(), limit);
//
// Box::new(move |entry| {
// if quit_flag.load(Ordering::Relaxed) {
// return WalkState::Quit;
// }
//
// let entry = match entry {
// Ok(ref e) if e.depth() == 0 => {
// // Skip the root directory entry.
// return WalkState::Continue;
// }
// Ok(e) => DirEntry::normal(e),
// Err(ignore::Error::WithPath {
// path,
// err: inner_err,
// }) => match inner_err.as_ref() {
// ignore::Error::Io(io_error)
// if io_error.kind() == io::ErrorKind::NotFound
// && path
// .symlink_metadata()
// .ok()
// .is_some_and(|m| m.file_type().is_symlink()) =>
// {
// DirEntry::broken_symlink(path)
// }
// _ => {
// return match tx.send(WorkerResult::Error(ignore::Error::WithPath {
// path,
// err: inner_err,
// })) {
// Ok(_) => WalkState::Continue,
// Err(_) => WalkState::Quit,
// }
// }
// },
// Err(err) => {
// return match tx.send(WorkerResult::Error(err)) {
// Ok(_) => WalkState::Continue,
// Err(_) => WalkState::Quit,
// }
// }
// };
//
// if let Some(min_depth) = config.min_depth {
// if entry.depth().map_or(true, |d| d < min_depth) {
// return WalkState::Continue;
// }
// }
//
// // Check the name first, since it doesn't require metadata
// let entry_path = entry.path();
//
// let search_str: Cow<OsStr> = if config.search_full_path {
// let path_abs_buf = filesystem::path_absolute_form(entry_path)
// .expect("Retrieving absolute path succeeds");
// Cow::Owned(path_abs_buf.as_os_str().to_os_string())
// } else {
// match entry_path.file_name() {
// Some(filename) => Cow::Borrowed(filename),
// None => unreachable!(
// "Encountered file system entry without a file name. This should only \
// happen for paths like 'foo/bar/..' or '/' which are not supposed to \
// appear in a file system traversal."
// ),
// }
// };
//
// if !patterns
// .iter()
// .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())))
// {
// return WalkState::Continue;
// }
//
// // Filter out unwanted extensions.
// if let Some(ref exts_regex) = config.extensions {
// if let Some(path_str) = entry_path.file_name() {
// if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) {
// return WalkState::Continue;
// }
// } else {
// return WalkState::Continue;
// }
// }
//
// // Filter out unwanted file types.
// if let Some(ref file_types) = config.file_types {
// if file_types.should_ignore(&entry) {
// return WalkState::Continue;
// }
// }
//
// #[cfg(unix)]
// {
// if let Some(ref owner_constraint) = config.owner_constraint {
// if let Some(metadata) = entry.metadata() {
// if !owner_constraint.matches(metadata) {
// return WalkState::Continue;
// }
// } else {
// return WalkState::Continue;
// }
// }
// }
//
// // Filter out unwanted sizes if it is a file and we have been given size constraints.
// if !config.size_constraints.is_empty() {
// if entry_path.is_file() {
// if let Some(metadata) = entry.metadata() {
// let file_size = metadata.len();
// if config
// .size_constraints
// .iter()
// .any(|sc| !sc.is_within(file_size))
// {
// return WalkState::Continue;
// }
// } else {
// return WalkState::Continue;
// }
// } else {
// return WalkState::Continue;
// }
// }
//
// // Filter out unwanted modification times
// if !config.time_constraints.is_empty() {
// let mut matched = false;
// if let Some(metadata) = entry.metadata() {
// if let Ok(modified) = metadata.modified() {
// matched = config
// .time_constraints
// .iter()
// .all(|tf| tf.applies_to(&modified));
// }
// }
// if !matched {
// return WalkState::Continue;
// }
// }
//
// if config.is_printing() {
// if let Some(ls_colors) = &config.ls_colors {
// // Compute colors in parallel
// entry.style(ls_colors);
// }
// }
//
// let send_result = tx.send(WorkerResult::Entry(entry));
//
// if send_result.is_err() {
// return WalkState::Quit;
// }
//
// // Apply pruning.
// if config.prune {
// return WalkState::Skip;
// }
//
// WalkState::Continue
// })
// });
// }
//
// /// Perform the recursive scan.
// fn scan(&self, paths: &[PathBuf]) -> Result<ExitCode> {
// let config = &self.config;
// let walker = self.build_walker(paths)?;
//
// if config.ls_colors.is_some() && config.is_printing() {
// let quit_flag = Arc::clone(&self.quit_flag);
// let interrupt_flag = Arc::clone(&self.interrupt_flag);
//
// ctrlc::set_handler(move || {
// quit_flag.store(true, Ordering::Relaxed);
//
// if interrupt_flag.fetch_or(true, Ordering::Relaxed) {
// // Ctrl-C has been pressed twice, exit NOW
// ExitCode::KilledBySigint.exit();
// }
// })
// .unwrap();
// }
//
// let (tx, rx) = bounded(2 * config.threads);
//
// let exit_code = thread::scope(|scope| {
// // Spawn the receiver thread(s)
// let receiver = scope.spawn(|| self.receive(rx));
//
// // Spawn the sender threads.
// self.spawn_senders(walker, tx);
//
// receiver.join().unwrap()
// });
//
// if self.interrupt_flag.load(Ordering::Relaxed) {
// Ok(ExitCode::KilledBySigint)
// } else {
// Ok(exit_code)
// }
// }
// }
//
// /// Recursively scan the given search path for files / pathnames matching the patterns.
// ///
// /// If the `--exec` argument was supplied, this will create a thread pool for executing
// /// jobs in parallel from a given command line and the discovered paths. Otherwise, each
// /// path will simply be written to standard output.
// pub fn scan(paths: &[PathBuf], patterns: Vec<Regex>, config: Config) -> Result<ExitCode> {
// WorkerState::new(patterns, config).scan(paths)
// }