v0.2.0 - Performance & Distribution #1

Merged
gamertan merged 3 commits from feat/optimization-refactor into main 2026-01-22 15:26:34 -05:00
7 changed files with 1014 additions and 681 deletions
Showing only changes of commit db9059e2fc - Show all commits
+6
View File
@@ -14,9 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added `MANUAL.md` with comprehensive usage documentation.
- Added comprehensive test suite (unit and integration tests).
- Added performance benchmarks using `criterion`.
### Removed
- Removed `walkdir` dependency (replaced by `ignore::WalkBuilder` for all traversal).
- Removed `humansize` (consolidated to `byte-unit`).
- Removed `chrono` (replaced with lightweight `time`).
- Removed `atty` (unused).
## [0.1.0] - 2026-01-22
Generated
+350 -140
View File
@@ -8,7 +8,7 @@ version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"getrandom 0.2.17",
"once_cell",
"version_check",
]
@@ -23,13 +23,10 @@ dependencies = [
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
@@ -88,14 +85,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "atty"
version = "0.2.14"
name = "assert_cmd"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514"
dependencies = [
"hermit-abi",
"anstyle",
"bstr",
"libc",
"winapi",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
@@ -152,6 +153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
@@ -202,14 +204,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.53"
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
dependencies = [
"find-msvc-tools",
"shlex",
]
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
@@ -224,16 +222,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.43"
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
@@ -323,10 +335,40 @@ dependencies = [
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
@@ -376,6 +418,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "csv"
version = "1.4.0"
@@ -397,6 +445,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "document-features"
version = "0.2.12"
@@ -435,10 +498,19 @@ dependencies = [
]
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]]
name = "funty"
@@ -457,6 +529,18 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "globset"
version = "0.4.18"
@@ -470,6 +554,17 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -499,45 +594,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "ignore"
@@ -565,12 +624,32 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
@@ -599,12 +678,6 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -638,6 +711,18 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -659,6 +744,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -682,6 +773,40 @@ dependencies = [
"windows-link",
]
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -691,6 +816,36 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -738,6 +893,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "radium"
version = "0.7.0"
@@ -771,7 +932,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.17",
]
[[package]]
@@ -823,6 +984,18 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
@@ -1001,12 +1174,6 @@ dependencies = [
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -1017,21 +1184,23 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
name = "sized"
version = "0.2.0"
dependencies = [
"atty",
"assert_cmd",
"byte-unit",
"chrono",
"clap",
"clap_complete",
"clap_mangen",
"colored",
"comfy-table",
"criterion",
"csv",
"humansize",
"ignore",
"predicates",
"rayon",
"serde",
"serde_json",
"strum",
"tempfile",
"time",
]
[[package]]
@@ -1096,6 +1265,66 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "time"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]]
name = "time-macros"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
@@ -1187,6 +1416,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -1203,6 +1441,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
@@ -1248,6 +1495,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -1279,65 +1536,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -1429,6 +1633,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "wyz"
version = "0.5.1"
+12 -3
View File
@@ -7,7 +7,6 @@ edition = "2021"
clap = { version = "4.4", features = ["derive"] }
rayon = "1.8"
humansize = "2.1"
colored = "2.0"
comfy-table = "7.0"
byte-unit = "5.0"
@@ -16,7 +15,17 @@ ignore = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.3"
chrono = "0.4"
time = { version = "0.3", features = ["macros", "formatting"] }
clap_complete = "4.4"
atty = "0.2"
clap_mangen = "0.2.31"
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.1"
tempfile = "3.10"
criterion = "0.5"
[[bench]]
name = "benchmark"
harness = false
+34
View File
@@ -0,0 +1,34 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use sized::build_tree;
use std::fs::{self, File};
use std::io::Write;
use tempfile::TempDir; // Ensure this is pub in lib.rs
fn setup_test_dir() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
// Create a moderately deep structure
for i in 0..10 {
let dir_path = base_path.join(format!("dir_{}", i));
fs::create_dir(&dir_path).unwrap();
for j in 0..10 {
let file_path = dir_path.join(format!("file_{}.txt", j));
let mut file = File::create(file_path).unwrap();
writeln!(file, "Some content for file {}-{}", i, j).unwrap();
}
}
temp_dir
}
fn criterion_benchmark(c: &mut Criterion) {
let temp_dir = setup_test_dir();
let path = temp_dir.path();
c.bench_function("build_tree small", |b| {
b.iter(|| build_tree(black_box(path), black_box(false)))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
+552
View File
@@ -0,0 +1,552 @@
use byte_unit::{Byte, UnitType};
use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::{generate, Shell};
use colored::*;
use comfy_table::presets::UTF8_FULL;
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
use csv::WriterBuilder;
use ignore::WalkBuilder;
use rayon::prelude::*;
use serde::Serialize;
use std::cmp::Ordering;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
#[command(disable_version_flag = true)]
pub struct Args {
/// Print version
#[arg(short = 'v', long, action = clap::ArgAction::Version)]
pub version: Option<bool>,
/// Directory to analyze
#[arg(default_value = ".")]
pub path: PathBuf,
/// Minimum size filter (e.g. "10MB", "1gb")
#[arg(short = 'm', long)]
pub min_size: Option<String>,
/// Limit the number of results per table
#[arg(short = 'n', long)]
pub number: Option<usize>,
/// Sort by columns (e.g. "type:asc,size:dsc").
/// defaults to size:dsc.
/// Columns: name, type, size, blocks.
#[arg(long)]
pub sort: Option<String>,
/// Depth to traverse (0 = only immediate children)
#[arg(short, long, default_value = "0")]
pub depth: usize,
/// Display full absolute paths
#[arg(short = 'f', long, conflicts_with = "path_relative")]
pub path_full: bool,
/// Display paths relative to current directory (default)
#[arg(long)]
pub path_relative: bool,
/// Number of threads to use (defaults to available logical CPUs)
#[arg(short = 'j', long = "threads")]
pub threads: Option<usize>,
/// Generate shell completions
#[arg(long, value_enum)]
pub completions: Option<Shell>,
/// Respect .gitignore and .ignore files
#[arg(short = 'i', long)]
pub ignore: bool,
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
/// Save output to file (optional filename, defaults to timestamped name)
#[arg(long, num_args(0..=1), default_missing_value = "_AUTO_")]
pub save: Option<PathBuf>,
}
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
pub enum OutputFormat {
Text,
Csv,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SortColumn {
Name,
Type,
Size,
Blocks,
}
impl FromStr for SortColumn {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"name" | "n" => Ok(SortColumn::Name),
"type" | "t" => Ok(SortColumn::Type),
"size" | "s" => Ok(SortColumn::Size),
"blocks" | "b" => Ok(SortColumn::Blocks),
_ => Err(format!("Unknown column: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SortDirection {
Asc,
Dsc,
}
impl FromStr for SortDirection {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"asc" | "a" => Ok(SortDirection::Asc),
"dsc" | "d" | "desc" => Ok(SortDirection::Dsc),
_ => Err(format!("Unknown direction: {}", s)),
}
}
}
#[derive(Clone, Serialize, Debug)]
pub struct Node {
pub path: PathBuf,
pub size_bytes: u64,
pub blocks: u64,
pub entry_type: EntryType,
#[serde(skip)]
pub children: Vec<Node>,
}
#[derive(Clone, Serialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EntryType {
File,
Dir,
}
impl std::fmt::Display for EntryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EntryType::File => write!(f, "File"),
EntryType::Dir => write!(f, "Dir"),
}
}
}
#[derive(Serialize)]
pub struct JsonMetrics {
pub size_bytes: u64,
pub blocks: u64,
}
#[derive(Serialize)]
pub struct JsonEntry<'a> {
pub path: &'a PathBuf,
pub metrics: JsonMetrics,
pub entry_type: String,
}
#[derive(Serialize)]
pub struct JsonReport<'a> {
pub path: &'a PathBuf,
pub total_size: u64,
pub blocks: u64,
pub entries: Vec<JsonEntry<'a>>,
}
pub fn run(args: Args, mut writer: &mut dyn Write) {
if let Some(shell) = args.completions {
let mut cmd = Args::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut std::io::stdout());
return;
}
if let Some(threads) = args.threads {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
.ok(); // Ignore error if initialized called multiple times (e.g. in tests)
}
let target_path = args.path.clone();
if !target_path.exists() {
eprintln!("Error: Path '{}' does not exist.", target_path.display());
std::process::exit(1);
}
let min_bytes = if let Some(size_str) = &args.min_size {
match Byte::parse_str(size_str, true) {
Ok(byte) => byte.as_u64(),
Err(e) => {
eprintln!("Error parsing size '{}': {}", size_str, e);
std::process::exit(1);
}
}
} else {
0
};
let sort_criteria = parse_sort_arg(args.sort.as_deref().unwrap_or("size:dsc"));
if args.format == OutputFormat::Csv {
writeln!(writer, "Path,Type,Size(Bytes),Blocks").expect("Failed to write CSV header");
}
let root_node = build_tree(&target_path, args.ignore);
if root_node.size_bytes < min_bytes {
if args.format == OutputFormat::Text {
writeln!(writer, "Root directory is smaller than minimum size.").ok();
}
return;
}
process_node_recursive(&mut writer, &root_node, 0, &args, min_bytes, &sort_criteria);
}
pub fn build_tree(path: &Path, ignore: bool) -> Node {
let metadata = path.metadata();
if let Ok(meta) = metadata {
if meta.is_file() {
return Node {
path: path.to_path_buf(),
size_bytes: meta.len(),
blocks: meta.blocks(),
entry_type: EntryType::File,
children: vec![],
};
}
}
let walker = WalkBuilder::new(path)
.standard_filters(false)
.hidden(false)
.git_ignore(ignore)
.ignore(ignore)
.max_depth(Some(1))
.build();
let child_paths: Vec<PathBuf> = walker
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path() != path)
.map(|e| e.path().to_path_buf())
.collect();
let children: Vec<Node> = child_paths
.par_iter()
.map(|p| build_tree(p, ignore))
.collect();
let (size_bytes, blocks) = children
.iter()
.fold((0, 0), |acc, c| (acc.0 + c.size_bytes, acc.1 + c.blocks));
let (self_size, self_blocks) = path
.metadata()
.map(|m| (m.len(), m.blocks()))
.unwrap_or((0, 0));
Node {
path: path.to_path_buf(),
size_bytes: size_bytes + self_size,
blocks: blocks + self_blocks,
entry_type: EntryType::Dir,
children,
}
}
fn process_node_recursive(
writer: &mut dyn Write,
node: &Node,
current_depth: usize,
args: &Args,
min_bytes: u64,
sort_criteria: &[(SortColumn, SortDirection)],
) {
if node.children.is_empty() && node.entry_type == EntryType::Dir {}
let mut display_children: Vec<&Node> = node
.children
.iter()
.filter(|c| c.size_bytes >= min_bytes)
.collect();
display_children.sort_by(|a, b| compare_nodes(a, b, sort_criteria));
if let Some(n) = args.number {
if n < display_children.len() {
display_children.truncate(n);
}
}
let show_relative = !args.path_full;
let relative_path = if show_relative {
let cwd = std::env::current_dir().unwrap_or_default();
node.path
.strip_prefix(&cwd)
.unwrap_or(&node.path)
.display()
.to_string()
} else {
node.path
.canonicalize()
.unwrap_or(node.path.clone())
.display()
.to_string()
};
print_output(
writer,
&node,
&display_children,
args,
&relative_path,
current_depth,
);
if current_depth < args.depth {
for child in display_children {
if child.entry_type == EntryType::Dir {
process_node_recursive(
writer,
child,
current_depth + 1,
args,
min_bytes,
sort_criteria,
);
}
}
}
}
pub fn compare_nodes(a: &Node, b: &Node, criteria: &[(SortColumn, SortDirection)]) -> Ordering {
for (col, dir) in criteria {
let order = match col {
SortColumn::Name => a.path.file_name().cmp(&b.path.file_name()),
SortColumn::Type => a.entry_type.cmp(&b.entry_type),
SortColumn::Size => a.size_bytes.cmp(&b.size_bytes),
SortColumn::Blocks => a.blocks.cmp(&b.blocks),
};
if order != Ordering::Equal {
return match dir {
SortDirection::Asc => order,
SortDirection::Dsc => order.reverse(),
};
}
}
Ordering::Equal
}
pub fn parse_sort_arg(s: &str) -> Vec<(SortColumn, SortDirection)> {
s.split(',')
.filter_map(|part| {
let parts: Vec<&str> = part.split(':').collect();
if parts.is_empty() {
return None;
}
let col_str = parts[0];
let col = SortColumn::from_str(col_str).ok()?;
let dir = if parts.len() > 1 {
SortDirection::from_str(parts[1]).unwrap_or(SortDirection::Dsc)
} else {
match col {
SortColumn::Name | SortColumn::Type => SortDirection::Asc,
_ => SortDirection::Dsc,
}
};
Some((col, dir))
})
.collect()
}
fn print_output(
writer: &mut dyn Write,
parent_node: &Node,
children: &[&Node],
args: &Args,
relative_path: &str,
depth: usize,
) {
match args.format {
OutputFormat::Text => print_text(writer, parent_node, children, relative_path, depth),
OutputFormat::Csv => print_csv(writer, children),
OutputFormat::Json => print_json(writer, parent_node, children),
}
}
fn print_text(
writer: &mut dyn Write,
parent_node: &Node,
children: &[&Node],
relative_path: &str,
depth: usize,
) {
if depth > 0 {
writeln!(writer, "{}", "-".repeat(60).dimmed()).ok();
}
writeln!(writer, "\n{}", relative_path.bold().blue()).ok();
let total_byte = Byte::from_u64(parent_node.size_bytes);
writeln!(
writer,
"Total size: {} / {} ({} blocks)",
total_byte
.get_appropriate_unit(UnitType::Binary)
.to_string()
.bold()
.green(),
parent_node.size_bytes.to_string().bold().green(),
parent_node.blocks
)
.ok();
if children.is_empty() {
writeln!(writer, "(No children)").ok();
return;
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("Name").add_attribute(Attribute::Bold),
Cell::new("Type").add_attribute(Attribute::Bold),
Cell::new("Size (Human)")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
Cell::new("Size (Bytes)")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
Cell::new("Blocks")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
]);
for child in children {
let name = child.path.file_name().unwrap_or_default().to_string_lossy();
let color = if child.entry_type == EntryType::Dir {
Color::Blue
} else {
Color::Cyan
};
let child_byte = Byte::from_u64(child.size_bytes);
table.add_row(vec![
Cell::new(name).fg(color),
Cell::new(&child.entry_type).fg(Color::Yellow),
Cell::new(
child_byte
.get_appropriate_unit(UnitType::Binary)
.to_string(),
)
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
Cell::new(child.size_bytes.to_string()) // Raw bytes instead of "Decimal" unit
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
Cell::new(child.blocks)
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
]);
}
writeln!(writer, "{}", table).ok();
}
fn print_csv(writer: &mut dyn Write, children: &[&Node]) {
let mut wtr = WriterBuilder::new().has_headers(false).from_writer(writer);
for child in children {
wtr.write_record(&[
child.path.to_string_lossy().as_ref(),
&child.entry_type.to_string(),
&child.size_bytes.to_string(),
&child.blocks.to_string(),
])
.ok();
}
wtr.flush().ok();
}
fn print_json(writer: &mut dyn Write, parent_node: &Node, children: &[&Node]) {
let entries_view: Vec<JsonEntry> = children
.iter()
.map(|c| JsonEntry {
path: &c.path,
metrics: JsonMetrics {
size_bytes: c.size_bytes,
blocks: c.blocks,
},
entry_type: c.entry_type.to_string(),
})
.collect();
let report = JsonReport {
path: &parent_node.path,
total_size: parent_node.size_bytes,
blocks: parent_node.blocks,
entries: entries_view,
};
serde_json::to_writer(&mut *writer, &report).ok();
writer.write_all(b"\n").ok();
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_parse_sort_arg() {
let criteria = parse_sort_arg("size:asc,name");
assert_eq!(criteria.len(), 2);
assert_eq!(criteria[0], (SortColumn::Size, SortDirection::Asc));
assert_eq!(criteria[1], (SortColumn::Name, SortDirection::Asc)); // Default for Name is Asc
}
#[test]
fn test_sort_defaults() {
let criteria = parse_sort_arg("size");
assert_eq!(criteria[0], (SortColumn::Size, SortDirection::Dsc)); // Default for Size is Dsc
}
#[test]
fn test_build_tree() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
File::create(&file_path)
.unwrap()
.write_all(b"Hello")
.unwrap();
let node = build_tree(temp_dir.path(), false);
assert_eq!(node.entry_type, EntryType::Dir);
assert!(!node.children.is_empty());
assert_eq!(node.children[0].path, file_path);
// Size should be at least 5 bytes
assert!(node.children[0].size_bytes >= 5);
}
}
+13 -538
View File
@@ -1,222 +1,23 @@
use byte_unit::Byte;
use chrono::Local;
use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::{generate, Shell};
use colored::*;
use comfy_table::presets::UTF8_FULL;
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
use csv::WriterBuilder;
use humansize::{format_size, BINARY, DECIMAL};
use ignore::WalkBuilder;
use rayon::prelude::*;
use serde::Serialize;
use std::cmp::Ordering;
use clap::Parser;
use sized::{run, Args, OutputFormat};
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
#[command(disable_version_flag = true)]
struct Args {
/// Print version
#[arg(short = 'v', long, action = clap::ArgAction::Version)]
version: Option<bool>,
/// Directory to analyze
#[arg(default_value = ".")]
path: PathBuf,
/// Minimum size filter (e.g. "10MB", "1gb")
#[arg(short = 'm', long)]
min_size: Option<String>,
/// Limit the number of results per table
#[arg(short = 'n', long)]
number: Option<usize>,
/// Sort by columns (e.g. "type:asc,size:dsc").
/// defaults to size:dsc.
/// Columns: name, type, size, blocks.
#[arg(long)]
sort: Option<String>,
/// Depth to traverse (0 = only immediate children)
#[arg(short, long, default_value = "0")]
depth: usize,
/// Display full absolute paths
#[arg(short = 'f', long, conflicts_with = "path_relative")]
path_full: bool,
/// Display paths relative to current directory (default)
#[arg(long)]
path_relative: bool,
/// Number of threads to use (defaults to available logical CPUs)
#[arg(short = 'j', long = "threads")]
threads: Option<usize>,
/// Generate shell completions
#[arg(long, value_enum)]
completions: Option<Shell>,
/// Respect .gitignore and .ignore files
#[arg(short = 'i', long)]
ignore: bool,
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
/// Save output to file (optional filename, defaults to timestamped name)
#[arg(long, num_args(0..=1), default_missing_value = "_AUTO_")]
save: Option<PathBuf>,
}
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum OutputFormat {
Text,
Csv,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SortColumn {
Name,
Type,
Size,
Blocks,
}
impl FromStr for SortColumn {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"name" | "n" => Ok(SortColumn::Name),
"type" | "t" => Ok(SortColumn::Type),
"size" | "s" => Ok(SortColumn::Size),
"blocks" | "b" => Ok(SortColumn::Blocks),
_ => Err(format!("Unknown column: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SortDirection {
Asc,
Dsc,
}
impl FromStr for SortDirection {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"asc" | "a" => Ok(SortDirection::Asc),
"dsc" | "d" | "desc" => Ok(SortDirection::Dsc),
_ => Err(format!("Unknown direction: {}", s)),
}
}
}
#[derive(Clone, Serialize, Debug)]
struct Node {
path: PathBuf,
size_bytes: u64,
blocks: u64,
entry_type: EntryType,
#[serde(skip)]
children: Vec<Node>,
}
#[derive(Clone, Serialize, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum EntryType {
File,
Dir,
}
impl std::fmt::Display for EntryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EntryType::File => write!(f, "File"),
EntryType::Dir => write!(f, "Dir"),
}
}
}
#[derive(Serialize)]
struct JsonMetrics {
size_bytes: u64,
blocks: u64,
}
#[derive(Serialize)]
struct JsonEntry<'a> {
path: &'a PathBuf,
metrics: JsonMetrics,
entry_type: String,
}
#[derive(Serialize)]
struct JsonReport<'a> {
path: &'a PathBuf,
total_size: u64,
blocks: u64,
entries: Vec<JsonEntry<'a>>,
}
use std::path::PathBuf;
use time::macros::format_description;
use time::OffsetDateTime;
fn main() {
let args = Args::parse();
// Handle completions
if let Some(shell) = args.completions {
let mut cmd = Args::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut std::io::stdout());
return;
}
// Determine output writer logic (moved mostly to main to keep lib clean,
// or keep in lib? Plan said simplified main. Let's keep file creation in main usually for CLI apps,
// but the logic for "auto name" is business logic.
// Wait, the lib function `run` takes `writer`. So we need to create the writer here.
// Configure Rayon
if let Some(threads) = args.threads {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
.expect("Failed to build thread pool");
}
let target_path = args.path.clone();
if !target_path.exists() {
eprintln!("Error: Path '{}' does not exist.", target_path.display());
std::process::exit(1);
}
// Parse min_size
let min_bytes = if let Some(size_str) = &args.min_size {
match Byte::parse_str(size_str, true) {
Ok(byte) => byte.as_u64(),
Err(e) => {
eprintln!("Error parsing size '{}': {}", size_str, e);
std::process::exit(1);
}
}
} else {
0
};
// Parse sort
let sort_criteria = parse_sort_arg(args.sort.as_deref().unwrap_or("size:dsc"));
// Determine output writer
let mut writer: Box<dyn Write> = if let Some(path_arg) = &args.save {
let output_path = if path_arg.to_string_lossy() == "_AUTO_" {
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
let dir_name = target_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let format = format_description!("[year][month][day]-[hour][minute][second]");
let timestamp = OffsetDateTime::now_utc().format(format).unwrap();
let dir_name = args.path.file_name().unwrap_or_default().to_string_lossy();
let ext = match args.format {
OutputFormat::Csv => "csv",
OutputFormat::Json => "json",
@@ -233,331 +34,5 @@ fn main() {
Box::new(std::io::stdout())
};
if args.format == OutputFormat::Csv {
// We do NOT write the header here manually anymore if we want to be safe/consistent,
// OR we write it manually but treat it as raw bytes.
// Actually, if we use csv::Writer, we should use it for the header too, OR assume the writer calls will handle it (no, we need to init).
// For simplicity in this logic, let's write the header manually *once* if we are creating a fresh file/stream.
// BUT, `process_node_recursive` might be called multiple times?
// Wait, `process_node_recursive` is called recursively.
// The CSV logic implementation in `print_output` currently printed children.
// If we recurse, we print children of children.
// So a single global header is correct.
writeln!(writer, "Path,Type,Size(Bytes),Blocks").expect("Failed to write CSV header");
}
// Build the tree!
let root_node = build_tree(&target_path, args.ignore);
if root_node.size_bytes < min_bytes {
// Even the root is too small?
if args.format == OutputFormat::Text {
writeln!(writer, "Root directory is smaller than minimum size.").ok();
}
return;
}
// Render output
process_node_recursive(&mut writer, &root_node, 0, &args, min_bytes, &sort_criteria);
}
/// Builds the directory tree in memory, calculating sizes in a single pass.
/// Uses Rayon for parallelism.
fn build_tree(path: &Path, ignore: bool) -> Node {
let metadata = path.metadata();
if let Ok(meta) = metadata {
if meta.is_file() {
return Node {
path: path.to_path_buf(),
size_bytes: meta.len(),
blocks: meta.blocks(),
entry_type: EntryType::File,
children: vec![],
};
}
}
// It's a directory (or error, which we treat as zero-size usually, but here we try to read)
// Use ignore::WalkBuilder to get *immediate children only* respecting gitignore
let walker = WalkBuilder::new(path)
.standard_filters(false) // We manually control ignore
.hidden(false) // Show hidden files
.git_ignore(ignore)
.ignore(ignore)
.max_depth(Some(1)) // ONLY immediate children
.build();
let child_paths: Vec<PathBuf> = walker
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path() != path) // Exclude self
.map(|e| e.path().to_path_buf())
.collect();
// Recursively build children in parallel
let children: Vec<Node> = child_paths
.par_iter()
.map(|p| build_tree(p, ignore))
.collect();
// Sum up stats
let (size_bytes, blocks) = children
.iter()
.fold((0, 0), |acc, c| (acc.0 + c.size_bytes, acc.1 + c.blocks));
// Add self-size (directory entry itself has size)
let (self_size, self_blocks) = path
.metadata()
.map(|m| (m.len(), m.blocks()))
.unwrap_or((0, 0));
Node {
path: path.to_path_buf(),
size_bytes: size_bytes + self_size,
blocks: blocks + self_blocks,
entry_type: EntryType::Dir,
children,
}
}
fn process_node_recursive(
writer: &mut dyn Write,
node: &Node,
current_depth: usize,
args: &Args,
min_bytes: u64,
sort_criteria: &[(SortColumn, SortDirection)],
) {
if node.children.is_empty() && node.entry_type == EntryType::Dir {
// Empty directory or leaf in terms of permissions/ignore
}
// Prepare children for display
let mut display_children: Vec<&Node> = node
.children
.iter()
.filter(|c| c.size_bytes >= min_bytes)
.collect();
// Sort
display_children.sort_by(|a, b| compare_nodes(a, b, sort_criteria));
// Limit
if let Some(n) = args.number {
if n < display_children.len() {
display_children.truncate(n);
}
}
// Default to relative paths unless --path-full is specified
let show_relative = !args.path_full;
let relative_path = if show_relative {
let cwd = std::env::current_dir().unwrap_or_default();
node.path
.strip_prefix(&cwd)
.unwrap_or(&node.path)
.display()
.to_string()
} else {
node.path
.canonicalize()
.unwrap_or(node.path.clone())
.display()
.to_string()
};
print_output(
writer,
&node,
&display_children,
args,
&relative_path,
current_depth,
);
// Recurse
if current_depth < args.depth {
for child in display_children {
if child.entry_type == EntryType::Dir {
process_node_recursive(
writer,
child,
current_depth + 1,
args,
min_bytes,
sort_criteria,
);
}
}
}
}
fn compare_nodes(a: &Node, b: &Node, criteria: &[(SortColumn, SortDirection)]) -> Ordering {
for (col, dir) in criteria {
let order = match col {
SortColumn::Name => a.path.file_name().cmp(&b.path.file_name()),
SortColumn::Type => a.entry_type.cmp(&b.entry_type),
SortColumn::Size => a.size_bytes.cmp(&b.size_bytes),
SortColumn::Blocks => a.blocks.cmp(&b.blocks),
};
if order != Ordering::Equal {
return match dir {
SortDirection::Asc => order,
SortDirection::Dsc => order.reverse(),
};
}
}
Ordering::Equal
}
fn parse_sort_arg(s: &str) -> Vec<(SortColumn, SortDirection)> {
s.split(',')
.filter_map(|part| {
let parts: Vec<&str> = part.split(':').collect();
if parts.is_empty() {
return None;
}
let col_str = parts[0];
let col = SortColumn::from_str(col_str).ok()?;
let dir = if parts.len() > 1 {
SortDirection::from_str(parts[1]).unwrap_or(SortDirection::Dsc)
} else {
match col {
SortColumn::Name | SortColumn::Type => SortDirection::Asc,
_ => SortDirection::Dsc,
}
};
Some((col, dir))
})
.collect()
}
fn print_output(
writer: &mut dyn Write,
parent_node: &Node,
children: &[&Node],
args: &Args,
relative_path: &str,
depth: usize,
) {
match args.format {
OutputFormat::Text => print_text(writer, parent_node, children, relative_path, depth),
OutputFormat::Csv => print_csv(writer, children),
OutputFormat::Json => print_json(writer, parent_node, children),
}
}
fn print_text(
writer: &mut dyn Write,
parent_node: &Node,
children: &[&Node],
relative_path: &str,
depth: usize,
) {
if depth > 0 {
writeln!(writer, "{}", "-".repeat(60).dimmed()).ok();
}
writeln!(writer, "\n{}", relative_path.bold().blue()).ok();
writeln!(
writer,
"Total size: {} / {} ({} blocks)",
format_size(parent_node.size_bytes, BINARY).bold().green(),
format_size(parent_node.size_bytes, DECIMAL).bold().green(),
parent_node.blocks
)
.ok();
if children.is_empty() {
writeln!(writer, "(No children)").ok();
return;
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("Name").add_attribute(Attribute::Bold),
Cell::new("Type").add_attribute(Attribute::Bold),
Cell::new("Size (Bin)")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
Cell::new("Size (Bytes)")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
Cell::new("Blocks")
.add_attribute(Attribute::Bold)
.set_alignment(CellAlignment::Right),
]);
for child in children {
let name = child.path.file_name().unwrap_or_default().to_string_lossy();
let color = if child.entry_type == EntryType::Dir {
Color::Blue
} else {
Color::Cyan
};
table.add_row(vec![
Cell::new(name).fg(color),
Cell::new(&child.entry_type).fg(Color::Yellow),
Cell::new(format_size(child.size_bytes, BINARY))
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
Cell::new(format_size(child.size_bytes, DECIMAL))
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
Cell::new(child.blocks)
.fg(Color::Green)
.set_alignment(CellAlignment::Right),
]);
}
writeln!(writer, "{}", table).ok();
}
fn print_csv(writer: &mut dyn Write, children: &[&Node]) {
// strict csv writing
let mut wtr = WriterBuilder::new()
.has_headers(false) // We already wrote the header globally
.from_writer(writer);
for child in children {
wtr.write_record(&[
child.path.to_string_lossy().as_ref(), // This handles escaping automatically
&child.entry_type.to_string(),
&child.size_bytes.to_string(),
&child.blocks.to_string(),
])
.ok();
}
wtr.flush().ok();
}
fn print_json(writer: &mut dyn Write, parent_node: &Node, children: &[&Node]) {
// Zero-copy serialization
let entries_view: Vec<JsonEntry> = children
.iter()
.map(|c| JsonEntry {
path: &c.path,
metrics: JsonMetrics {
size_bytes: c.size_bytes,
blocks: c.blocks,
},
entry_type: c.entry_type.to_string(),
})
.collect();
let report = JsonReport {
path: &parent_node.path,
total_size: parent_node.size_bytes,
blocks: parent_node.blocks,
entries: entries_view,
};
serde_json::to_writer(&mut *writer, &report).ok();
writer.write_all(b"\n").ok();
run(args, &mut writer);
}
+47
View File
@@ -0,0 +1,47 @@
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::fs::File;
use std::io::Write;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_basic_cli_run() {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
cmd.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("sized 0.2.0"));
}
#[test]
fn test_run_on_directory() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
File::create(&file_path)
.unwrap()
.write_all(b"Hello")
.unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
cmd.arg(temp_dir.path())
.assert()
.success()
.stdout(predicate::str::contains("test_file.txt")); // Basic check
}
#[test]
fn test_json_output() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("data.json");
File::create(&file_path).unwrap().write_all(b"{}").unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_sized"));
cmd.arg(temp_dir.path())
.arg("--format")
.arg("json")
.assert()
.success()
.stdout(predicate::str::contains("\"entry_type\":\"File\""))
.stdout(predicate::str::contains("\"path\":"));
}