From ab5558f537961e17c36ae9dbb5b8069dff3092e9 Mon Sep 17 00:00:00 2001 From: elipeter Date: Mon, 16 Jun 2025 16:46:22 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + Cargo.lock | 813 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 + src/cli.rs | 88 +++++ src/commands/clean.rs | 37 ++ src/commands/index.rs | 48 +++ src/commands/list.rs | 35 ++ src/commands/mod.rs | 29 ++ src/commands/scan.rs | 53 +++ src/exit_codes.rs | 0 src/filetypes.rs | 43 +++ src/main.rs | 59 +++ src/utils/config.rs | 260 ++++++++++++++ src/utils/mod.rs | 6 + src/utils/project.rs | 31 ++ src/walk.rs | 670 ++++++++++++++++++++++++++++++++++ 16 files changed, 2187 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/cli.rs create mode 100644 src/commands/clean.rs create mode 100644 src/commands/index.rs create mode 100644 src/commands/list.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/scan.rs create mode 100644 src/exit_codes.rs create mode 100644 src/filetypes.rs create mode 100644 src/main.rs create mode 100644 src/utils/config.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/project.rs create mode 100644 src/walk.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..667d9ebb --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..6b6ad1bd --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..3055fa13 --- /dev/null +++ b/src/cli.rs @@ -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, + + /// 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, +} \ No newline at end of file diff --git a/src/commands/clean.rs b/src/commands/clean.rs new file mode 100644 index 00000000..67892980 --- /dev/null +++ b/src/commands/clean.rs @@ -0,0 +1,37 @@ +use std::{env, fs}; +use crate::utils::get_project_info; + +pub fn handle( + project: Option, + all: bool, + config_dir: &std::path::Path, +) -> Result<(), Box> { + 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(¤t_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(()) +} \ No newline at end of file diff --git a/src/commands/index.rs b/src/commands/index.rs new file mode 100644 index 00000000..a96bf9f2 --- /dev/null +++ b/src/commands/index.rs @@ -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> { + 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> { + // TODO: Implement actual index building + fs::File::create(db_path)?; + println!("Index building logic goes here..."); + Ok(()) +} \ No newline at end of file diff --git a/src/commands/list.rs b/src/commands/list.rs new file mode 100644 index 00000000..4b9200ab --- /dev/null +++ b/src/commands/list.rs @@ -0,0 +1,35 @@ +use std::fs; + +pub fn handle( + verbose: bool, + database_dir: &std::path::Path, +) -> Result<(), Box> { + 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(()) +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 00000000..6d81a9e4 --- /dev/null +++ b/src/commands/mod.rs @@ -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> { + 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) + } + } +} \ No newline at end of file diff --git a/src/commands/scan.rs b/src/commands/scan.rs new file mode 100644 index 00000000..44e27122 --- /dev/null +++ b/src/commands/scan.rs @@ -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> { + 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> { + // TODO: Implement direct filesystem scanning + tracing::info!("Direct filesystem scan of: {}", path.display()); + Ok(()) +} + +fn scan_with_index(db_path: &Path) -> Result<(), Box> { + // TODO: Implement index-based scanning + tracing::info!("Index-based scan using: {}", db_path.display()); + Ok(()) +} \ No newline at end of file diff --git a/src/exit_codes.rs b/src/exit_codes.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/filetypes.rs b/src/filetypes.rs new file mode 100644 index 00000000..a4b129db --- /dev/null +++ b/src/filetypes.rs @@ -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 +// } +// } +// } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..7473334e --- /dev/null +++ b/src/main.rs @@ -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> { + 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(()) +} + diff --git a/src/utils/config.rs b/src/utils/config.rs new file mode 100644 index 00000000..32a97b2f --- /dev/null +++ b/src/utils/config.rs @@ -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, + + /// Directories to exclude from scanning. TODO: IMPLEMENT + pub excluded_directories: Vec, + + /// 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, +} + +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, // TODO: IMPLEMENT + + /// The minimum depth for reported entries, or `None`. + pub min_depth: Option, // 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, // 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> { + 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> { + 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 +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..0dfa31d0 --- /dev/null +++ b/src/utils/mod.rs @@ -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; \ No newline at end of file diff --git a/src/utils/project.rs b/src/utils/project.rs new file mode 100644 index 00000000..c53cf256 --- /dev/null +++ b/src/utils/project.rs @@ -0,0 +1,31 @@ +use std::path::{Path, PathBuf}; + +pub fn get_project_info( + project_path: &Path, + config_dir: &Path, +) -> Result<(String, PathBuf), Box> { + 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::() + .split('_') + .filter(|s| !s.is_empty()) + .collect::>() + .join("_") +} \ No newline at end of file diff --git a/src/walk.rs b/src/walk.rs new file mode 100644 index 00000000..190777f2 --- /dev/null +++ b/src/walk.rs @@ -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>>>, +// } +// +// impl Batch { +// fn new() -> Self { +// Self { +// items: Arc::new(Mutex::new(Some(vec![]))), +// } +// } +// +// fn lock(&self) -> MutexGuard<'_, Option>> { +// self.items.lock().unwrap() +// } +// } +// +// impl IntoIterator for Batch { +// type Item = WorkerResult; +// type IntoIter = std::vec::IntoIter; +// +// 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, +// limit: usize, +// } +// +// impl BatchSender { +// fn new(tx: Sender, limit: usize) -> Self { +// Self { +// batch: Batch::new(), +// tx, +// limit, +// } +// } +// +// /// Check if we need to flush a batch. +// fn needs_flush(&self, batch: Option<&Vec>) -> 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, +// /// 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, +// /// Result count. +// num_results: usize, +// } +// +// impl<'a, W: Write> ReceiverBuffer<'a, W> { +// /// Create a new receiver buffer. +// fn new(state: &'a WorkerState, rx: Receiver, 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 { +// 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, +// /// The command line configuration. +// config: Config, +// /// Flag for cleanly shutting down the parallel walk +// quit_flag: Arc, +// /// Flag specifically for quitting due to ^C +// interrupt_flag: Arc, +// } +// +// impl WorkerState { +// fn new(patterns: Vec, 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 { +// 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 { +// 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) -> 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) { +// 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 = 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 { +// 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, config: Config) -> Result { +// WorkerState::new(patterns, config).scan(paths) +// } \ No newline at end of file