diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f48e57..3c59af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ -# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev -# Copyright 2025 Astral Software Inc. # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -65,7 +64,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: @@ -218,8 +217,8 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 75b02f6..3c2bb37 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,21 +1,26 @@ -name: Rust +name: Rust CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always jobs: - build: - + checks: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - \ No newline at end of file + - uses: actions/checkout@v4 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + - name: Cargo fmt + run: cargo fmt --all -- --check + - name: Cargo build + run: cargo build --verbose + - name: Cargo test + run: cargo test --verbose + - name: Cargo clippy + run: cargo clippy --all-targets --all-features diff --git a/Cargo.lock b/Cargo.lock index 0a8677b..2c08f15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,20 +3,14 @@ version = 4 [[package]] -name = "addr2line" -version = "0.25.1" +name = "aho-corasick" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ - "gimli", + "memchr", ] -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "allocator-api2" version = "0.2.21" @@ -64,29 +58,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "async-trait" @@ -96,7 +90,16 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", ] [[package]] @@ -106,19 +109,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backtrace" -version = "0.3.76" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" @@ -140,6 +134,39 @@ dependencies = [ "virtue", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.116", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -148,15 +175,39 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -166,34 +217,46 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] [[package]] -name = "cassowary" -version = "0.3.0" +name = "castaway" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] [[package]] name = "cc" -version = "1.2.41" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -203,9 +266,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -215,20 +278,32 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.48" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" dependencies = [ "anstream", "anstyle", @@ -236,11 +311,23 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -250,36 +337,36 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ - "crossterm 0.29.0", + "crossterm", "unicode-segmentation", - "unicode-width 0.2.2", + "unicode-width", ] [[package]] -name = "console" -version = "0.16.1" +name = "compact_str" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width 0.2.2", - "windows-sys 0.61.2", + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "core-foundation-sys", - "libc", + "unicode-segmentation", ] [[package]] @@ -289,48 +376,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "crossterm" -version = "0.25.0" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "bitflags 1.3.2", - "crossterm_winapi", "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", ] [[package]] -name = "crossterm" -version = "0.27.0" +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "bitflags 2.9.4", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +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 = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm_winapi", + "derive_more", "document-features", + "futures-core", + "mio", "parking_lot", "rustix", + "signal-hook", + "signal-hook-mio", "winapi", ] @@ -343,42 +442,146 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.116", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.116", +] + [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deltae" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.116", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -387,7 +590,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -403,9 +606,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -422,22 +625,16 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -456,11 +653,59 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -468,6 +713,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -479,9 +730,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -494,9 +745,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -504,15 +755,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -521,38 +772,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -562,15 +813,33 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -578,39 +847,73 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.32.3" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", ] [[package]] -name = "heck" -version = "0.4.1" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hickory-proto" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "cfg-if", @@ -622,8 +925,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand", - "thiserror 1.0.69", + "rand 0.9.2", + "ring", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -632,39 +936,39 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", - "lru-cache", + "moka", "once_cell", "parking_lot", - "rand", + "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -686,9 +990,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -699,9 +1003,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -712,11 +1016,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -727,42 +1030,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -770,6 +1069,18 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -792,49 +1103,51 @@ dependencies = [ ] [[package]] -name = "indicatif" -version = "0.18.0" +name = "indexmap" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.2", - "unit-prefix", - "web-time", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "inquire" -version = "0.6.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +checksum = "979f5ab9760427ada4fa5762b2d905e5b12704fb1fada07b6bfa66aeaa586f87" dependencies = [ - "bitflags 1.3.2", - "crossterm 0.25.0", + "bitflags 2.11.0", + "crossterm", "dyn-clone", - "lazy_static", - "newline-converter", - "thiserror 1.0.69", + "fuzzy-matcher", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] -name = "io-uring" -version = "0.7.10" +name = "instability" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.116", ] [[package]] @@ -846,7 +1159,7 @@ dependencies = [ "socket2 0.5.10", "widestring", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -860,52 +1173,108 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libproc" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" +dependencies = [ + "bindgen", + "errno", + "libc", +] + +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "linux-raw-sys" @@ -915,15 +1284,15 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -936,144 +1305,108 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "lru-cache" -version = "0.1.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "linked-hash-map", + "hashbrown 0.16.1", ] [[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "mac-addr" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" dependencies = [ - "adler2", + "serde", ] [[package]] -name = "mio" -version = "0.8.11" +name = "mac_address" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", + "nix 0.29.0", + "winapi", ] [[package]] -name = "mio" -version = "1.0.4" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.59.0", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "ndb-as" -version = "0.3.0" +name = "memmem" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db9338c36180d04f2bc643ffe58f16f8a05aca91aed39bb76d361037bbbc704" -dependencies = [ - "anyhow", - "bincode", - "csv", - "serde", -] +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" [[package]] -name = "ndb-core" -version = "0.3.0" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f689236631fba991a5b369d9a1ff6d8b80beffe41f63b3c2799a59b2d83c5" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "serde", + "autocfg", ] [[package]] -name = "ndb-ipv4-asn" -version = "0.3.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420d278f3bcfd844e0eb534db5792fbd03221cda722c77de35f419e0d55159ce" -dependencies = [ - "anyhow", - "bincode", - "csv", - "rangemap", - "serde", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "ndb-ipv4-country" -version = "0.3.0" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ff6dd47f8c0b43b176780a7b6ee387ed164ffd7165c668e5adf22a40c91b54" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "anyhow", - "bincode", - "csv", - "rangemap", - "serde", + "libc", + "log", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "ndb-ipv6-asn" -version = "0.3.0" +name = "moka" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f22c4931b16f6cbb0d2c8714ba1d038d6c92ffe64ee0e5416b6ef1eb7908354" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ - "anyhow", - "bincode", - "csv", - "rangemap", - "serde", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", ] [[package]] -name = "ndb-ipv6-country" -version = "0.3.0" +name = "ndb-core" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa9624cfe664a56ae9f981d29b14e525999cfaad4324fc8dfb8476d609c6e9" +checksum = "108c0d5054ede174dce131d107ecca1bf1d802065ea9a561c62d3ebed3ce7b1c" dependencies = [ - "anyhow", - "bincode", - "csv", - "rangemap", "serde", ] [[package]] name = "ndb-tcp-service" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a6c17a7056c70fbf51642882574f4f956d4cf23513774ee2302b024ddf9adc" +checksum = "4ee136c79a7f3791a91760ca237d4baec4de732d140e5d4da2b283493796e45d" dependencies = [ "anyhow", "bincode", @@ -1084,9 +1417,9 @@ dependencies = [ [[package]] name = "ndb-udp-service" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2419c0c85f94fa317d29bcf4052bb6b884e199749a2c662020e02b7d5be143c4" +checksum = "144e1644cbd997fb7382100c5534ec311d3fc2fe33cca045edcec7beaf99d46e" dependencies = [ "anyhow", "bincode", @@ -1097,19 +1430,24 @@ dependencies = [ [[package]] name = "netdev" -version = "0.38.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c1c4a111cf1ba52aa040e77e55fd0e2066d03a3d6ea7d8f383725166a60820" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" dependencies = [ + "block2", + "dispatch2", "dlopen2", "ipnet", "libc", + "mac-addr", "netlink-packet-core 0.8.1", "netlink-packet-route", "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", "once_cell", + "plist", "serde", - "system-configuration", "windows-sys 0.59.0", ] @@ -1139,7 +1477,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", "log", "netlink-packet-core 0.8.1", @@ -1174,9 +1512,9 @@ dependencies = [ [[package]] name = "netlink-sys" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" dependencies = [ "bytes", "libc", @@ -1185,12 +1523,13 @@ dependencies = [ [[package]] name = "netsock" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0619d68669a4107fe8be0e76d4e7557304c83d7e6a23d50e3111b5dab888741" +checksum = "41e6a43de4034ef4b62e99c90081a371f9050cf6e2aa1651dfa4ee87bc818574" dependencies = [ - "bitflags 2.9.4", - "byteorder", + "bitflags 2.11.0", + "libproc", + "log", "netlink-packet-core 0.7.0", "netlink-packet-sock-diag", "netlink-packet-utils", @@ -1198,24 +1537,15 @@ dependencies = [ "num-derive", "num-traits", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.59.0", ] -[[package]] -name = "newline-converter" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "nex" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b9259ac90d76abdcc6b94ac0c0e4192426aac7f99ae41a0a7dafd7008dd598" +checksum = "d54423ee704c3a4df300c209ef1be5dd42dc17683ae78b920d0863eded6658c4" dependencies = [ "nex-core", "nex-datalink", @@ -1225,9 +1555,9 @@ dependencies = [ [[package]] name = "nex-core" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab4e51a2f7d20a50921aaace9f7989a5a1da737f383f2a0b4156e90f7dbb384" +checksum = "f9e8055dfcf3622e5986181c870d545dadfe195a684b3caf1a67aa2caf5a65f3" dependencies = [ "netdev", "serde", @@ -1235,9 +1565,9 @@ dependencies = [ [[package]] name = "nex-datalink" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8ede3ead0cf82254e0c7591aaf7db2443698d5ded5c5fb322e38c0c5d5e123" +checksum = "938606aa1e6187e3c07ec7db051acf21c32e322e1b29cfa8afe4faadc24eda18" dependencies = [ "bytes", "futures-core", @@ -1251,26 +1581,26 @@ dependencies = [ [[package]] name = "nex-packet" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b48c882ea0e576780c6f48b32f205e9104767347d9da0946750d2e74b84d24" +checksum = "b825e8bea9b5b32c8d00ee8ba32102c674fbb98dd3e6e2d7696fd6975e1519bc" dependencies = [ "bytes", "nex-core", - "rand", + "rand 0.8.5", "serde", ] [[package]] name = "nex-socket" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255e645ceedc1db4b859442a4ad4f21a2ee6830aa7c4819c064154fec1b82bb4" +checksum = "4514c5ae210acf26e1f7fa2c6299f1cc5b20ef63d7156496a86faa617a38ac71" dependencies = [ "libc", "nex-core", "nex-packet", - "nix", + "nix 0.30.1", "socket2 0.5.10", "tokio", "windows-sys 0.59.0", @@ -1278,53 +1608,70 @@ dependencies = [ [[package]] name = "nex-sys" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613fe5446f03f18d8f976ab9e7fe335becd1f766bc83a09c50d33079877b6070" +checksum = "79a9bff7e7d28a00ad4bfa07d2ee23d48071db085cb850eddd56aacac1c58b01" dependencies = [ "libc", "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] name = "ntap" -version = "0.9.1" +version = "0.10.0" dependencies = [ "anyhow", "bytes", "chrono", "clap", "comfy-table", - "crossterm 0.27.0", + "crossterm", "futures", "hickory-resolver", "home", - "indicatif", "inquire", "ipnet", - "ndb-as", - "ndb-ipv4-asn", - "ndb-ipv4-country", - "ndb-ipv6-asn", - "ndb-ipv6-country", "ndb-tcp-service", "ndb-udp-service", "netdev", "netsock", "nex", - "rand", "ratatui", "serde", "serde_json", @@ -1332,7 +1679,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -1346,9 +1693,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -1358,7 +1705,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -1371,12 +1718,65 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.37.3" +name = "num_threads" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ - "memchr", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", ] [[package]] @@ -1384,12 +1784,25 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] [[package]] name = "parking_lot" @@ -1426,6 +1839,101 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1433,22 +1941,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "plist" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1468,24 +1983,49 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.116", +] + [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" -version = "1.0.41" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 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 = "rand" version = "0.8.5" @@ -1493,8 +2033,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1504,7 +2054,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1513,32 +2073,101 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] -name = "rangemap" -version = "1.6.0" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] [[package]] name = "ratatui" -version = "0.25.0" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.9.4", - "cassowary", - "crossterm 0.27.0", + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", "indoc", - "itertools", + "itertools 0.14.0", + "kasuari", "lru", - "paste", - "stability", "strum", + "thiserror 2.0.18", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -1547,28 +2176,80 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -1583,9 +2264,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1593,6 +2274,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1620,20 +2307,31 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -1663,29 +2361,36 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1705,22 +2410,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "stability" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" -dependencies = [ - "quote", - "syn 1.0.109", + "windows-sys 0.60.2", ] [[package]] @@ -1729,6 +2424,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1737,24 +2438,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.25.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -1770,9 +2470,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -1787,35 +2487,83 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] -name = "system-configuration" -version = "0.6.1" +name = "tagptr" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "system-configuration-sys", + "fnv", + "nom", + "phf", + "phf_codegen", ] [[package]] -name = "system-configuration-sys" -version = "0.6.0" +name = "termios" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ - "core-foundation-sys", "libc", ] [[package]] name = "termtree" -version = "0.5.1" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d1330fe7f7f872cd05165130b10602d667b205fd85be09be2814b115d4ced9" + +[[package]] +name = "termwiz" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] [[package]] name = "thiserror" @@ -1828,11 +2576,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -1843,18 +2591,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -1868,30 +2616,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -1899,9 +2649,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1924,26 +2674,36 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", - "mio 1.0.4", + "mio", "pin-project-lite", - "slab", - "socket2 0.6.0", - "windows-sys 0.59.0", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", ] [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1952,20 +2712,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1984,9 +2744,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "chrono", "nu-ansi-term", @@ -1998,11 +2758,23 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2011,10 +2783,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.1.14" +name = "unicode-truncate" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width", +] [[package]] name = "unicode-width" @@ -2023,10 +2800,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "unit-prefix" -version = "0.5.1" +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "unty" @@ -2036,9 +2819,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2058,18 +2841,45 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "atomic", + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "virtue" version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2077,37 +2887,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.104" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2115,34 +2929,130 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.116", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "js-sys", - "wasm-bindgen", + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", ] [[package]] @@ -2194,7 +3104,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -2205,7 +3115,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -2473,19 +3383,116 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.116", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.116", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2493,34 +3500,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] [[package]] @@ -2540,15 +3547,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -2557,9 +3564,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2568,11 +3575,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.116", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 7bab8f8..e9b9d5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ntap" -version = "0.9.1" -edition = "2021" +version = "0.10.0" +edition = "2024" authors = ["shellrow "] description = "Network traffic monitor/analyzer" repository = "https://github.com/shellrow/ntap" @@ -13,52 +13,32 @@ categories = ["network-programming"] license = "MIT" [dependencies] -anyhow = { version = "1" } -bytes = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" +anyhow = { version = "1.0" } +bytes = "1.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = ["time", "chrono"] } -netdev = { version = "0.38", features = ["serde"] } -nex = { version = "0.23", features = ["serde"] } -tokio = { version = "1" } -clap = { version = "4.5", features = ["cargo"] } -crossterm = "0.27" -rand = "0.8" -ratatui = "0.25" -comfy-table = "7.1" -hickory-resolver = { version = "0.24" } +netdev = { version = "0.40", features = ["serde"] } +nex = { version = "0.25", features = ["serde"] } +tokio = { version = "1.49", features = ["rt-multi-thread", "macros", "time", "signal"] } +clap = { version = "4.5", features = ["cargo", "derive"] } +crossterm = { version = "0.29", features = ["event-stream"] } +ratatui = "0.30" +comfy-table = "7.2" +hickory-resolver = { version = "0.25" } futures = {version = "0.3"} -netsock = { version = "0.3", features = ["serde"] } +netsock = { version = "0.6", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } ipnet = { version = "2.11" } home = "0.5" -termtree = "0.5" -indicatif = { version = "0.18" } -inquire = "0.6" -ndb-tcp-service = { version = "0.3", features = ["bundled"] } -ndb-udp-service = { version = "0.3", features = ["bundled"] } -ndb-as = { version = "0.3", features = ["bundled"] } -ndb-ipv4-asn = { version = "0.3", features = ["bundled"] } -ndb-ipv6-asn = { version = "0.3", features = ["bundled"] } -ndb-ipv4-country = { version = "0.3", features = ["bundled"] } -ndb-ipv6-country = { version = "0.3", features = ["bundled"] } +termtree = "1.0" +inquire = "0.9" +ndb-tcp-service = { version = "0.4", features = ["bundled"] } +ndb-udp-service = { version = "0.4", features = ["bundled"] } [target.'cfg(windows)'.dependencies] -winreg = "0.50" - -#[features] -#default = ["bundled"] -#bundled = [ -# "ndb-oui/bundled", -# "ndb-tcp-service/bundled", -# "ndb-udp-service/bundled", -# "ndb-as/bundled", -# "ndb-ipv4-asn/bundled", -# "ndb-ipv6-asn/bundled", -# "ndb-ipv4-country/bundled", -# "ndb-ipv6-country/bundled", -#] +winreg = "0.55" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/README.md b/README.md index 9da7a70..ddb699a 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,38 @@ Network traffic monitor/analyzer, for Linux, macOS, and Windows. ## Overview -**ntap** provides comprehensive insights into your network's activity, enabling users to monitor traffic, manage connections, and view network configurations with ease. - -## Features -- **Network Statistics**: Dive into comprehensive statistics about your network traffic, covering bytes/bandwidth usage, top remote hosts, connections, and processes. -- **Real-time Monitoring**: Monitor network utilization with country and Autonomous System (AS) or Internet Service Provider (ISP) information as it unfolds. -- **Live Packet Capture**: Continuously track the flow of network packets in real-time, offering insights into ongoing traffic. -- **Connection Management**: Quickly and effectively analyze active network connections to optimize performance and security. -- **Interface and Routing Insights**: Obtain detailed views of network interfaces and routing tables to enhance network management and troubleshooting. +**ntap** is a cross-platform network traffic monitor/analyzer focused on: +- Traffic monitoring (`monitor`) +- Packet capture (`live`) ## Usage -See [usage](resources/doc/USAGE.md) +### Commands +- `ntap` or `ntap monitor` : monitor mode +- `ntap live` : live packet capture mode +- `ntap interfaces` : list available interfaces +- `ntap interface` : show default interface + +### Common options +- `-i, --interfaces ` : interface filter +- `-P, --protocols ` : protocol filter +- `-a, --ips ` : host filter +- `-p, --ports ` : port filter +- `-r, --tickrate ` : UI refresh tick (global) + +### Live mode options +- `-l, --limit ` : max packets kept in live table + +### Examples +```sh +# Start monitor mode (default) +ntap + +# Monitor specific interfaces and protocols +ntap monitor -i en0 -P tcp,udp + +# Live capture with a packet list cap +ntap live -i en0 -P tcp -l 200 +``` ## Prerequisites - Ensure you have a compatible operating system (Linux, macOS, Windows). diff --git a/dist-workspace.toml b/dist-workspace.toml index 555e296..4d11844 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,7 +4,7 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.7" +cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app @@ -18,6 +18,6 @@ pr-run-mode = "plan" # Whether to install an updater program install-updater = false # Extra static files to include in each App (path relative to this Cargo.toml's dir) -include = ["resources/doc/USAGE.md"] +include = [] # Path that installers should place binaries in install-path = "CARGO_HOME" diff --git a/resources/doc/USAGE.md b/resources/doc/USAGE.md deleted file mode 100644 index 894f424..0000000 --- a/resources/doc/USAGE.md +++ /dev/null @@ -1,107 +0,0 @@ -# Usage Guide for ntap - -## Overview -`ntap` is a real-time network monitoring tool that provides comprehensive insights into your network activity. -This guide describes how to use `ntap` and its various commands and options. - -## Basic Usage -To run `ntap`, use the following syntax: - -```bash -ntap [OPTIONS] [COMMAND] -``` - -## Default Behavior -By default, if no subcommand is specified, ntap enters the `monitor` mode, which displays live network usage statistics: -```bash -ntap -``` -This default mode captures packets on all available network interfaces and continuously displays live network usage, providing a quick and easy way to monitor current network activity without the need for additional configuration. - -## Commands - -### stat: Continuously displays network statistics, including bytes/bandwidth usage, top remote hosts, connections, and processes. -```bash -ntap stat -``` - -### live: Start live packet capture, continuously display live network packet data. -```bash -ntap live -``` - -### monitor: Enters monitor mode to continuously display live network usage statistics. -```bash -ntap monitor -``` - -### socket: Displays active TCP connections and the ports for TCP and UDP that are listening. -```bash -ntap socket -``` - -### interfaces: Shows all network interfaces. -```bash -ntap interfaces -``` - -### interface: Displays the default network interface. -```bash -ntap interface -``` - -### route: Shows the IP routing table. -```bash -ntap route -``` - -### help: Prints the main help message or help for a specific command. -```bash -ntap help -``` - -For help on a specific command, such as live: -```bash -ntap help live -``` - -## Options -Help (`-h`, `--help`): Prints help information. -```bash -ntap --help -``` - -Subcommand Help (`-h`, `--help`): Prints subcommand's help information. -```bash -ntap live --help -``` - -Tick Rate (`-r`, `--tickrate`): Sets the refresh rate in milliseconds. -```bash -ntap --tickrate 1000 -``` - -Interfaces (`-i`, `--interfaces`): Specifies the interfaces by name. -```bash -ntap -i eth0,eth1 -``` - -Protocols (`-P`, `--protocols`): Filters traffic by protocols. -```bash -ntap -P tcp,udp -``` - -IP Addresses (`-a`, `--ips`): Filters traffic by specific IP addresses. -```bash -ntap --ips 1.1.1.1,8.8.8.8 -``` - -Ports (`-p`, `--ports`): Filters traffic by ports. -```bash -ntap -p 80,443 -``` - -Version (`-V`, `--version`): Displays the version of the ntap. -```bash -ntap --version -``` diff --git a/src/config.rs b/src/config.rs index ff43fa2..1d02674 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,7 @@ impl AppConfig { } #[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] +#[allow(clippy::upper_case_acronyms)] pub enum LogLevel { DEBUG, INFO, @@ -86,15 +87,6 @@ impl LogLevel { LogLevel::ERROR => level == &LogLevel::ERROR, } } - pub fn to_string(&self) -> String { - match self { - LogLevel::DEBUG => "DEBUG", - LogLevel::INFO => "INFO", - LogLevel::WARN => "WARN", - LogLevel::ERROR => "ERROR", - } - .to_owned() - } pub fn to_level_filter(&self) -> tracing::Level { match self { LogLevel::DEBUG => tracing::Level::DEBUG, @@ -105,6 +97,18 @@ impl LogLevel { } } +impl std::fmt::Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + LogLevel::DEBUG => "DEBUG", + LogLevel::INFO => "INFO", + LogLevel::WARN => "WARN", + LogLevel::ERROR => "ERROR", + }; + write!(f, "{s}") + } +} + #[derive(Deserialize, Serialize, Debug)] pub struct LoggingConfig { /// Log level. @@ -117,11 +121,8 @@ impl LoggingConfig { pub fn new() -> LoggingConfig { LoggingConfig { level: LogLevel::INFO, - file_path: if let Some(path) = sys::get_user_file_path(DEFAULT_LOG_FILE_PATH) { - Some(path.to_string_lossy().to_string()) - } else { - None - }, + file_path: sys::get_user_file_path(DEFAULT_LOG_FILE_PATH) + .map(|path| path.to_string_lossy().to_string()), } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 60d9e67..8c302da 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,94 +1,35 @@ use anyhow::Result; -use ndb_as::AsDb; -use ndb_ipv4_asn::Ipv4AsnDb; -use ndb_ipv4_country::Ipv4CountryDb; -use ndb_ipv6_asn::Ipv6AsnDb; -use ndb_ipv6_country::Ipv6CountryDb; use ndb_tcp_service::TcpServiceDb; use ndb_udp_service::UdpServiceDb; use std::sync::{OnceLock, RwLock}; pub static TCP_SERVICE_DB: OnceLock> = OnceLock::new(); pub static UDP_SERVICE_DB: OnceLock> = OnceLock::new(); -pub static AS_DB: OnceLock> = OnceLock::new(); -pub static IPV4_ASN_DB: OnceLock> = OnceLock::new(); -pub static IPV6_ASN_DB: OnceLock> = OnceLock::new(); -pub static IPV4_COUNTRY_DB: OnceLock> = OnceLock::new(); -pub static IPV6_COUNTRY_DB: OnceLock> = OnceLock::new(); -/// Initialize all databases pub fn init_databases() -> Result<()> { tracing::info!("Initializing databases..."); init_tcp_service_db()?; init_udp_service_db()?; - init_as_db()?; - init_ipv4_asn_db()?; - init_ipv6_asn_db()?; - init_ipv4_country_db()?; - init_ipv6_country_db()?; tracing::info!("Databases initialized successfully."); Ok(()) } pub fn init_tcp_service_db() -> Result<()> { - // Initialize TCP Service database let tcp_service_db = TcpServiceDb::bundled(); - TCP_SERVICE_DB - .set(RwLock::new(tcp_service_db)) - .map_err(|_| anyhow::anyhow!("Failed to set TCP_SERVICE_DB in OnceLock"))?; + if TCP_SERVICE_DB.get().is_none() { + TCP_SERVICE_DB + .set(RwLock::new(tcp_service_db)) + .map_err(|_| anyhow::anyhow!("Failed to set TCP_SERVICE_DB in OnceLock"))?; + } Ok(()) } pub fn init_udp_service_db() -> Result<()> { - // Initialize UDP Service database let udp_service_db = UdpServiceDb::bundled(); - UDP_SERVICE_DB - .set(RwLock::new(udp_service_db)) - .map_err(|_| anyhow::anyhow!("Failed to set UDP_SERVICE_DB in OnceLock"))?; - Ok(()) -} - -pub fn init_as_db() -> Result<()> { - // Initialize AS database - let as_db = AsDb::bundled(); - AS_DB - .set(RwLock::new(as_db)) - .map_err(|_| anyhow::anyhow!("Failed to set AS_DB in OnceLock"))?; - Ok(()) -} - -pub fn init_ipv4_asn_db() -> Result<()> { - // Initialize IPv4 ASN database - let ipv4_asn_db = Ipv4AsnDb::bundled(); - IPV4_ASN_DB - .set(RwLock::new(ipv4_asn_db)) - .map_err(|_| anyhow::anyhow!("Failed to set IPV4_ASN_DB in OnceLock"))?; - Ok(()) -} - -pub fn init_ipv6_asn_db() -> Result<()> { - // Initialize IPv6 ASN database - let ipv6_asn_db = Ipv6AsnDb::bundled(); - IPV6_ASN_DB - .set(RwLock::new(ipv6_asn_db)) - .map_err(|_| anyhow::anyhow!("Failed to set IPV6_ASN_DB in OnceLock"))?; - Ok(()) -} - -pub fn init_ipv4_country_db() -> Result<()> { - // Initialize IPv4 Country database - let ipv4_country_db = Ipv4CountryDb::bundled(); - IPV4_COUNTRY_DB - .set(RwLock::new(ipv4_country_db)) - .map_err(|_| anyhow::anyhow!("Failed to set IPV4_COUNTRY_DB in OnceLock"))?; - Ok(()) -} - -pub fn init_ipv6_country_db() -> Result<()> { - // Initialize IPv6 Country database - let ipv6_country_db = Ipv6CountryDb::bundled(); - IPV6_COUNTRY_DB - .set(RwLock::new(ipv6_country_db)) - .map_err(|_| anyhow::anyhow!("Failed to set IPV6_COUNTRY_DB in OnceLock"))?; + if UDP_SERVICE_DB.get().is_none() { + UDP_SERVICE_DB + .set(RwLock::new(udp_service_db)) + .map_err(|_| anyhow::anyhow!("Failed to set UDP_SERVICE_DB in OnceLock"))?; + } Ok(()) } diff --git a/src/handler/interface.rs b/src/handler/interface.rs index 5aac270..9f0d871 100644 --- a/src/handler/interface.rs +++ b/src/handler/interface.rs @@ -1,7 +1,7 @@ use crate::util::tree::node_label; use anyhow::Result; -use netdev::MacAddr; use netdev::Interface; +use netdev::MacAddr; use termtree::Tree; pub fn show_interfaces() -> Result<()> { @@ -52,7 +52,7 @@ pub fn show_interfaces() -> Result<()> { iface_tree.push(gateway_tree); } - if iface.dns_servers.len() > 0 { + if !iface.dns_servers.is_empty() { let mut dns_tree = Tree::new(node_label("DNS Servers", None, None)); for server_addr in &iface.dns_servers { dns_tree.push(node_label(&server_addr.to_string(), None, None)); @@ -114,7 +114,7 @@ pub fn show_default_interface() -> Result<()> { gateway_tree.push(ipv6_tree); tree.push(gateway_tree); } - if iface.dns_servers.len() > 0 { + if !iface.dns_servers.is_empty() { let mut dns_tree = Tree::new(node_label("DNS Servers", None, None)); for server_addr in &iface.dns_servers { dns_tree.push(node_label(&server_addr.to_string(), None, None)); diff --git a/src/handler/live.rs b/src/handler/live.rs index 3a702ba..8c649c1 100644 --- a/src/handler/live.rs +++ b/src/handler/live.rs @@ -1,178 +1,104 @@ use crate::config::AppConfig; use crate::net::packet::{PacketFrame, PacketStorage}; use anyhow::Result; +use nex::packet::ethernet::EtherType; +use nex::packet::ip::IpNextProtocol; use std::collections::HashSet; use std::net::IpAddr; -use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; +use std::sync::mpsc::{Receiver, Sender, channel}; use std::thread; -use clap::ArgMatches; -use nex::packet::ethernet::EtherType; -use nex::packet::ip::IpNextProtocol; +#[derive(Debug, Clone, Default)] +pub struct LiveOptions { + pub interfaces: Vec, + pub protocols: Vec, + pub ips: Vec, + pub ports: Vec, + pub tickrate: Option, + pub enhanced_graphics: bool, + pub limit: Option, +} -pub fn live_capture(app: &ArgMatches) -> Result<()> { - let sub_args = match app.subcommand_matches("live") { - Some(matches) => matches, - None => { - eprintln!("Error: Could not get subcommand matches"); - return Ok(()); - } - }; - // Check .ntap directory - match crate::sys::get_config_dir_path() { - Some(_config_dir) => {} - None => { - eprintln!("Error: Could not get config directory path"); - return Ok(()); - } +pub async fn live_capture(opts: LiveOptions) -> Result<()> { + if crate::sys::get_config_dir_path().is_none() { + anyhow::bail!("Could not get config directory path"); } - // Check dependencies (Currently only for Windows) - match crate::sys::check_deps() { - Ok(_) => {} - Err(e) => { - eprintln!("Error: {:?}", e); - return Ok(()); - } - } + crate::sys::check_deps().map_err(|e| anyhow::anyhow!(e.to_string()))?; - // Load AppConfig let mut config = AppConfig::load(); - - // Initialize logger crate::log::init_logger(&config)?; - if app.contains_id("tickrate") { - config.display.tick_rate = *app.get_one("tickrate").unwrap_or(&1000); + if let Some(tick_rate) = opts.tickrate { + config.display.tick_rate = tick_rate; } - - // Interface filter - if sub_args.contains_id("interfaces") { - match sub_args.get_many::("interfaces") { - Some(interfaces) => { - config.network.interfaces = interfaces.cloned().collect(); - } - None => { - config.network.interfaces = Vec::new(); - } - } + if !opts.interfaces.is_empty() { + config.network.interfaces = opts.interfaces.clone(); } - // Protocol filter let mut ethertypes: HashSet = HashSet::new(); let mut ip_next_protocols: HashSet = HashSet::new(); - if sub_args.contains_id("protocols") { - match sub_args.get_many::("protocols") { - Some(protocols_ref) => { - let protocols: Vec = protocols_ref.cloned().collect(); - for protocol in protocols { - if let Some(ethertype) = crate::net::packet::get_ethertype_from_str(&protocol) { - ethertypes.insert(ethertype); - } - if let Some(ip_next_protocol) = - crate::net::packet::get_ip_next_protocol_from_str(&protocol) - { - ip_next_protocols.insert(ip_next_protocol); - } - } - } - None => {} + for protocol in &opts.protocols { + if let Some(ethertype) = crate::net::packet::get_ethertype_from_str(protocol) { + ethertypes.insert(ethertype); + } + if let Some(ip_next_protocol) = crate::net::packet::get_ip_next_protocol_from_str(protocol) + { + ip_next_protocols.insert(ip_next_protocol); } } - // IP Address filter - let ips: HashSet = match sub_args.get_many::("ips") { - Some(ips_ref) => ips_ref.cloned().collect(), - None => HashSet::new(), - }; + let ips: HashSet = opts.ips.iter().copied().collect(); + let ports: HashSet = opts.ports.iter().copied().collect(); - // Port filter - let ports: HashSet = match sub_args.get_many::("ports") { - Some(ports_ref) => ports_ref.cloned().collect(), - None => HashSet::new(), - }; - - if !ip_next_protocols.is_empty() || ips.len() > 0 || ports.len() > 0 { + if !ip_next_protocols.is_empty() || !ips.is_empty() || !ports.is_empty() { ethertypes.insert(EtherType::Ipv4); ethertypes.insert(EtherType::Ipv6); - if ports.len() > 0 { + if !ports.is_empty() { ip_next_protocols.insert(IpNextProtocol::Tcp); ip_next_protocols.insert(IpNextProtocol::Udp); } } - let storage_capacity: u8; - if sub_args.contains_id("limit") { - storage_capacity = *sub_args.get_one("limit").unwrap_or(&100); - } else { - storage_capacity = u8::MAX; - } - - // Start threads - let mut threads: Vec> = vec![]; + let storage_capacity = opts.limit.unwrap_or(u8::MAX as usize); let packet_strage: Arc = - Arc::new(PacketStorage::with_capacity(storage_capacity as usize)); + Arc::new(PacketStorage::with_capacity(storage_capacity)); let packet_strage_ui: Arc = Arc::clone(&packet_strage); - let target_interfaces: Vec; - if config.network.interfaces.is_empty() { - target_interfaces = crate::net::interface::get_usable_interfaces(); + let target_interfaces = if config.network.interfaces.is_empty() { + crate::net::interface::get_usable_interfaces() } else { - target_interfaces = - crate::net::interface::get_interfaces_by_name(&config.network.interfaces); - } - let mut pcap_thread_index = 0; - let (tx, rx): (Sender, Receiver) = channel(); - let pcap_handlers = target_interfaces - .iter() - .map(|iface| { - let iface = iface.clone(); - let mut pcap_option = crate::net::pcap::PacketCaptureOptions::from_interface(&iface); - pcap_option.ether_types = ethertypes.clone(); - pcap_option.ip_protocols = ip_next_protocols.clone(); - pcap_option.src_ips = ips.clone(); - pcap_option.src_ports = ports.clone(); - pcap_option.dst_ips = ips.clone(); - pcap_option.dst_ports = ports.clone(); - let thread_name = format!("pcap-thread-{}", iface.name.clone()); - let pcap_thread = thread::Builder::new().name(thread_name.clone()); - let tx_clone = tx.clone(); - let pcap_handler = pcap_thread.spawn(move || { - crate::net::pcap::start_live_capture(pcap_option, tx_clone, iface); - }); - tracing::info!("start thread {}", thread_name); - pcap_thread_index += 1; - pcap_handler - }) - .collect::>(); - - let receiver_handler = thread::spawn(move || { - tracing::info!("start mpsc reveiver thread"); - while let Ok(mut frame) = rx.recv() { - frame.capture_no = packet_strage.generate_capture_no(); - packet_strage.add_packet(frame); - } - }); + crate::net::interface::get_interfaces_by_name(&config.network.interfaces) + }; - threads.push(receiver_handler); + let (tx, rx): (Sender, Receiver) = channel(); - for pcap_handler in pcap_handlers { - match pcap_handler { - Ok(handle) => { - threads.push(handle); - } - Err(e) => { - tracing::error!("Error: {:?}", e); - } - } + for iface in target_interfaces { + let iface = iface.clone(); + let mut pcap_option = crate::net::pcap::PacketCaptureOptions::from_interface(&iface); + pcap_option.ether_types = ethertypes.clone(); + pcap_option.ip_protocols = ip_next_protocols.clone(); + pcap_option.src_ips = ips.clone(); + pcap_option.src_ports = ports.clone(); + pcap_option.dst_ips = ips.clone(); + pcap_option.dst_ports = ports.clone(); + let tx_clone = tx.clone(); + thread::Builder::new() + .name(format!("live-pcap-{}", iface.name)) + .spawn(move || { + crate::net::pcap::start_live_capture(pcap_option, tx_clone, iface); + })?; } - tracing::info!("start TUI, live_packet_capture"); + thread::Builder::new() + .name("live-packet-receiver".to_string()) + .spawn(move || { + while let Ok(mut frame) = rx.recv() { + frame.capture_no = packet_strage.generate_capture_no(); + packet_strage.add_packet(frame); + } + })?; - crate::tui::live::terminal::run( - config, - app.contains_id("enhanced-graphics"), - &packet_strage_ui, - )?; + crate::tui::live::terminal::run(config, opts.enhanced_graphics, &packet_strage_ui).await?; Ok(()) } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 723e81b..794b87b 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,35 +1,3 @@ pub mod interface; pub mod live; pub mod monitor; -pub mod route; -pub mod socket; -pub mod stat; -//pub mod update; - -pub enum AppCommands { - Stat, - Live, - Monitor, - Interfaces, - Interface, - Route, - Socket, - //Update, - Default, -} - -impl AppCommands { - pub fn from_str(s: &str) -> AppCommands { - match s { - "stat" => AppCommands::Stat, - "live" => AppCommands::Live, - "monitor" => AppCommands::Monitor, - "interfaces" => AppCommands::Interfaces, - "interface" => AppCommands::Interface, - "route" => AppCommands::Route, - "socket" => AppCommands::Socket, - //"update" => AppCommands::Update, - _ => AppCommands::Default, - } - } -} diff --git a/src/handler/monitor.rs b/src/handler/monitor.rs index 4429f92..168ffd0 100644 --- a/src/handler/monitor.rs +++ b/src/handler/monitor.rs @@ -1,313 +1,109 @@ use crate::config::AppConfig; use crate::net::stat::NetStatStrage; use anyhow::Result; +use nex::packet::ethernet::EtherType; +use nex::packet::ip::IpNextProtocol; use std::collections::HashSet; use std::net::IpAddr; use std::sync::Arc; use std::thread; -use clap::ArgMatches; - -//#[cfg(not(feature = "bundled"))] -//use inquire::Confirm; - -use nex::packet::ethernet::EtherType; -use nex::packet::ip::IpNextProtocol; - -pub fn monitor(app: &ArgMatches) -> Result<()> { - let sub_args = match app.subcommand_matches("monitor") { - Some(matches) => matches, - None => { - eprintln!("Error: Could not get subcommand matches"); - return Ok(()); - } - }; - // Check .ntap directory - match crate::sys::get_config_dir_path() { - Some(_config_dir) => {} - None => { - let err_msg = "Could not get config directory path"; - tracing::error!("{err_msg}"); - anyhow::bail!(err_msg); - } - } +#[derive(Debug, Clone, Default)] +pub struct MonitorOptions { + pub interfaces: Vec, + pub protocols: Vec, + pub ips: Vec, + pub ports: Vec, + pub tickrate: Option, + pub enhanced_graphics: bool, +} - // Check dependencies (Currently only for Windows) - match crate::sys::check_deps() { - Ok(_) => {} - Err(e) => { - tracing::error!("Error: {:?}", e); - anyhow::bail!(e.to_string()); - } +pub async fn monitor(opts: MonitorOptions) -> Result<()> { + if crate::sys::get_config_dir_path().is_none() { + anyhow::bail!("Could not get config directory path"); } - /* // Check Database files - #[cfg(not(feature = "bundled"))] - match crate::deps::check_db_files() { - Ok(_) => {} - Err(e) => { - tracing::error!("{}", e.to_string()); - let ans: bool = - Confirm::new("ntap databases are missing. Do you want to download them now?") - .prompt() - .unwrap_or(false); - if ans { - crate::handler::update::download_db_files()?; - println!("Please restart ntap"); - return Ok(()); - } else { - return Err(e.to_string().into()); - } - } - } */ + crate::sys::check_deps().map_err(|e| anyhow::anyhow!(e.to_string()))?; - // Load AppConfig let mut config = AppConfig::load(); - - // Initialize logger crate::log::init_logger(&config)?; - - // Initialize DB crate::db::init_databases()?; - if app.contains_id("tickrate") { - config.display.tick_rate = *app.get_one("tickrate").unwrap_or(&1000); + if let Some(tick_rate) = opts.tickrate { + config.display.tick_rate = tick_rate; } - - // Interface filter - if sub_args.contains_id("interfaces") { - match sub_args.get_many::("interfaces") { - Some(interfaces) => { - config.network.interfaces = interfaces.cloned().collect(); - } - None => { - config.network.interfaces = Vec::new(); - } - } + if !opts.interfaces.is_empty() { + config.network.interfaces = opts.interfaces.clone(); } - // Protocol filter let mut ethertypes: HashSet = HashSet::new(); let mut ip_next_protocols: HashSet = HashSet::new(); - if sub_args.contains_id("protocols") { - match sub_args.get_many::("protocols") { - Some(protocols_ref) => { - let protocols: Vec = protocols_ref.cloned().collect(); - for protocol in protocols { - if let Some(ethertype) = crate::net::packet::get_ethertype_from_str(&protocol) { - ethertypes.insert(ethertype); - } - if let Some(ip_next_protocol) = - crate::net::packet::get_ip_next_protocol_from_str(&protocol) - { - ip_next_protocols.insert(ip_next_protocol); - } - } - } - None => {} + for protocol in &opts.protocols { + if let Some(ethertype) = crate::net::packet::get_ethertype_from_str(protocol) { + ethertypes.insert(ethertype); + } + if let Some(ip_next_protocol) = crate::net::packet::get_ip_next_protocol_from_str(protocol) + { + ip_next_protocols.insert(ip_next_protocol); } } - // IP Address filter - let ips: HashSet = match sub_args.get_many::("ips") { - Some(ips_ref) => ips_ref.cloned().collect(), - None => HashSet::new(), - }; - - // Port filter - let ports: HashSet = match sub_args.get_many::("ports") { - Some(ports_ref) => ports_ref.cloned().collect(), - None => HashSet::new(), - }; + let ips: HashSet = opts.ips.iter().copied().collect(); + let ports: HashSet = opts.ports.iter().copied().collect(); - if !ip_next_protocols.is_empty() || ips.len() > 0 || ports.len() > 0 { + if !ip_next_protocols.is_empty() || !ips.is_empty() || !ports.is_empty() { ethertypes.insert(EtherType::Ipv4); ethertypes.insert(EtherType::Ipv6); - if ports.len() > 0 { + if !ports.is_empty() { ip_next_protocols.insert(IpNextProtocol::Tcp); ip_next_protocols.insert(IpNextProtocol::Udp); } } - // Start threads - let mut threads: Vec> = vec![]; - let netstat_strage: Arc = Arc::new(NetStatStrage::new()); let mut netstat_strage_socket = Arc::clone(&netstat_strage); let mut netstat_strage_ui = Arc::clone(&netstat_strage); - let target_interfaces: Vec; - if config.network.interfaces.is_empty() { - target_interfaces = crate::net::interface::get_usable_interfaces(); + let target_interfaces = if config.network.interfaces.is_empty() { + crate::net::interface::get_usable_interfaces() } else { - target_interfaces = - crate::net::interface::get_interfaces_by_name(&config.network.interfaces); - } - let mut pcap_thread_index = 0; - let pcap_handlers = target_interfaces - .iter() - .map(|iface| { - let mut netstat_strage_pcap = Arc::clone(&netstat_strage); - let iface = iface.clone(); - let mut pcap_option = crate::net::pcap::PacketCaptureOptions::from_interface(&iface); - pcap_option.ether_types = ethertypes.clone(); - pcap_option.ip_protocols = ip_next_protocols.clone(); - pcap_option.src_ips = ips.clone(); - pcap_option.src_ports = ports.clone(); - pcap_option.dst_ips = ips.clone(); - pcap_option.dst_ports = ports.clone(); - let thread_name = format!("pcap-thread-{}", iface.name.clone()); - let pcap_thread = thread::Builder::new().name(thread_name.clone()); - let pcap_handler = pcap_thread.spawn(move || { - crate::net::pcap::start_background_capture( - pcap_option, - &mut netstat_strage_pcap, - iface, - ); - }); - tracing::info!("start thread {}", thread_name); - pcap_thread_index += 1; - pcap_handler - }) - .collect::>(); - - let socket_handler = thread::spawn(move || { - tracing::info!("start thread socket_info_update"); - crate::net::socket::start_socket_info_update(&mut netstat_strage_socket); - }); - - for pcap_handler in pcap_handlers { - match pcap_handler { - Ok(handle) => { - threads.push(handle); - } - Err(e) => { - tracing::error!("Error: {:?}", e); - } - } - } - threads.push(socket_handler); - - if config.network.reverse_dns { - let mut netstat_strage_dns = Arc::clone(&netstat_strage); - let dns_handler = thread::spawn(move || { - tracing::info!("start thread dns_map_update"); - crate::net::dns::start_dns_map_update(&mut netstat_strage_dns); - }); - threads.push(dns_handler); - } - - tracing::info!("start TUI, netstat_data_update"); - - crate::tui::monitor::terminal::run( - config, - app.contains_id("enhanced-graphics"), - &mut netstat_strage_ui, - )?; - Ok(()) -} - -pub fn monitor_default(app: &ArgMatches) -> Result<()> { - // Check .ntap directory - match crate::sys::get_config_dir_path() { - Some(_config_dir) => {} - None => { - let err_msg = "Could not get config directory path"; - tracing::error!("{err_msg}"); - anyhow::bail!(err_msg); - } - } - - // Check dependencies (Currently only for Windows) - match crate::sys::check_deps() { - Ok(_) => {} - Err(e) => { - tracing::error!("Error: {:?}", e); - anyhow::bail!(e.to_string()); - } - } - - // Load AppConfig - let mut config = AppConfig::load(); - - // Initialize logger - crate::log::init_logger(&config)?; - - // Initialize DB - crate::db::init_databases()?; - - if app.contains_id("tickrate") { - config.display.tick_rate = *app.get_one("tickrate").unwrap_or(&1000); - } - - // Start threads - let mut threads: Vec> = vec![]; - - let netstat_strage: Arc = Arc::new(NetStatStrage::new()); - let mut netstat_strage_socket = Arc::clone(&netstat_strage); - let mut netstat_strage_ui = Arc::clone(&netstat_strage); + crate::net::interface::get_interfaces_by_name(&config.network.interfaces) + }; - let target_interfaces: Vec; - if config.network.interfaces.is_empty() { - target_interfaces = crate::net::interface::get_usable_interfaces(); - } else { - target_interfaces = - crate::net::interface::get_interfaces_by_name(&config.network.interfaces); - } - let mut pcap_thread_index = 0; - let pcap_handlers = target_interfaces - .iter() - .map(|iface| { - let mut netstat_strage_pcap = Arc::clone(&netstat_strage); - let iface = iface.clone(); - let pcap_option = crate::net::pcap::PacketCaptureOptions::from_interface(&iface); - let thread_name = format!("pcap-thread-{}", iface.name.clone()); - let pcap_thread = thread::Builder::new().name(thread_name.clone()); - let pcap_handler = pcap_thread.spawn(move || { + for iface in target_interfaces { + let mut netstat_strage_pcap = Arc::clone(&netstat_strage); + let mut pcap_option = crate::net::pcap::PacketCaptureOptions::from_interface(&iface); + pcap_option.ether_types = ethertypes.clone(); + pcap_option.ip_protocols = ip_next_protocols.clone(); + pcap_option.src_ips = ips.clone(); + pcap_option.src_ports = ports.clone(); + pcap_option.dst_ips = ips.clone(); + pcap_option.dst_ports = ports.clone(); + thread::Builder::new() + .name(format!("pcap-thread-{}", iface.name)) + .spawn(move || { crate::net::pcap::start_background_capture( pcap_option, &mut netstat_strage_pcap, iface, ); - }); - tracing::info!("start thread {}", thread_name); - pcap_thread_index += 1; - pcap_handler - }) - .collect::>(); - - let socket_handler = thread::spawn(move || { - tracing::info!("start thread socket_info_update"); - crate::net::socket::start_socket_info_update(&mut netstat_strage_socket); - }); - - for pcap_handler in pcap_handlers { - match pcap_handler { - Ok(handle) => { - threads.push(handle); - } - Err(e) => { - tracing::error!("Error: {:?}", e); - } - } + })?; } - threads.push(socket_handler); - if config.network.reverse_dns { - let mut netstat_strage_dns = Arc::clone(&netstat_strage); - let dns_handler = thread::spawn(move || { - tracing::info!("start thread dns_map_update"); - crate::net::dns::start_dns_map_update(&mut netstat_strage_dns); - }); - threads.push(dns_handler); - } + thread::Builder::new() + .name("socket-info-update".to_string()) + .spawn(move || { + crate::net::socket::start_socket_info_update(&mut netstat_strage_socket); + })?; - tracing::info!("start TUI, netstat_data_update"); + let mut netstat_strage_dns = Arc::clone(&netstat_strage); + thread::Builder::new() + .name("dns-map-update".to_string()) + .spawn(move || { + crate::net::dns::start_dns_map_update(&mut netstat_strage_dns); + })?; - crate::tui::monitor::terminal::run( - config, - app.contains_id("enhanced-graphics"), - &mut netstat_strage_ui, - )?; + crate::tui::monitor::terminal::run(config, opts.enhanced_graphics, &mut netstat_strage_ui) + .await?; Ok(()) } diff --git a/src/handler/route.rs b/src/handler/route.rs deleted file mode 100644 index 02f8a78..0000000 --- a/src/handler/route.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::net::{Ipv4Addr, Ipv6Addr}; - -use anyhow::Result; -use comfy_table::presets::NOTHING; -use comfy_table::*; -use netdev::interface::types::InterfaceType; - -pub fn show_routes() -> Result<()> { - let interfaces = netdev::get_interfaces(); - - // IPv4 routing table - println!("IPv4 Routing Table"); - let mut table = Table::new(); - table - .load_preset(NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![ - "Interface", - "Source", - "Destination", - "Netmask", - "Gateway", - ]); - for iface in interfaces.clone() { - if iface.ipv4.len() == 0 { - continue; - } - if let Some(gateway) = iface.gateway { - for ipv4 in &iface.ipv4 { - let ipv4_gateway = if let Some(gw) = gateway.ipv4.first() { - gw.to_string() - } else { - String::new() - }; - if iface.default { - table.add_row(vec![ - Cell::new(format!("{} (default)", &iface.name)), - Cell::new(&ipv4.addr()), - Cell::new(Ipv4Addr::UNSPECIFIED), - Cell::new(&ipv4.netmask()), - Cell::new(ipv4_gateway), - ]); - } else { - table.add_row(vec![ - Cell::new(&iface.name), - Cell::new(&ipv4.addr()), - Cell::new(&ipv4.network()), - Cell::new(&ipv4.netmask()), - Cell::new(ipv4_gateway), - ]); - } - } - } else { - if iface.if_type == InterfaceType::Loopback - || iface.ipv4[0].addr() == Ipv4Addr::LOCALHOST - { - table.add_row(vec![ - Cell::new(iface.name), - Cell::new(&iface.ipv4[0].addr()), - Cell::new(&iface.ipv4[0].network()), - Cell::new(&iface.ipv4[0].netmask()), - Cell::new(""), - ]); - } - } - } - println!("{table}"); - - // IPv6 routing table - println!("IPv6 Routing Table"); - let mut table = Table::new(); - table - .load_preset(NOTHING) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_header(vec![ - "Interface", - "Source", - "Destination", - "Netmask", - "Gateway", - ]); - for iface in interfaces { - if iface.ipv6.len() == 0 { - continue; - } - if let Some(gateway) = iface.gateway { - for ipv6 in &iface.ipv6 { - let ipv6_gateway = if let Some(gw) = gateway.ipv6.first() { - gw.to_string() - } else { - String::new() - }; - if iface.default { - table.add_row(vec![ - Cell::new(format!("{} (default)", &iface.name)), - Cell::new(&ipv6.addr()), - Cell::new(Ipv6Addr::UNSPECIFIED), - Cell::new(&ipv6.netmask()), - Cell::new(ipv6_gateway), - ]); - } else { - table.add_row(vec![ - Cell::new(&iface.name), - Cell::new(&ipv6.addr()), - Cell::new(&ipv6.network()), - Cell::new(&ipv6.netmask()), - Cell::new(ipv6_gateway), - ]); - } - } - } else { - if iface.if_type == InterfaceType::Loopback - || iface.ipv6[0].addr() == Ipv6Addr::LOCALHOST - { - table.add_row(vec![ - Cell::new(iface.name), - Cell::new(&iface.ipv6[0].addr()), - Cell::new(&iface.ipv6[0].network()), - Cell::new(&iface.ipv6[0].netmask()), - Cell::new(""), - ]); - } - } - } - println!("{table}"); - Ok(()) -} diff --git a/src/handler/socket.rs b/src/handler/socket.rs index 63d1746..3fdceb4 100644 --- a/src/handler/socket.rs +++ b/src/handler/socket.rs @@ -76,14 +76,11 @@ pub fn show_socket_info(app: &ArgMatches) -> Result<()> { table.add_row(vec![ Cell::new(&socket.protocol.as_str()), Cell::new(format!("{}:{}", socket.local_ip_addr, socket.local_port)), - if socket.remote_ip_addr.is_none() { - Cell::new("") + if let (Some(remote_ip), Some(remote_port)) = (socket.remote_ip_addr, socket.remote_port) + { + Cell::new(format!("{}:{}", remote_ip, remote_port)) } else { - Cell::new(format!( - "{}:{}", - socket.remote_ip_addr.unwrap(), - socket.remote_port.unwrap() - )) + Cell::new("") }, if let Some(process) = &socket.process { Cell::new(&process.pid.to_string()) diff --git a/src/handler/update.rs b/src/handler/update.rs index 21a704f..4492bc7 100644 --- a/src/handler/update.rs +++ b/src/handler/update.rs @@ -56,9 +56,12 @@ pub fn download_db_files() -> Result<()> { .with_target(false) .with_timer(ChronoLocal::rfc_3339()) .finish(); - tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + if let Err(e) = tracing::subscriber::set_global_default(subscriber) { + eprintln!("logger already initialized or unavailable: {}", e); + } - let database_dir = crate::sys::get_database_dir_path().unwrap(); + let database_dir = crate::sys::get_database_dir_path() + .ok_or_else(|| anyhow::anyhow!("failed to resolve database directory path"))?; // OUI match download_file( crate::db::oui::OUI_R2_URL, diff --git a/src/log.rs b/src/log.rs index da58478..93f49bb 100644 --- a/src/log.rs +++ b/src/log.rs @@ -2,10 +2,10 @@ use anyhow::Result; use std::fs::File; use std::path::Path; use tracing::Level; +use tracing_subscriber::FmtSubscriber; use tracing_subscriber::fmt::time::ChronoLocal; use tracing_subscriber::fmt::writer::BoxMakeWriter; use tracing_subscriber::fmt::writer::MakeWriterExt; -use tracing_subscriber::FmtSubscriber; pub fn init_logger(config: &crate::config::AppConfig) -> Result<()> { // Init logger @@ -13,7 +13,8 @@ pub fn init_logger(config: &crate::config::AppConfig) -> Result<()> { // Convert to PathBuf Path::new(&file_path).to_path_buf() } else { - crate::sys::get_user_file_path(crate::config::DEFAULT_LOG_FILE_PATH).unwrap() + crate::sys::get_user_file_path(crate::config::DEFAULT_LOG_FILE_PATH) + .ok_or_else(|| anyhow::anyhow!("failed to resolve default log file path"))? }; let log_file: File = if log_file_path.exists() { File::options().write(true).open(&log_file_path)? @@ -33,8 +34,9 @@ pub fn init_logger(config: &crate::config::AppConfig) -> Result<()> { .with_timer(ChronoLocal::rfc_3339()) .with_writer(writer) .finish(); - tracing::subscriber::set_global_default(subscriber) - .expect("setting default subscriber failed"); + if let Err(e) = tracing::subscriber::set_global_default(subscriber) { + eprintln!("logger already initialized or unavailable: {}", e); + } } else { // In release mode, log only to the error log file let error_writer = error_log.with_max_level(Level::ERROR); @@ -45,8 +47,9 @@ pub fn init_logger(config: &crate::config::AppConfig) -> Result<()> { .with_timer(ChronoLocal::rfc_3339()) .with_writer(error_writer) .finish(); - tracing::subscriber::set_global_default(subscriber) - .expect("setting default subscriber failed"); + if let Err(e) = tracing::subscriber::set_global_default(subscriber) { + eprintln!("logger already initialized or unavailable: {}", e); + } } Ok(()) diff --git a/src/main.rs b/src/main.rs index 1ff71c0..a3dc02d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,241 +10,171 @@ mod tui; mod util; use anyhow::Result; -use clap::{crate_description, crate_name, crate_version, value_parser}; -use clap::{Arg, ArgMatches, Command}; -use handler::AppCommands; +use clap::{ArgAction, Parser, Subcommand}; use std::net::IpAddr; -fn main() -> Result<()> { - // Parse command line arguments - let args: ArgMatches = parse_args(); - let subcommand_name = args.subcommand_name().unwrap_or(""); - let app_command = AppCommands::from_str(subcommand_name); +#[derive(Debug, Parser)] +#[command( + name = clap::crate_name!(), + version = clap::crate_version!(), + about = clap::crate_description!() +)] +struct Cli { + #[arg( + short = 'i', + long = "interfaces", + value_delimiter = ',', + value_name = "interfaces" + )] + interfaces: Vec, + #[arg( + short = 'P', + long = "protocols", + value_delimiter = ',', + value_name = "protocols" + )] + protocols: Vec, + #[arg(short = 'a', long = "ips", value_delimiter = ',', value_name = "ips")] + ips: Vec, + #[arg( + short = 'p', + long = "ports", + value_delimiter = ',', + value_name = "ports" + )] + ports: Vec, + #[arg(short = 'r', long = "tickrate", value_name = "duration_ms")] + tickrate: Option, + #[arg(long = "enhanced-graphics", action = ArgAction::SetTrue)] + enhanced_graphics: bool, + #[command(subcommand)] + command: Option, +} - match app_command { - AppCommands::Stat => handler::stat::show_stat(&args), - AppCommands::Live => handler::live::live_capture(&args), - AppCommands::Monitor => handler::monitor::monitor(&args), - AppCommands::Interfaces => handler::interface::show_interfaces(), - AppCommands::Interface => handler::interface::show_default_interface(), - AppCommands::Route => handler::route::show_routes(), - AppCommands::Socket => handler::socket::show_socket_info(&args), - AppCommands::Default => { - // If no subcommand is specified, enter monitor mode by default - handler::monitor::monitor_default(&args) - } +#[derive(Debug, Subcommand)] +enum Command { + Monitor(MonitorArgs), + Live(LiveArgs), + Interfaces, + Interface, +} + +#[derive(Debug, Parser, Default)] +struct MonitorArgs { + #[arg( + short = 'i', + long = "interfaces", + value_delimiter = ',', + value_name = "interfaces" + )] + interfaces: Vec, + #[arg( + short = 'P', + long = "protocols", + value_delimiter = ',', + value_name = "protocols" + )] + protocols: Vec, + #[arg(short = 'a', long = "ips", value_delimiter = ',', value_name = "ips")] + ips: Vec, + #[arg( + short = 'p', + long = "ports", + value_delimiter = ',', + value_name = "ports" + )] + ports: Vec, +} + +#[derive(Debug, Parser, Default)] +struct LiveArgs { + #[arg( + short = 'i', + long = "interfaces", + value_delimiter = ',', + value_name = "interfaces" + )] + interfaces: Vec, + #[arg( + short = 'P', + long = "protocols", + value_delimiter = ',', + value_name = "protocols" + )] + protocols: Vec, + #[arg(short = 'a', long = "ips", value_delimiter = ',', value_name = "ips")] + ips: Vec, + #[arg( + short = 'p', + long = "ports", + value_delimiter = ',', + value_name = "ports" + )] + ports: Vec, + #[arg(short = 'l', long = "limit", value_name = "count")] + limit: Option, +} + +fn to_monitor_options(cli: &Cli, cmd: Option<&MonitorArgs>) -> handler::monitor::MonitorOptions { + let interfaces = cmd + .map(|c| c.interfaces.clone()) + .unwrap_or_else(|| cli.interfaces.clone()); + let protocols = cmd + .map(|c| c.protocols.clone()) + .unwrap_or_else(|| cli.protocols.clone()); + let ips = cmd + .map(|c| c.ips.clone()) + .unwrap_or_else(|| cli.ips.clone()); + let ports = cmd + .map(|c| c.ports.clone()) + .unwrap_or_else(|| cli.ports.clone()); + handler::monitor::MonitorOptions { + interfaces, + protocols, + ips, + ports, + tickrate: cli.tickrate, + enhanced_graphics: cli.enhanced_graphics, + } +} + +fn to_live_options(cli: &Cli, cmd: Option<&LiveArgs>) -> handler::live::LiveOptions { + let interfaces = cmd + .map(|c| c.interfaces.clone()) + .unwrap_or_else(|| cli.interfaces.clone()); + let protocols = cmd + .map(|c| c.protocols.clone()) + .unwrap_or_else(|| cli.protocols.clone()); + let ips = cmd + .map(|c| c.ips.clone()) + .unwrap_or_else(|| cli.ips.clone()); + let ports = cmd + .map(|c| c.ports.clone()) + .unwrap_or_else(|| cli.ports.clone()); + let limit = cmd.and_then(|c| c.limit); + handler::live::LiveOptions { + interfaces, + protocols, + ips, + ports, + tickrate: cli.tickrate, + enhanced_graphics: cli.enhanced_graphics, + limit, } } -fn parse_args() -> ArgMatches { - let app: Command = Command::new(crate_name!()) - .version(crate_version!()) - .about(crate_description!()) - .after_help("By default, if no options are specified, ntap enters the monitor mode.") - .arg( - Arg::new("interfaces") - .help("Specify the interfaces by name. Example: ntap -i eth0,eth1") - .short('i') - .long("interfaces") - .value_name("interfaces") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("protocols") - .help("Specify protocols. Example: ntap -P tcp,udp") - .short('P') - .long("protocols") - .value_name("protocols") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("ips") - .help("Specify IP addresses. Example: ntap -a 1.1.1.1,8.8.8.8") - .short('a') - .long("ips") - .value_name("ips") - .value_delimiter(',') - .value_parser(value_parser!(IpAddr)) - ) - .arg( - Arg::new("ports") - .help("Specify ports. Example: ntap -p 80,443") - .short('p') - .long("ports") - .value_name("ports") - .value_delimiter(',') - .value_parser(value_parser!(u16)) - ) - .arg( - Arg::new("tickrate") - .help("Time in milliseconds between refreshes") - .short('r') - .long("tickrate") - .value_name("duration_ms") - .value_parser(value_parser!(u64)), - ) - .arg( - Arg::new("enhanced-graphics") - .help("Whether unicode symbols are used to improve the overall look of the app") - .long("enhanced-graphics") - .num_args(0), - ) - // Sub-command for stat mode. This is the default mode of ntap - .subcommand(Command::new("stat") - .about("Continuously displays network statistics, covering bytes/bandwidth usage, top remote hosts, connections, and processes.") - .arg( - Arg::new("interfaces") - .help("Specify the interfaces by name. Example: ntap stat -i eth0,eth1") - .short('i') - .long("interfaces") - .value_name("interfaces") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("protocols") - .help("Specify protocols. Example: ntap stat -P tcp,udp") - .short('P') - .long("protocols") - .value_name("protocols") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("ips") - .help("Specify IP addresses. Example: ntap stat -a 1.1.1.1,8.8.8.8") - .short('a') - .long("ips") - .value_name("ips") - .value_delimiter(',') - .value_parser(value_parser!(IpAddr)) - ) - .arg( - Arg::new("ports") - .help("Specify ports. Example: ntap stat -p 80,443") - .short('p') - .long("ports") - .value_name("ports") - .value_delimiter(',') - .value_parser(value_parser!(u16)) - ) - ) - // Sub-command for live mode. - .subcommand(Command::new("live") - .about("Start live packet capture. Live mode captures and displays live network packets.") - .arg( - Arg::new("limit") - .help("Limit the number of packets to display") - .short('l') - .long("limit") - .value_name("count") - .value_parser(value_parser!(u8)), - ) - .arg( - Arg::new("interfaces") - .help("Specify the interfaces by name. Example: ntap live -i eth0,eth1") - .short('i') - .long("interfaces") - .value_name("interfaces") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("protocols") - .help("Specify protocols. Example: ntap live -P tcp,udp") - .short('P') - .long("protocols") - .value_name("protocols") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("ips") - .help("Specify IP addresses. Example: ntap live -a 1.1.1.1,8.8.8.8") - .short('a') - .long("ips") - .value_name("ips") - .value_delimiter(',') - .value_parser(value_parser!(IpAddr)) - ) - .arg( - Arg::new("ports") - .help("Specify ports. Example: ntap live -p 80,443") - .short('p') - .long("ports") - .value_name("ports") - .value_delimiter(',') - .value_parser(value_parser!(u16)) - ) - ) - // Sub-command for monitor mode. - .subcommand(Command::new("monitor") - .about("Enter monitor mode. Monitor mode continuously displays live network statistics.") - .arg( - Arg::new("interfaces") - .help("Specify the interfaces by name. Example: ntap monitor -i eth0,eth1") - .short('i') - .long("interfaces") - .value_name("interfaces") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("protocols") - .help("Specify protocols. Example: ntap monitor -P tcp,udp") - .short('P') - .long("protocols") - .value_name("protocols") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ) - .arg( - Arg::new("ips") - .help("Specify IP addresses. Example: ntap monitor -a 1.1.1.1,8.8.8.8") - .short('a') - .long("ips") - .value_name("ips") - .value_delimiter(',') - .value_parser(value_parser!(IpAddr)) - ) - .arg( - Arg::new("ports") - .help("Specify ports. Example: ntap monitor -p 80,443") - .short('p') - .long("ports") - .value_name("ports") - .value_delimiter(',') - .value_parser(value_parser!(u16)) - ) - ) - // Sub-command for show active TCP connections and the TCP and UDP ports on which is listening - .subcommand( - Command::new("socket") - .about("Show active TCP connections and the TCP and UDP ports on which is listening. ntap socket --help for more information") - .arg( - Arg::new("protocols") - .help("Specify protocols. The protocol can be ipv4, ipv6, tcp, udp. Example: ntap socket -P tcp") - .short('P') - .long("protocols") - .value_name("protocols") - .value_delimiter(',') - .value_parser(value_parser!(String)) - ), - ) - // Sub-command for show network interfaces - .subcommand(Command::new("interfaces") - .about("Show network interfaces") - ) - // Sub-command for show default network interface - .subcommand(Command::new("interface") - .about("Show default network interface") - ) - // Sub-command for show IP routing table - .subcommand(Command::new("route") - .about("Show IP routing table") - ) - ; - app.get_matches() +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<()> { + let cli = Cli::parse(); + match &cli.command { + Some(Command::Monitor(args)) => { + handler::monitor::monitor(to_monitor_options(&cli, Some(args))).await + } + Some(Command::Live(args)) => { + handler::live::live_capture(to_live_options(&cli, Some(args))).await + } + Some(Command::Interfaces) => handler::interface::show_interfaces(), + Some(Command::Interface) => handler::interface::show_default_interface(), + None => handler::monitor::monitor(to_monitor_options(&cli, None)).await, + } } diff --git a/src/net/dns.rs b/src/net/dns.rs index cf23110..d9ea6d4 100644 --- a/src/net/dns.rs +++ b/src/net/dns.rs @@ -1,38 +1,48 @@ +use std::collections::HashMap; use std::net::IpAddr; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -#[cfg(not(any(unix, target_os = "windows")))] -use hickory_resolver::config::{ResolverConfig, ResolverOpts}; -use hickory_resolver::Resolver; - use futures::stream::{self, StreamExt}; - -use hickory_resolver::AsyncResolver; -use std::collections::HashMap; -use std::str::FromStr; -use std::thread; +use hickory_resolver::Resolver; +use hickory_resolver::config::ResolverConfig; +use hickory_resolver::name_server::TokioConnectionProvider; use crate::net::stat::NetStatStrage; +fn create_resolver(timeout: Option) -> Option> { + #[cfg(any(unix, target_os = "windows"))] + { + let mut builder = Resolver::builder_tokio().ok()?; + if let Some(timeout) = timeout { + builder.options_mut().timeout = timeout; + } + Some(builder.build()) + } + #[cfg(not(any(unix, target_os = "windows")))] + { + let mut builder = Resolver::builder_with_config( + ResolverConfig::default(), + TokioConnectionProvider::default(), + ); + if let Some(timeout) = timeout { + builder.options_mut().timeout = timeout; + } + Some(builder.build()) + } +} + pub fn lookup_host_name(host_name: String) -> Option { let ip_vec: Vec = resolve_domain(host_name); let mut ipv6_vec: Vec = vec![]; for ip in ip_vec { match ip { - IpAddr::V4(_) => { - return Some(ip); - } - IpAddr::V6(_) => { - ipv6_vec.push(ip); - } + IpAddr::V4(_) => return Some(ip), + IpAddr::V6(_) => ipv6_vec.push(ip), } } - if ipv6_vec.len() > 0 { - return Some(ipv6_vec[0]); - } else { - None - } + ipv6_vec.first().copied() } pub async fn lookup_host_name_async(host_name: String) -> Option { @@ -40,222 +50,72 @@ pub async fn lookup_host_name_async(host_name: String) -> Option { let mut ipv6_vec: Vec = vec![]; for ip in ip_vec { match ip { - IpAddr::V4(_) => { - return Some(ip); - } - IpAddr::V6(_) => { - ipv6_vec.push(ip); - } + IpAddr::V4(_) => return Some(ip), + IpAddr::V6(_) => ipv6_vec.push(ip), } } - if ipv6_vec.len() > 0 { - return Some(ipv6_vec[0]); - } else { - None - } + ipv6_vec.first().copied() } pub fn lookup_ip_addr(ip_addr: IpAddr) -> Option { - let names: Vec = resolve_ip(ip_addr); - if names.len() > 0 { - return Some(names[0].clone()); - } else { - return None; - } + resolve_ip(ip_addr).first().cloned() } pub async fn lookup_ip_addr_async(ip_addr: String) -> String { - let ips: Vec = resolve_ip_async(ip_addr).await; - if ips.len() > 0 { - return ips[0].clone(); - } else { - return String::new(); - } -} - -#[cfg(any(unix, target_os = "windows"))] -fn resolve_domain(host_name: String) -> Vec { - let mut ips: Vec = vec![]; - let resolver = Resolver::from_system_conf().unwrap(); - match resolver.lookup_ip(host_name) { - Ok(lip) => { - for ip in lip.iter() { - ips.push(ip); - } - } - Err(_) => {} - } - ips + resolve_ip_async(ip_addr) + .await + .first() + .cloned() + .unwrap_or_default() } -#[cfg(not(any(unix, target_os = "windows")))] fn resolve_domain(host_name: String) -> Vec { - let mut ips: Vec = vec![]; - let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap(); - match resolver.lookup_ip(host_name) { - Ok(lip) => { - for ip in lip.iter() { - ips.push(ip); - } - } - Err(_) => {} - } - ips -} - -#[cfg(any(unix, target_os = "windows"))] -fn resolve_ip(ip_addr: IpAddr) -> Vec { - let mut names: Vec = vec![]; - let mut system_conf = hickory_resolver::system_conf::read_system_conf().unwrap(); - if crate::net::ip::is_global_addr(ip_addr) { - system_conf.1.timeout = Duration::from_millis(1000); - } else { - system_conf.1.timeout = Duration::from_millis(200); - } - let resolver = Resolver::new(system_conf.0, system_conf.1).unwrap(); - match resolver.reverse_lookup(ip_addr) { - Ok(rlookup) => { - for record in rlookup.as_lookup().record_iter() { - match record.data() { - Some(data) => { - let name = data.to_string(); - if name.ends_with(".") { - names.push(name[0..name.len() - 1].to_string()); - } else { - names.push(name); - } - } - None => {} - } - } - names - } - Err(_) => { - return names; - } - } + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return vec![], + }; + rt.block_on(resolve_domain_async(host_name)) } -#[cfg(not(any(unix, target_os = "windows")))] fn resolve_ip(ip_addr: IpAddr) -> Vec { - let mut names: Vec = vec![]; - let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default()).unwrap(); - match resolver.reverse_lookup(ip_addr) { - Ok(rlookup) => { - for record in rlookup.as_lookup().record_iter() { - match record.data() { - Some(data) => { - let name = data.to_string(); - if name.ends_with(".") { - names.push(name[0..name.len() - 1].to_string()); - } else { - names.push(name); - } - } - None => {} - } - } - names - } - Err(_) => { - return names; - } - } + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return vec![], + }; + rt.block_on(resolve_ip_async(ip_addr.to_string())) } -#[cfg(any(unix, target_os = "windows"))] async fn resolve_domain_async(host_name: String) -> Vec { - let mut ips: Vec = vec![]; - let resolver = AsyncResolver::tokio_from_system_conf().unwrap(); + let resolver = match create_resolver(None) { + Some(resolver) => resolver, + None => return vec![], + }; match resolver.lookup_ip(host_name).await { - Ok(lip) => { - for ip in lip.iter() { - ips.push(ip); - } - } - Err(_) => {} + Ok(lookup) => lookup.iter().collect(), + Err(_) => vec![], } - ips } -#[cfg(not(any(unix, target_os = "windows")))] -async fn resolve_domain_async(host_name: String) -> Vec { - let mut ips: Vec = vec![]; - let resolver = - AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()).unwrap(); - match resolver.lookup_ip(host_name).await { - Ok(lip) => { - for ip in lip.iter() { - ips.push(ip); - } - } - Err(_) => {} - } - ips -} - -#[cfg(any(unix, target_os = "windows"))] async fn resolve_ip_async(ip_addr: String) -> Vec { - let ip_addr: IpAddr = IpAddr::from_str(ip_addr.as_str()).unwrap(); - let mut names: Vec = vec![]; - let mut system_conf = hickory_resolver::system_conf::read_system_conf().unwrap(); - if crate::net::ip::is_global_addr(ip_addr) { - system_conf.1.timeout = Duration::from_millis(1000); + let ip_addr = match IpAddr::from_str(&ip_addr) { + Ok(ip_addr) => ip_addr, + Err(_) => return vec![], + }; + let timeout = if crate::net::ip::is_global_addr(ip_addr) { + Duration::from_millis(1000) } else { - system_conf.1.timeout = Duration::from_millis(200); - } - let resolver = AsyncResolver::tokio(system_conf.0, system_conf.1); + Duration::from_millis(200) + }; + let resolver = match create_resolver(Some(timeout)) { + Some(resolver) => resolver, + None => return vec![], + }; match resolver.reverse_lookup(ip_addr).await { - Ok(rlookup) => { - for record in rlookup.as_lookup().record_iter() { - match record.data() { - Some(data) => { - let name = data.to_string(); - if name.ends_with(".") { - names.push(name[0..name.len() - 1].to_string()); - } else { - names.push(name); - } - } - None => {} - } - } - names - } - Err(_) => { - return names; - } - } -} - -#[cfg(not(any(unix, target_os = "windows")))] -async fn resolve_ip_async(ip_addr: String) -> Vec { - let mut names: Vec = vec![]; - let resolver = - AsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default()).unwrap(); - match resolver - .reverse_lookup(IpAddr::from_str(ip_addr.as_str()).unwrap()) - .await - { - Ok(rlookup) => { - for record in rlookup.as_lookup().record_iter() { - match record.data() { - Some(data) => { - let name = data.to_string(); - if name.ends_with(".") { - names.push(name[0..name.len() - 1].to_string()); - } else { - names.push(name); - } - } - None => {} - } - } - names - } - Err(_) => { - return names; - } + Ok(lookup) => lookup + .iter() + .map(|name| name.to_string().trim_end_matches('.').to_string()) + .collect(), + Err(_) => vec![], } } @@ -267,19 +127,22 @@ pub async fn lookup_ips_async(ips: Vec) -> HashMap { }) .buffer_unordered(10); let mut results: HashMap = HashMap::new(); - while let Some(result) = tasks.next().await { - results.insert( - result.0, - result.1.first().unwrap_or(&String::new()).to_string(), - ); + while let Some((ip, names)) = tasks.next().await { + if let Some(name) = names.first() { + if !name.is_empty() { + results.insert(ip, name.clone()); + } + } } results } pub fn lookup_ips(ips: Vec) -> HashMap { - let rt: tokio::runtime::Runtime = tokio::runtime::Runtime::new().unwrap(); - let handle = thread::spawn(move || rt.block_on(async { lookup_ips_async(ips).await })); - handle.join().unwrap() + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return HashMap::new(), + }; + rt.block_on(lookup_ips_async(ips)) } pub fn lookup_host(host: String) -> Vec { @@ -293,55 +156,61 @@ pub fn lookup_addr(addr: IpAddr) -> Vec { pub fn start_dns_map_update(netstat_strage: &mut Arc) { loop { let mut lookup_target_ips: Vec = vec![]; - // Lock the remote_hosts let remote_hosts_inner = match netstat_strage.remote_hosts.try_lock() { Ok(remote_hosts) => remote_hosts, Err(e) => { tracing::error!("[dns_map_update] lock error: {}", e); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } }; - // Lock the reverse_dns_map let reverse_dns_map_inner = match netstat_strage.reverse_dns_map.try_lock() { Ok(reverse_dns_map) => reverse_dns_map, Err(e) => { tracing::error!("[dns_map_update] lock error: {}", e); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } }; - for (ip_addr, _remote_host) in remote_hosts_inner.iter() { - if !reverse_dns_map_inner.contains_key(ip_addr) { + for (ip_addr, remote_host) in remote_hosts_inner.iter() { + // Best-effort reverse DNS only for unresolved hosts. + if remote_host.hostname.is_empty() && !reverse_dns_map_inner.contains_key(ip_addr) { lookup_target_ips.push(*ip_addr); } } - // Drop the lock before calling lookup_ips drop(remote_hosts_inner); drop(reverse_dns_map_inner); - let mut resolver = DnsResolver::new(); + + let resolver = DnsResolver::new(); let dns_map = resolver.lookup_ips(lookup_target_ips); - // Lock the remote_hosts + let mut remote_hosts_inner = match netstat_strage.remote_hosts.try_lock() { Ok(remote_hosts) => remote_hosts, Err(e) => { tracing::error!("[dns_map_update] lock error: {}", e); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } }; - // Lock the reverse_dns_map let mut reverse_dns_map_inner = match netstat_strage.reverse_dns_map.try_lock() { Ok(reverse_dns_map) => reverse_dns_map, Err(e) => { tracing::error!("[dns_map_update] lock error: {}", e); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } }; for (ip_addr, hostname) in dns_map { + if hostname.is_empty() { + continue; + } if let Some(remote_host) = remote_hosts_inner.get_mut(&ip_addr) { - remote_host.hostname = hostname.clone(); + if remote_host.hostname.is_empty() { + remote_host.hostname = hostname.clone(); + } } reverse_dns_map_inner.insert(ip_addr, hostname); } - // Drop the lock drop(remote_hosts_inner); drop(reverse_dns_map_inner); std::thread::sleep(std::time::Duration::from_secs(8)); @@ -359,6 +228,6 @@ impl DnsResolver { } pub fn lookup_ips(&self, ips: Vec) -> HashMap { - self.rt.block_on(async { lookup_ips_async(ips).await }) + self.rt.block_on(lookup_ips_async(ips)) } } diff --git a/src/net/host.rs b/src/net/host.rs index 93c6465..ad81cb2 100644 --- a/src/net/host.rs +++ b/src/net/host.rs @@ -13,9 +13,9 @@ pub struct RemoteHostInfo { impl RemoteHostInfo { pub fn new(mac_addr: String, ip_addr: IpAddr, hostname: String) -> Self { RemoteHostInfo { - mac_addr: mac_addr, - ip_addr: ip_addr, - hostname: hostname, + mac_addr, + ip_addr, + hostname, traffic_info: TrafficInfo::new(), } } diff --git a/src/net/interface.rs b/src/net/interface.rs index a5c36d1..e06a1e1 100644 --- a/src/net/interface.rs +++ b/src/net/interface.rs @@ -18,32 +18,23 @@ pub fn get_interface_by_ip(ip_addr: IpAddr) -> Option { } } } - return None; + None } pub fn get_interface_by_index(index: u32) -> Option { - for iface in nex::net::interface::get_interfaces() { - if iface.index == index { - return Some(iface); - } - } - return None; + nex::net::interface::get_interfaces() + .into_iter() + .find(|iface| iface.index == index) } pub fn get_interface_by_name(name: String) -> Option { - for iface in nex::net::interface::get_interfaces() { - if iface.name == name { - return Some(iface); - } - } - return None; + nex::net::interface::get_interfaces() + .into_iter() + .find(|iface| iface.name == name) } pub fn get_interface_ipv4(iface: &Interface) -> Option { - for ip in iface.ipv4.clone() { - return Some(IpAddr::V4(ip.addr())); - } - return None; + iface.ipv4.first().map(|ip| IpAddr::V4(ip.addr())) } pub fn get_interface_global_ipv6(iface: &Interface) -> Option { @@ -52,7 +43,7 @@ pub fn get_interface_global_ipv6(iface: &Interface) -> Option { return Some(IpAddr::V6(ip.addr())); } } - return None; + None } pub fn get_interface_local_ipv6(iface: &Interface) -> Option { @@ -61,7 +52,7 @@ pub fn get_interface_local_ipv6(iface: &Interface) -> Option { return Some(IpAddr::V6(ip.addr())); } } - return None; + None } pub fn get_interface_ips(iface: &Interface) -> Vec { @@ -76,13 +67,19 @@ pub fn get_interface_ips(iface: &Interface) -> Vec { } pub fn get_local_ips(if_index: u32) -> HashSet { - let interface = get_interface_by_index(if_index).unwrap(); let mut ips: HashSet = HashSet::new(); - for ip in interface.ipv4.clone() { - ips.insert(IpAddr::V4(ip.addr())); - } - for ip in interface.ipv6.clone() { - ips.insert(IpAddr::V6(ip.addr())); + if let Some(interface) = get_interface_by_index(if_index) { + for ip in interface.ipv4.clone() { + ips.insert(IpAddr::V4(ip.addr())); + } + for ip in interface.ipv6.clone() { + ips.insert(IpAddr::V6(ip.addr())); + } + } else { + tracing::warn!( + "failed to resolve interface by index {}; falling back to localhost only", + if_index + ); } // localhost IP addresses ips.insert(IpAddr::V4(Ipv4Addr::LOCALHOST)); @@ -91,14 +88,23 @@ pub fn get_local_ips(if_index: u32) -> HashSet { } pub fn get_default_local_ips() -> HashSet { - // Default interface IP addresses - let default_interface = netdev::get_default_interface().unwrap(); let mut ips: HashSet = HashSet::new(); - for ip in default_interface.ipv4.clone() { - ips.insert(IpAddr::V4(ip.addr())); - } - for ip in default_interface.ipv6.clone() { - ips.insert(IpAddr::V6(ip.addr())); + // Default interface IP addresses + match netdev::get_default_interface() { + Ok(default_interface) => { + for ip in default_interface.ipv4.clone() { + ips.insert(IpAddr::V4(ip.addr())); + } + for ip in default_interface.ipv6.clone() { + ips.insert(IpAddr::V6(ip.addr())); + } + } + Err(e) => { + tracing::warn!( + "failed to resolve default interface; falling back to localhost only: {}", + e + ); + } } // localhost IP addresses ips.insert(IpAddr::V4(Ipv4Addr::LOCALHOST)); @@ -137,14 +143,14 @@ pub fn get_local_ip_map() -> HashMap { pub fn get_usable_interfaces() -> Vec { let mut usable_interfaces: Vec = Vec::new(); for iface in nex::net::interface::get_interfaces() { - if iface.is_up() && (iface.ipv4.len() > 0 || iface.ipv6.len() > 0) { + if iface.is_up() && (!iface.ipv4.is_empty() || !iface.ipv6.is_empty()) { usable_interfaces.push(iface); } } usable_interfaces } -pub fn get_interfaces_by_name(names: &Vec) -> Vec { +pub fn get_interfaces_by_name(names: &[String]) -> Vec { let mut interfaces: Vec = Vec::new(); for iface in nex::net::interface::get_interfaces() { if names.contains(&iface.name) { @@ -156,14 +162,14 @@ pub fn get_interfaces_by_name(names: &Vec) -> Vec { pub fn get_interface_macaddr(iface: &Interface) -> MacAddr { match &iface.mac_addr { - Some(mac_addr) => mac_addr.clone(), + Some(mac_addr) => *mac_addr, None => MacAddr::zero(), } } pub fn get_gateway_macaddr(iface: &Interface) -> MacAddr { match &iface.gateway { - Some(gateway) => gateway.mac_addr.clone(), + Some(gateway) => gateway.mac_addr, None => MacAddr::zero(), } } diff --git a/src/net/ip.rs b/src/net/ip.rs index f893c65..570ff79 100644 --- a/src/net/ip.rs +++ b/src/net/ip.rs @@ -31,11 +31,7 @@ pub fn in_same_network(src_ip: IpAddr, dst_ip: IpAddr) -> bool { Ok(nw) => nw, Err(_) => return false, }; - if src_ip_nw == dst_ip_nw { - true - } else { - false - } + src_ip_nw == dst_ip_nw } pub fn guess_initial_ttl(ttl: u8) -> u8 { @@ -54,8 +50,8 @@ pub fn ipv4_to_int(ipv4: Ipv4Addr) -> u64 { let o2: u64 = ipv4.octets()[1].into(); let o3: u64 = ipv4.octets()[2].into(); let o4: u64 = ipv4.octets()[3].into(); - let ip_int: u64 = ((o1 << 24) + (o2 << 16) + (o3 << 8) + o4).into(); - return ip_int; + let ip_int: u64 = ((o1 << 24) + (o2 << 16) + (o3 << 8) + o4); + ip_int } pub fn ipv6_to_dec(ipv6: Ipv6Addr) -> u128 { @@ -64,7 +60,7 @@ pub fn ipv6_to_dec(ipv6: Ipv6Addr) -> u128 { let mut ip_int: u128 = 0; for i in 0..ipv6.segments().len() { let cur_seg: u128 = segments[(ipv6.segments().len() - 1) - i].into(); - ip_int = (cur_seg << i * 16) | ip_int; + ip_int |= (cur_seg << (i * 16)); } - return ip_int; + ip_int } diff --git a/src/net/packet.rs b/src/net/packet.rs index 1bcbbe5..9fc4234 100644 --- a/src/net/packet.rs +++ b/src/net/packet.rs @@ -52,9 +52,9 @@ impl PacketFrame { frame: nex::packet::frame::Frame, ) -> PacketFrame { PacketFrame { - capture_no: capture_no, - if_index: if_index, - if_name: if_name, + capture_no, + if_index, + if_name, datalink: frame.datalink, ip: frame.ip, transport: frame.transport, @@ -72,14 +72,11 @@ impl PacketFrame { }; // Remove UTC offset that start from + or - let datetime_vec = timestamp.split('+').collect::>(); - let timestamp: String = if datetime_vec.len() > 1 { - datetime_vec[0].to_string() - } else if datetime_vec.len() > 1 { + if datetime_vec.len() > 1 { datetime_vec[0].to_string() } else { timestamp - }; - timestamp + } } // Get most high level protocol pub fn get_protocol(&self) -> String { @@ -206,7 +203,7 @@ impl PacketStorage { } pub fn add_packet(&self, packet: PacketFrame) { - match self.storage.try_write() { + match self.storage.write() { Ok(mut storage) => { // If the storage is full, remove the oldest packet if storage.len() == self.max_capacity { @@ -214,18 +211,23 @@ impl PacketStorage { } storage.push_back(packet); } - Err(_) => { - // TODO: Log error or return error + Err(e) => { + tracing::error!("failed to lock packet storage for write: {}", e); + let mut storage = e.into_inner(); + if storage.len() == self.max_capacity { + storage.pop_front(); + } + storage.push_back(packet); } } } pub fn get_packets(&self) -> Vec { - match self.storage.try_read() { + match self.storage.read() { Ok(storage) => storage.iter().cloned().collect(), - Err(_) => { - // TODO: Log error or return error - Vec::new() + Err(e) => { + tracing::error!("failed to lock packet storage for read: {}", e); + e.into_inner().iter().cloned().collect() } } } diff --git a/src/net/pcap.rs b/src/net/pcap.rs index 6a4fe1d..58fa3a0 100644 --- a/src/net/pcap.rs +++ b/src/net/pcap.rs @@ -111,9 +111,9 @@ impl PacketCaptureOptions { }; Some(options) } - pub fn from_interface_name(if_name: String) -> PacketCaptureOptions { - let iface = interface::get_interface_by_name(if_name).unwrap(); - let options = PacketCaptureOptions { + pub fn from_interface_name(if_name: String) -> Option { + let iface = interface::get_interface_by_name(if_name)?; + Some(PacketCaptureOptions { interface_index: iface.index, interface_name: iface.name.clone(), src_ips: HashSet::new(), @@ -128,11 +128,10 @@ impl PacketCaptureOptions { receive_undefined: true, tunnel: iface.is_tun(), loopback: iface.is_loopback(), - }; - options + }) } pub fn from_interface(iface: &Interface) -> PacketCaptureOptions { - let options = PacketCaptureOptions { + PacketCaptureOptions { interface_index: iface.index, interface_name: iface.name.clone(), src_ips: HashSet::new(), @@ -147,8 +146,7 @@ impl PacketCaptureOptions { receive_undefined: true, tunnel: iface.is_tun(), loopback: iface.is_loopback(), - }; - options + } } pub fn add_ethertype_filter(&mut self, ethertype_name: &str) { // Currently, EtherType not support from_str, so we need to match it manually @@ -240,53 +238,38 @@ pub fn start_capture( let start_time = Instant::now(); report.start_time = sys::get_sysdate(); loop { - match rx.next() { - Ok(packet) => { - let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() - || (cfg!(any(target_os = "macos", target_os = "ios")) - && interface.is_loopback()) - { - let payload_offset; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } - parse_option.from_ip_packet = true; - parse_option.offset = payload_offset; - } - report.bytes = report.bytes.saturating_add(packet.len()); - report.packets = report.packets.saturating_add(1); - let frame: Frame = match Frame::from_buf(&packet, parse_option) { - Some(frame) => frame, - None => { - tracing::error!("Failed to parse packet"); - continue; - } - }; - if filter_packet(&frame, &capture_options) { - let packet_frame = PacketFrame::from_nex_frame( - report.packets, - interface.index, - interface.name.clone(), - frame, - ); - match msg_tx.send(packet_frame) { - Ok(_) => {} - Err(_) => {} - } + if let Ok(packet) = rx.next() { + let mut parse_option: ParseOption = ParseOption::default(); + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { + let payload_offset = if interface.is_loopback() { 14 } else { 0 }; + parse_option.from_ip_packet = true; + parse_option.offset = payload_offset; + } + report.bytes = report.bytes.saturating_add(packet.len()); + report.packets = report.packets.saturating_add(1); + let frame: Frame = match Frame::from_buf(packet, parse_option) { + Some(frame) => frame, + None => { + tracing::error!("Failed to parse packet"); + continue; } + }; + if filter_packet(&frame, &capture_options) { + let packet_frame = PacketFrame::from_nex_frame( + report.packets, + interface.index, + interface.name.clone(), + frame, + ); + if msg_tx.send(packet_frame).is_ok() {} } - Err(_) => {} } - match stop.lock() { - Ok(stop) => { - if *stop { - break; - } + if let Ok(stop) = stop.lock() { + if *stop { + break; } - Err(_) => {} } if Instant::now().duration_since(start_time) > capture_options.capture_timeout { break; @@ -325,43 +308,27 @@ pub fn start_live_capture( }; let start_time = Instant::now(); loop { - match rx.next() { - Ok(packet) => { - let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() - || (cfg!(any(target_os = "macos", target_os = "ios")) - && interface.is_loopback()) - { - let payload_offset; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } - parse_option.from_ip_packet = true; - parse_option.offset = payload_offset; - } - let frame: Frame = match Frame::from_buf(&packet, parse_option) { - Some(frame) => frame, - None => { - tracing::error!("Failed to parse packet"); - continue; - } - }; - if filter_packet(&frame, &capture_options) { - let packet_frame = PacketFrame::from_nex_frame( - 0, - interface.index, - interface.name.clone(), - frame, - ); - match msg_tx.send(packet_frame) { - Ok(_) => {} - Err(_) => {} - } + if let Ok(packet) = rx.next() { + let mut parse_option: ParseOption = ParseOption::default(); + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { + let payload_offset = if interface.is_loopback() { 14 } else { 0 }; + parse_option.from_ip_packet = true; + parse_option.offset = payload_offset; + } + let frame: Frame = match Frame::from_buf(packet, parse_option) { + Some(frame) => frame, + None => { + tracing::error!("Failed to parse packet"); + continue; } + }; + if filter_packet(&frame, &capture_options) { + let packet_frame = + PacketFrame::from_nex_frame(0, interface.index, interface.name.clone(), frame); + if msg_tx.send(packet_frame).is_ok() {} } - Err(_) => {} } if Instant::now().duration_since(start_time) > capture_options.capture_timeout { break; @@ -397,40 +364,27 @@ pub fn start_background_capture( }; let start_time = Instant::now(); loop { - match rx.next() { - Ok(packet) => { - let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() - || (cfg!(any(target_os = "macos", target_os = "ios")) - && interface.is_loopback()) - { - let payload_offset; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } - parse_option.from_ip_packet = true; - parse_option.offset = payload_offset; - } - let frame: Frame = match Frame::from_buf(&packet, parse_option) { - Some(frame) => frame, - None => { - tracing::error!("Failed to parse packet"); - continue; - } - }; - if filter_packet(&frame, &capture_options) { - let packet_frame = PacketFrame::from_nex_frame( - 0, - interface.index, - interface.name.clone(), - frame, - ); - netstat_strage.update(packet_frame); + if let Ok(packet) = rx.next() { + let mut parse_option: ParseOption = ParseOption::default(); + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { + let payload_offset = if interface.is_loopback() { 14 } else { 0 }; + parse_option.from_ip_packet = true; + parse_option.offset = payload_offset; + } + let frame: Frame = match Frame::from_buf(packet, parse_option) { + Some(frame) => frame, + None => { + tracing::error!("Failed to parse packet"); + continue; } + }; + if filter_packet(&frame, &capture_options) { + let packet_frame = + PacketFrame::from_nex_frame(0, interface.index, interface.name.clone(), frame); + netstat_strage.update(packet_frame); } - Err(_) => {} } if Instant::now().duration_since(start_time) > capture_options.capture_timeout { break; @@ -497,41 +451,23 @@ fn filter_packet(frame: &Frame, capture_options: &PacketCaptureOptions) -> bool } fn filter_host(src_ip: IpAddr, dst_ip: IpAddr, capture_options: &PacketCaptureOptions) -> bool { - if capture_options.src_ips.len() == 0 && capture_options.dst_ips.len() == 0 { + if capture_options.src_ips.is_empty() && capture_options.dst_ips.is_empty() { return true; } - if capture_options.src_ips.contains(&src_ip) || capture_options.dst_ips.contains(&dst_ip) { - return true; - } else { - return false; - } + capture_options.src_ips.contains(&src_ip) || capture_options.dst_ips.contains(&dst_ip) } fn filter_port(src_port: u16, dst_port: u16, capture_options: &PacketCaptureOptions) -> bool { - if capture_options.src_ports.len() == 0 && capture_options.dst_ports.len() == 0 { - return true; - } - if capture_options.src_ports.contains(&src_port) - || capture_options.dst_ports.contains(&dst_port) - { + if capture_options.src_ports.is_empty() && capture_options.dst_ports.is_empty() { return true; - } else { - return false; } + capture_options.src_ports.contains(&src_port) || capture_options.dst_ports.contains(&dst_port) } fn filter_ether_type(ether_type: EtherType, capture_options: &PacketCaptureOptions) -> bool { - if capture_options.ether_types.len() == 0 || capture_options.ether_types.contains(ðer_type) { - return true; - } else { - return false; - } + capture_options.ether_types.is_empty() || capture_options.ether_types.contains(ðer_type) } fn filter_ip_protocol(protocol: IpNextProtocol, capture_options: &PacketCaptureOptions) -> bool { - if capture_options.ip_protocols.len() == 0 || capture_options.ip_protocols.contains(&protocol) { - return true; - } else { - return false; - } + capture_options.ip_protocols.is_empty() || capture_options.ip_protocols.contains(&protocol) } diff --git a/src/net/protocol.rs b/src/net/protocol.rs index 199658e..f05a518 100644 --- a/src/net/protocol.rs +++ b/src/net/protocol.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord, Copy)] +#[allow(clippy::upper_case_acronyms)] pub enum Protocol { ARP, NDP, diff --git a/src/net/socket.rs b/src/net/socket.rs index 54a52c4..90050d8 100644 --- a/src/net/socket.rs +++ b/src/net/socket.rs @@ -184,6 +184,7 @@ pub enum AddressFamily { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Copy)] +#[allow(clippy::upper_case_acronyms)] pub enum TransportProtocol { TCP, UDP, @@ -214,9 +215,9 @@ pub struct LocalSocket { impl LocalSocket { pub fn new(interface_name: String, port: u16, protocol: TransportProtocol) -> Self { LocalSocket { - interface_name: interface_name, - port: port, - protocol: protocol, + interface_name, + port, + protocol, } } pub fn to_key_string(&self) -> String { @@ -237,11 +238,9 @@ pub struct ProtocolPort { impl ProtocolPort { pub fn new(port: u16, protocol: TransportProtocol) -> Self { - ProtocolPort { - port: port, - protocol: protocol, - } + ProtocolPort { port, protocol } } + #[allow(clippy::wrong_self_convention)] pub fn to_key_string(&self) -> String { format!("{}-{}", self.port, self.protocol.as_str()) } @@ -268,8 +267,8 @@ impl SocketInfoOption { transport_protocol: Vec, ) -> SocketInfoOption { SocketInfoOption { - address_family: address_family, - transport_protocol: transport_protocol, + address_family, + transport_protocol, } } pub fn get_address_family_flags(&self) -> AddressFamilyFlags { @@ -306,7 +305,13 @@ pub fn get_sockets_info(opt: SocketInfoOption) -> Vec { let af_flags: AddressFamilyFlags = opt.get_address_family_flags(); let proto_flags: ProtocolFlags = opt.get_protocol_flags(); let sockets: Vec = - netsock::get_sockets(af_flags, proto_flags).unwrap(); + match netsock::get_sockets(af_flags, proto_flags) { + Ok(sockets) => sockets, + Err(e) => { + tracing::warn!("[get_sockets_info] failed to fetch socket list: {}", e); + return Vec::new(); + } + }; let mut sockets_info: Vec = Vec::new(); for si in sockets { @@ -327,11 +332,10 @@ pub fn get_sockets_info(opt: SocketInfoOption) -> Vec { } else { AddressFamily::IPv6 }, - process: if let Some(proc) = si.processes.first() { - Some(ProcessInfo::new(proc.pid, proc.name.clone())) - } else { - None - }, + process: si + .processes + .first() + .map(|proc| ProcessInfo::new(proc.pid, proc.name.clone())), }; sockets_info.push(socket_info); } @@ -351,11 +355,10 @@ pub fn get_sockets_info(opt: SocketInfoOption) -> Vec { } else { AddressFamily::IPv6 }, - process: if let Some(proc) = si.processes.first() { - Some(ProcessInfo::new(proc.pid, proc.name.clone())) - } else { - None - }, + process: si + .processes + .first() + .map(|proc| ProcessInfo::new(proc.pid, proc.name.clone())), }; sockets_info.push(socket_info); } @@ -374,15 +377,12 @@ pub fn start_socket_info_update(netstat_strage: &mut Arc) { // Create Vec let mut local_sockets: HashSet = HashSet::new(); for si in &sockets_info { - match local_ip_map.get(&si.local_ip_addr) { - Some(interface_name) => { - local_sockets.insert(LocalSocket::new( - interface_name.to_owned(), - si.local_port, - si.protocol, - )); - } - None => {} + if let Some(interface_name) = local_ip_map.get(&si.local_ip_addr) { + local_sockets.insert(LocalSocket::new( + interface_name.to_owned(), + si.local_port, + si.protocol, + )); } } // Lock the local_socket_map @@ -390,6 +390,7 @@ pub fn start_socket_info_update(netstat_strage: &mut Arc) { Ok(connections) => connections, Err(e) => { tracing::error!("[socket_info_update] lock error: {}", e); + std::thread::sleep(std::time::Duration::from_millis(25)); continue; } }; @@ -405,20 +406,17 @@ pub fn start_socket_info_update(netstat_strage: &mut Arc) { } // Update socket info for socket_info in sockets_info { - match local_ip_map.get(&socket_info.local_ip_addr) { - Some(interface_name) => { - let local_socket = LocalSocket::new( - interface_name.to_owned(), - socket_info.local_port, - socket_info.protocol, - ); - let socket_process = local_socket_inner - .entry(local_socket) - .or_insert(SocketProcess::new()); - socket_process.status = socket_info.status; - socket_process.process = socket_info.process.clone(); - } - None => {} + if let Some(interface_name) = local_ip_map.get(&socket_info.local_ip_addr) { + let local_socket = LocalSocket::new( + interface_name.to_owned(), + socket_info.local_port, + socket_info.protocol, + ); + let socket_process = local_socket_inner + .entry(local_socket) + .or_insert(SocketProcess::new()); + socket_process.status = socket_info.status; + socket_process.process = socket_info.process.clone(); } } // Drop the lock diff --git a/src/net/stat.rs b/src/net/stat.rs index 40fa704..670a5f2 100644 --- a/src/net/stat.rs +++ b/src/net/stat.rs @@ -11,7 +11,7 @@ use crate::net::socket::{ }; use crate::process::{ProcessDisplayInfo, ProcessInfo}; use bytes::Bytes; -use netdev::{MacAddr, Interface}; +use netdev::{Interface, MacAddr}; use nex::packet::dns::{DnsPacket, DnsType}; use nex::packet::packet::Packet; use serde::{Deserialize, Serialize}; @@ -500,10 +500,10 @@ impl NetStatStrage { if let Some(_tcp) = transport.tcp { let socket_connection: SocketConnection = SocketConnection { interface_name: interface_name.clone(), - local_ip_addr: local_ip_addr, - local_port: local_port, - remote_ip_addr: remote_ip_addr, - remote_port: remote_port, + local_ip_addr, + local_port, + remote_ip_addr, + remote_port, protocol: TransportProtocol::TCP, }; let socket_traffic: &mut TrafficInfo = connections_inner @@ -522,11 +522,11 @@ impl NetStatStrage { } if let Some(_udp) = transport.udp { let socket_connection: SocketConnection = SocketConnection { - interface_name: interface_name, - local_ip_addr: local_ip_addr, - local_port: local_port, - remote_ip_addr: remote_ip_addr, - remote_port: remote_port, + interface_name, + local_ip_addr, + local_port, + remote_ip_addr, + remote_port, protocol: TransportProtocol::UDP, }; let socket_traffic: &mut TrafficInfo = connections_inner @@ -627,7 +627,7 @@ impl NetStatData { } }; NetStatData { - default_interface: default_interface, + default_interface, traffic: TrafficInfo::new(), remote_hosts: HashMap::new(), connection_map: HashMap::new(), @@ -753,18 +753,12 @@ impl NetStatData { } pub fn get_remote_hosts(&self, limit: Option) -> Vec { - let ipv4_asn_db = crate::db::IPV4_ASN_DB.get().unwrap().read().unwrap(); - let ipv4_country_db = crate::db::IPV4_COUNTRY_DB.get().unwrap().read().unwrap(); - let ipv6_asn_db = crate::db::IPV6_ASN_DB.get().unwrap().read().unwrap(); - let ipv6_country_db = crate::db::IPV6_COUNTRY_DB.get().unwrap().read().unwrap(); - let as_db = crate::db::AS_DB.get().unwrap().read().unwrap(); - // Create a map to store the traffic info for each remote host. let mut host_traffic_map: HashMap = HashMap::new(); self.remote_hosts.iter().for_each(|(_ip, host)| { match host_traffic_map.get(&host.ip_addr) { Some(traffic) => { - let mut traffic = traffic.clone(); + let mut traffic = *traffic; traffic += host.traffic_info.bytes_sent; traffic += host.traffic_info.bytes_received; host_traffic_map.insert(host.ip_addr, traffic); @@ -789,38 +783,8 @@ impl NetStatData { let host = HostDisplayInfo { ip_addr: host.ip_addr, hostname: host.hostname.clone(), - country_code: { - if nex::net::ip::is_global_ip(&host.ip_addr) { - match host.ip_addr { - IpAddr::V4(ipv4) => match ipv4_country_db.lookup(&ipv4) { - Some(country) => country.to_string(), - None => "N/A".to_string(), - }, - IpAddr::V6(ipv6) => match ipv6_country_db.lookup(&ipv6) { - Some(country) => country.to_string(), - None => "N/A".to_string(), - }, - } - } else { - String::from("N/A") - } - }, - as_name: { - if nex::net::ip::is_global_ip(&host.ip_addr) { - match host.ip_addr { - IpAddr::V4(ipv4) => ipv4_asn_db - .lookup(&ipv4) - .and_then(|asn| as_db.get_name(*asn)) - .map_or_else(|| String::from("N/A"), |asn| asn.to_string()), - IpAddr::V6(ipv6) => ipv6_asn_db - .lookup(&ipv6) - .and_then(|asn| as_db.get_name(*asn)) - .map_or_else(|| String::from("N/A"), |asn| asn.to_string()), - } - } else { - String::from("N/A") - } - }, + country_code: String::from("N/A"), + as_name: String::from("N/A"), traffic: host.traffic_info.to_display_info(), }; remote_hosts.push(host); @@ -838,23 +802,20 @@ impl NetStatData { port: conn.local_port, protocol: conn.protocol, }; - match self.local_socket_map.get(&local_socket) { - Some(socket_process) => { - if let Some(process) = &socket_process.process { - match process_traffic_map.get(&process.pid) { - Some(traffic) => { - let mut traffic = traffic.clone(); - traffic.add_traffic(traffic_info); - process_traffic_map.insert(process.pid, traffic); - } - None => { - process_traffic_map.insert(process.pid, traffic_info.clone()); - } + if let Some(socket_process) = self.local_socket_map.get(&local_socket) { + if let Some(process) = &socket_process.process { + match process_traffic_map.get(&process.pid) { + Some(traffic) => { + let mut traffic = traffic.clone(); + traffic.add_traffic(traffic_info); + process_traffic_map.insert(process.pid, traffic); + } + None => { + process_traffic_map.insert(process.pid, traffic_info.clone()); } - process_map.insert(process.pid, process.clone()); } + process_map.insert(process.pid, process.clone()); } - None => {} } }); // Create process total traffic map from process_traffic_map @@ -924,7 +885,7 @@ impl NetStatData { IpAddr::V6(_) => AddressFamily::IPv6, }, traffic: traffic.to_display_info(), - process: process, + process, }; top_connections.push(socket_traffic_info); } @@ -973,7 +934,7 @@ impl NetStatData { IpAddr::V6(_) => AddressFamily::IPv6, }, traffic: traffic.to_display_info(), - process: process, + process, }; if opt.address_family.contains(&socket_traffic_info.ip_version) && opt @@ -988,8 +949,32 @@ impl NetStatData { } pub fn get_app_protocols(&self, limit: Option) -> Vec { - let tcp_db = crate::db::TCP_SERVICE_DB.get().unwrap().read().unwrap(); - let udp_db = crate::db::UDP_SERVICE_DB.get().unwrap().read().unwrap(); + let tcp_db = match crate::db::TCP_SERVICE_DB.get() { + Some(db) => match db.read() { + Ok(guard) => Some(guard), + Err(e) => { + tracing::warn!("failed to read TCP service DB: {}", e); + None + } + }, + None => { + tracing::warn!("TCP service DB is not initialized"); + None + } + }; + let udp_db = match crate::db::UDP_SERVICE_DB.get() { + Some(db) => match db.read() { + Ok(guard) => Some(guard), + Err(e) => { + tracing::warn!("failed to read UDP service DB: {}", e); + None + } + }, + None => { + tracing::warn!("UDP service DB is not initialized"); + None + } + }; let mut protocol_port_map: HashMap = HashMap::new(); self.connection_map.iter().for_each(|(conn, traffic_info)| { let protocol_port: ProtocolPort = ProtocolPort { @@ -1026,11 +1011,13 @@ impl NetStatData { protocol: protocol_port.protocol.as_str().to_string(), name: match protocol_port.protocol { TransportProtocol::TCP => tcp_db - .get_name(protocol_port.port) + .as_ref() + .and_then(|db| db.get_name(protocol_port.port)) .unwrap_or("Unknown TCP Service") .to_string(), TransportProtocol::UDP => udp_db - .get_name(protocol_port.port) + .as_ref() + .and_then(|db| db.get_name(protocol_port.port)) .unwrap_or("Unknown UDP Service") .to_string(), }, diff --git a/src/process.rs b/src/process.rs index fd6b4c5..34190bc 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,14 +1,14 @@ use serde::{Deserialize, Serialize}; -use crate::net::traffic::{TrafficDisplayInfo, TrafficInfo}; +use crate::net::traffic::TrafficDisplayInfo; -#[derive(Serialize, Deserialize, Debug, Clone)] +/* #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserInfo { pub user_id: String, pub group_id: String, pub user_name: String, pub groups: Vec, -} +} */ #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessInfo { @@ -18,18 +18,15 @@ pub struct ProcessInfo { impl ProcessInfo { pub fn new(pid: u32, name: String) -> ProcessInfo { - ProcessInfo { - pid: pid, - name: name, - } + ProcessInfo { pid, name } } } -#[derive(Serialize, Deserialize, Debug, Clone)] +/* #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessTrafficInfo { pub process: ProcessInfo, pub traffic: TrafficInfo, -} +} */ #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessDisplayInfo { diff --git a/src/sys/windows.rs b/src/sys/windows.rs index b382f81..da0997c 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -1,6 +1,6 @@ use inquire::Confirm; -use winreg::enums::HKEY_LOCAL_MACHINE; use winreg::RegKey; +use winreg::enums::HKEY_LOCAL_MACHINE; use crate::deps::DepsError; diff --git a/src/tui/live/app.rs b/src/tui/live/app.rs index 0b710d4..ed8b19e 100644 --- a/src/tui/live/app.rs +++ b/src/tui/live/app.rs @@ -59,8 +59,8 @@ impl<'a> App<'a> { talbe_state: TableState::default(), row_selecting: false, packets: Vec::new(), - enhanced_graphics: enhanced_graphics, - config: config, + enhanced_graphics, + config, } } @@ -68,6 +68,10 @@ impl<'a> App<'a> { // Select the previous row self.row_selecting = true; let row_count = self.packets.len(); + if row_count == 0 { + self.talbe_state.select(None); + return; + } let i = match self.talbe_state.selected() { Some(i) => { if i == 0 { @@ -85,6 +89,10 @@ impl<'a> App<'a> { // Select the next row self.row_selecting = true; let row_count = self.packets.len(); + if row_count == 0 { + self.talbe_state.select(None); + return; + } let i = match self.talbe_state.selected() { Some(i) => { if i >= row_count - 1 { @@ -130,7 +138,11 @@ impl<'a> App<'a> { } 'b' => { // Scroll to the bottom - self.talbe_state.select(Some(self.packets.len() - 1)); + if self.packets.is_empty() { + self.talbe_state.select(None); + } else { + self.talbe_state.select(Some(self.packets.len() - 1)); + } self.row_selecting = false; } _ => {} @@ -142,8 +154,10 @@ impl<'a> App<'a> { // Set the latest packets self.packets = packets; // If the user is not selecting a row, scroll to the bottom - if !self.row_selecting { + if !self.row_selecting && !self.packets.is_empty() { self.talbe_state.select(Some(self.packets.len() - 1)); + } else if self.packets.is_empty() { + self.talbe_state.select(None); } } } diff --git a/src/tui/live/terminal.rs b/src/tui/live/terminal.rs index 6d1f029..8c04eab 100644 --- a/src/tui/live/terminal.rs +++ b/src/tui/live/terminal.rs @@ -1,90 +1,80 @@ -use anyhow::Result; -use std::{ - io, - time::{Duration, Instant}, -}; - use crate::config::AppConfig; use crate::{net::packet::PacketStorage, sys}; +use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + event::{Event, EventStream, KeyCode, KeyEventKind}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; +use futures::StreamExt; use ratatui::prelude::*; -use std::sync::Arc; +use std::{io, sync::Arc, time::Duration}; +use tokio::time::{self, Instant}; use super::app::App; use super::ui; -pub fn run( +pub async fn run( app_config: AppConfig, enhanced_graphics: bool, packet_strage: &Arc, ) -> Result<()> { - // setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; - // create app and run it + let title = sys::get_app_title(); - let app = App::new(&title, enhanced_graphics, app_config); - let res = run_app(&mut terminal, app, packet_strage); + let mut app = App::new(&title, enhanced_graphics, app_config); + let result = run_app(&mut terminal, &mut app, packet_strage).await; - // restore terminal disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; - if let Err(err) = res { - eprintln!("{err:?}"); - } - - Ok(()) + Ok(result?) } -fn run_app( +async fn run_app( terminal: &mut Terminal, - mut app: App, + app: &mut App<'_>, packet_strage: &Arc, ) -> io::Result<()> { let tick_rate = Duration::from_millis(app.config.display.tick_rate); - let mut last_tick = Instant::now(); + let mut tick = time::interval(tick_rate); + let mut events = EventStream::new(); + let mut last_draw = Instant::now(); + loop { - if last_tick.elapsed() >= tick_rate { - if !app.should_pause { - app.on_tick(packet_strage.get_packets()); + tokio::select! { + _ = tick.tick() => { + if !app.should_pause { + app.on_tick(packet_strage.get_packets()); + } } - last_tick = Instant::now(); - } - - terminal.draw(|f| ui::draw(f, &mut app))?; - - let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Left | KeyCode::Char('a') => app.on_left(), - KeyCode::Up | KeyCode::Char('w') => app.on_up(), - KeyCode::Right | KeyCode::Char('d') => app.on_right(), - KeyCode::Down | KeyCode::Char('s') => app.on_down(), - KeyCode::Tab => app.on_tab(), - KeyCode::BackTab => app.on_shift_tab(), - KeyCode::Char(c) => app.on_key(c), - _ => {} + maybe_event = events.next() => { + if let Some(Ok(Event::Key(key))) = maybe_event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Up | KeyCode::Char('w') => app.on_up(), + KeyCode::Down | KeyCode::Char('s') => app.on_down(), + KeyCode::Char(c) => app.on_key(c), + _ => {} + } } } } } + if last_draw.elapsed() >= Duration::from_millis(16) { + terminal + .draw(|f| ui::draw(f, app)) + .map_err(|e| io::Error::other(e.to_string()))?; + last_draw = Instant::now(); + } + if app.should_quit { return Ok(()); } diff --git a/src/tui/live/ui.rs b/src/tui/live/ui.rs index 373d41e..cce043b 100644 --- a/src/tui/live/ui.rs +++ b/src/tui/live/ui.rs @@ -1,4 +1,7 @@ use super::app::App; +use nex::packet::dns::DnsPacket; +use nex::packet::packet::Packet; +use nex::packet::tcp::TcpFlags; use ratatui::{prelude::*, widgets::*}; pub fn draw(f: &mut Frame, app: &mut App) { @@ -8,8 +11,8 @@ pub fn draw(f: &mut Frame, app: &mut App) { Constraint::Min(0), Constraint::Length(1), ]) - .split(f.size()); - let titles = app + .split(f.area()); + let titles: Vec> = app .tabs .titles .iter() @@ -33,14 +36,11 @@ pub fn draw(f: &mut Frame, app: &mut App) { .select(app.tabs.index) }; f.render_widget(tabs, chunks[0]); - match app.tabs.index { - 0 => draw_live_capture_tab(f, app, chunks[1]), - _ => {} + if app.tabs.index == 0 { + draw_live_capture_tab(f, app, chunks[1]); }; // Draw footer - let footer = format!( - "Press to quit, to pause, / to scroll, to scroll to the bottom" - ); + let footer = "Press quit | pause | / select | jump latest"; let footer = Paragraph::new(text::Line::from(Span::styled( footer, Style::default().fg(Color::DarkGray), @@ -48,8 +48,208 @@ pub fn draw(f: &mut Frame, app: &mut App) { f.render_widget(footer, chunks[2]); } +fn packet_info(packet: &crate::net::packet::PacketFrame) -> String { + let app_hint = packet_app_hint(packet); + if let Some(transport) = &packet.transport { + if let Some(tcp) = &transport.tcp { + let flags = tcp.flags; + let has = |bit: u8| flags & bit == bit; + let mut flag_text = String::new(); + if has(TcpFlags::SYN) { + flag_text.push('S'); + } + if has(TcpFlags::FIN) { + flag_text.push('F'); + } + if has(TcpFlags::PSH) { + flag_text.push('P'); + } + if has(TcpFlags::RST) { + flag_text.push('R'); + } + if has(TcpFlags::URG) { + flag_text.push('U'); + } + if has(TcpFlags::ECE) { + flag_text.push('E'); + } + if has(TcpFlags::CWR) { + flag_text.push('W'); + } + if has(TcpFlags::ACK) { + flag_text.push('.'); + } + if flag_text.is_empty() { + flag_text.push('.'); + } + + let payload_len = packet.payload.len() as u32; + let seq_end = tcp.sequence.saturating_add(payload_len); + if payload_len > 0 { + let base = format!( + "Flags [{}], seq {}:{}, ack {}, win {}, length {}", + flag_text, tcp.sequence, seq_end, tcp.acknowledgement, tcp.window, payload_len + ); + return if app_hint.is_empty() { + base + } else { + format!("{base} | {app_hint}") + }; + } + let base = format!( + "Flags [{}], seq {}, ack {}, win {}, length 0", + flag_text, tcp.sequence, tcp.acknowledgement, tcp.window + ); + return if app_hint.is_empty() { + base + } else { + format!("{base} | {app_hint}") + }; + } + if let Some(udp) = &transport.udp { + let base = format!( + "{} > {}: UDP, length {}", + udp.source, + udp.destination, + packet.payload.len() + ); + return if app_hint.is_empty() { + base + } else { + format!("{base} | {app_hint}") + }; + } + } + if let Some(ip) = &packet.ip { + if ip.icmp.is_some() || ip.icmpv6.is_some() { + return "ICMP".to_string(); + } + if let Some(ipv4) = &ip.ipv4 { + return format!("IPv4 ttl={} id={}", ipv4.ttl, ipv4.identification); + } + if let Some(ipv6) = &ip.ipv6 { + return format!("IPv6 hop_limit={}", ipv6.hop_limit); + } + } + if let Some(datalink) = &packet.datalink { + if datalink.arp.is_some() { + return "ARP".to_string(); + } + } + String::new() +} + +fn is_tls_client_hello(payload: &[u8]) -> bool { + // TLS record: handshake(0x16), then client_hello(0x01) + payload.len() > 5 && payload[0] == 0x16 && payload[5] == 0x01 +} + +fn first_http_line(payload: &[u8]) -> Option { + let s = std::str::from_utf8(payload).ok()?; + let first = s.lines().next()?.trim(); + if first.starts_with("GET ") + || first.starts_with("POST ") + || first.starts_with("PUT ") + || first.starts_with("DELETE ") + || first.starts_with("HEAD ") + || first.starts_with("PATCH ") + || first.starts_with("OPTIONS ") + || first.starts_with("HTTP/") + { + Some(first.to_string()) + } else { + None + } +} + +fn http_host(payload: &[u8]) -> Option { + let s = std::str::from_utf8(payload).ok()?; + for line in s.lines() { + if let Some(rest) = line.strip_prefix("Host:") { + return Some(rest.trim().to_string()); + } + } + None +} + +fn dns_hint(payload: bytes::Bytes) -> Option { + let dns = DnsPacket::from_buf(&payload)?; + if let Some(query) = dns.queries.first() { + if let Ok(name) = query.get_qname_parsed() { + return Some(format!("DNS query {}", name)); + } + } + if !dns.responses.is_empty() { + return Some(format!("DNS response {} record(s)", dns.responses.len())); + } + Some("DNS".to_string()) +} + +fn packet_app_hint(packet: &crate::net::packet::PacketFrame) -> String { + if let Some(transport) = &packet.transport { + if let Some(udp) = &transport.udp { + if udp.source == 53 || udp.destination == 53 { + if let Some(hint) = dns_hint(packet.payload.clone()) { + return hint; + } + } + } + if let Some(tcp) = &transport.tcp { + let payload = packet.payload.as_ref(); + if payload.is_empty() { + return String::new(); + } + if tcp.source == 53 || tcp.destination == 53 { + if let Some(hint) = dns_hint(packet.payload.clone()) { + return hint; + } + } + if (tcp.source == 443 || tcp.destination == 443) && is_tls_client_hello(payload) { + return "TLS ClientHello".to_string(); + } + if tcp.source == 80 + || tcp.destination == 80 + || tcp.source == 8080 + || tcp.destination == 8080 + { + if let Some(line) = first_http_line(payload) { + if let Some(host) = http_host(payload) { + return format!("HTTP {} host={}", line, host); + } + return format!("HTTP {}", line); + } + } + } + } + String::new() +} + +fn payload_hex_preview(payload: &[u8], max_len: usize) -> String { + if payload.is_empty() { + return String::from("-"); + } + let take_len = payload.len().min(max_len); + let mut out = String::new(); + for (idx, b) in payload.iter().take(take_len).enumerate() { + if idx > 0 { + out.push(' '); + } + out.push_str(&format!("{b:02x}")); + } + if payload.len() > max_len { + out.push_str(" ..."); + } + out +} + +fn selected_packet<'a>(app: &'a App) -> Option<&'a crate::net::packet::PacketFrame> { + app.talbe_state + .selected() + .and_then(|idx| app.packets.get(idx)) + .or_else(|| app.packets.last()) +} + fn draw_packet_table(f: &mut Frame, app: &mut App, area: Rect) { - // Draw top Remote Address Table let rows = app .packets .iter() @@ -61,63 +261,191 @@ fn draw_packet_table(f: &mut Frame, app: &mut App, area: Rect) { packet.get_dst_addr(), packet.get_protocol(), packet.packet_len.to_string(), - packet.get_src_port(), - packet.get_dst_port(), - packet.if_name.clone(), + packet_info(packet), ]) }) .collect::>(); let widths = [ Constraint::Length(6), - Constraint::Length(16), - Constraint::Length(40), - Constraint::Length(40), + Constraint::Length(14), + Constraint::Length(24), + Constraint::Length(24), + Constraint::Length(7), Constraint::Length(8), - Constraint::Length(8), - Constraint::Length(8), - Constraint::Length(8), - Constraint::Length(38), + Constraint::Min(20), ]; - let table_title: String; - if app.config.network.interfaces.is_empty() { - table_title = "Capturing from all available interfaces".to_string(); + let table_title: String = if app.config.network.interfaces.is_empty() { + "Capturing from all available interfaces".to_string() } else { - table_title = format!( + format!( "Capturing from {}", app.config.network.interfaces.join(", ") - ); - } + ) + }; //let mut table_state = TableState::default(); let table = Table::new(rows, widths) .column_spacing(1) - //.style(Style::new().blue()) .header( Row::new(vec![ "No.", - "Timestamp", - "SRC Address", - "DST Address", - "Protocol", - "Length", - "SRC Port", - "DST Port", - "Interface", + "Time", + "Source", + "Destination", + "Proto", + "Len", + "Info", ]) .style(Style::new().bold()), //.bottom_margin(1), ) .block(Block::default().borders(Borders::ALL).title(table_title)) - .highlight_style(Style::new().reversed()) + .row_highlight_style(Style::new().reversed()) .highlight_symbol(">>"); //f.render_widget(table, area); f.render_stateful_widget(table, area, &mut app.talbe_state); } +fn draw_packet_detail(f: &mut Frame, app: &App, area: Rect) { + let mut lines: Vec> = Vec::new(); + if let Some(packet) = selected_packet(app) { + lines.push(Line::raw(format!( + "No={} Iface={} Time={}", + packet.capture_no, packet.if_name, packet.timestamp + ))); + lines.push(Line::raw(format!( + "Source={} Destination={}", + packet.get_src_addr(), + packet.get_dst_addr() + ))); + lines.push(Line::raw(format!( + "Proto={} Len={} {}", + packet.get_protocol(), + packet.packet_len, + packet_info(packet) + ))); + + if let Some(ip) = &packet.ip { + if let Some(ipv4) = &ip.ipv4 { + lines.push(Line::raw(format!( + "IPv4: {} -> {} ttl={} id={} proto={}", + ipv4.source, + ipv4.destination, + ipv4.ttl, + ipv4.identification, + ipv4.next_level_protocol.as_str() + ))); + } + if let Some(ipv6) = &ip.ipv6 { + lines.push(Line::raw(format!( + "IPv6: {} -> {} hop_limit={} next={}", + ipv6.source, + ipv6.destination, + ipv6.hop_limit, + ipv6.next_header.as_str() + ))); + } + } + + if let Some(transport) = &packet.transport { + if let Some(tcp) = &transport.tcp { + lines.push(Line::raw(format!( + "TCP: {} -> {} flags=0x{:02x} seq={} ack={} win={}", + tcp.source, + tcp.destination, + tcp.flags, + tcp.sequence, + tcp.acknowledgement, + tcp.window + ))); + } + if let Some(udp) = &transport.udp { + lines.push(Line::raw(format!( + "UDP: {} -> {} len={} checksum=0x{:04x}", + udp.source, udp.destination, udp.length, udp.checksum + ))); + } + } + + if let Some(datalink) = &packet.datalink { + if let Some(eth) = &datalink.ethernet { + lines.push(Line::raw(format!( + "Ethernet: {} -> {} type={}", + eth.source, + eth.destination, + eth.ethertype.name() + ))); + } + if let Some(arp) = &datalink.arp { + lines.push(Line::raw(format!( + "ARP: {} -> {}", + arp.sender_proto_addr, arp.target_proto_addr + ))); + } + } + + lines.push(Line::raw(format!( + "Payload({}B): {}", + packet.payload.len(), + payload_hex_preview(packet.payload.as_ref(), 48) + ))); + } else { + lines.push(Line::raw("No packets captured yet.")); + } + + let detail = Paragraph::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .title("Packet Details"), + ) + .wrap(Wrap { trim: true }); + f.render_widget(detail, area); +} + fn draw_live_capture_tab(f: &mut Frame, app: &mut App, area: Rect) { let chunks = Layout::default() - .constraints(vec![Constraint::Percentage(100)]) + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(68), Constraint::Percentage(32)]) .split(area); draw_packet_table(f, app, chunks[0]); + draw_packet_detail(f, app, chunks[1]); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tls_client_hello_detector_works() { + let payload = [0x16, 0x03, 0x03, 0x00, 0x20, 0x01, 0x00]; + assert!(is_tls_client_hello(&payload)); + assert!(!is_tls_client_hello(&[])); + assert!(!is_tls_client_hello(&[0x17, 0x03, 0x03, 0x00, 0x20, 0x01])); + } + + #[test] + fn http_first_line_and_host_parse() { + let req = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: x\r\n\r\n"; + assert_eq!( + first_http_line(req).as_deref(), + Some("GET /index.html HTTP/1.1") + ); + assert_eq!(http_host(req).as_deref(), Some("example.com")); + + let res = b"HTTP/1.1 200 OK\r\nServer: test\r\n\r\n"; + assert_eq!(first_http_line(res).as_deref(), Some("HTTP/1.1 200 OK")); + assert_eq!(http_host(res), None); + } + + #[test] + fn payload_hex_preview_formats_and_truncates() { + assert_eq!(payload_hex_preview(&[], 8), "-"); + assert_eq!(payload_hex_preview(&[0x01, 0xab, 0xff], 8), "01 ab ff"); + assert_eq!( + payload_hex_preview(&[0xde, 0xad, 0xbe, 0xef, 0x01], 4), + "de ad be ef ..." + ); + } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index e3287c3..db67f90 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,3 +1,2 @@ pub mod live; pub mod monitor; -pub mod stat; diff --git a/src/tui/monitor/app.rs b/src/tui/monitor/app.rs index cdbed5c..a47a814 100644 --- a/src/tui/monitor/app.rs +++ b/src/tui/monitor/app.rs @@ -1,164 +1,101 @@ -#![allow(unused)] - +use crate::config::AppConfig; +use crate::net::host::HostDisplayInfo; +use crate::net::socket::SocketDisplayInfo; +use crate::net::stat::NetStatData; +use crate::process::ProcessDisplayInfo; use std::time::Duration; -use crate::{ - config::AppConfig, - net::{ - host::HostDisplayInfo, service::ServiceDisplayInfo, socket::SocketDisplayInfo, - stat::NetStatData, - }, - process::ProcessDisplayInfo, -}; -use ratatui::widgets::TableState; - -pub struct TabsState<'a> { - pub titles: Vec<&'a str>, - pub index: usize, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FocusTab { + Overview, + Hosts, + Connections, + Processes, } -impl<'a> TabsState<'a> { - pub fn new(titles: Vec<&'a str>) -> TabsState<'a> { - TabsState { titles, index: 0 } - } - pub fn next(&mut self) { - self.index = (self.index + 1) % self.titles.len(); - } +impl FocusTab { + pub const ALL: [FocusTab; 4] = [ + FocusTab::Overview, + FocusTab::Hosts, + FocusTab::Connections, + FocusTab::Processes, + ]; - pub fn previous(&mut self) { - if self.index > 0 { - self.index -= 1; - } else { - self.index = self.titles.len() - 1; + pub fn title(self) -> &'static str { + match self { + FocusTab::Overview => "Overview", + FocusTab::Hosts => "Hosts", + FocusTab::Connections => "Connections", + FocusTab::Processes => "Processes", } } } -pub struct App<'a> { - pub title: &'a str, +pub struct App { + pub title: String, pub should_pause: bool, pub should_quit: bool, - pub tabs: TabsState<'a>, - pub talbe_state: TableState, + pub show_bandwidth: bool, + pub selected_tab: FocusTab, pub netstat_data: NetStatData, pub remote_hosts: Vec, pub processes: Vec, pub connections: Vec, - pub app_protocols: Vec, - pub enhanced_graphics: bool, pub config: AppConfig, } -impl<'a> App<'a> { - pub fn new(title: &'a str, enhanced_graphics: bool, config: AppConfig) -> App<'a> { +impl App { + pub fn new(title: String, config: AppConfig) -> App { App { title, should_pause: false, should_quit: false, - tabs: TabsState::new(vec!["Statistics", "RemoteAddresses", "Connections"]), - talbe_state: TableState::default(), + show_bandwidth: config.display.show_bandwidth, + selected_tab: FocusTab::Overview, netstat_data: NetStatData::new(), remote_hosts: vec![], processes: vec![], connections: vec![], - app_protocols: vec![], - enhanced_graphics: enhanced_graphics, - config: config, + config, } } - pub fn on_up(&mut self) { - if self.tabs.index == 0 { - return; - } - // Select the previous row - let row_count = match self.tabs.index { - 1 => self.remote_hosts.len(), - 2 => self.connections.len(), - _ => 0, - }; - let i = match self.talbe_state.selected() { - Some(i) => { - if i == 0 { - row_count - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.talbe_state.select(Some(i)); + pub fn switch_next_tab(&mut self) { + let index = FocusTab::ALL + .iter() + .position(|tab| *tab == self.selected_tab) + .unwrap_or(0); + let next_index = (index + 1) % FocusTab::ALL.len(); + self.selected_tab = FocusTab::ALL[next_index]; } - pub fn on_down(&mut self) { - if self.tabs.index == 0 { - return; - } - // Select the next row - let row_count = match self.tabs.index { - 1 => self.remote_hosts.len(), - 2 => self.connections.len(), - _ => 0, - }; - let i = match self.talbe_state.selected() { - Some(i) => { - if i >= row_count - 1 { - 0 - } else { - i + 1 - } - } - None => 0, + pub fn switch_prev_tab(&mut self) { + let index = FocusTab::ALL + .iter() + .position(|tab| *tab == self.selected_tab) + .unwrap_or(0); + let prev_index = if index == 0 { + FocusTab::ALL.len() - 1 + } else { + index - 1 }; - self.talbe_state.select(Some(i)); - } - - pub fn on_right(&mut self) { - // Select the next tab - self.tabs.next(); - } - - pub fn on_left(&mut self) { - // Select the previous tab - self.tabs.previous(); - } - - pub fn on_tab(&mut self) { - // Select the next tab - self.tabs.next(); - } - - pub fn on_shift_tab(&mut self) { - // Select the previous tab - self.tabs.previous(); + self.selected_tab = FocusTab::ALL[prev_index]; } pub fn on_key(&mut self, c: char) { match c { - 'q' => { - // Quit the application - self.should_quit = true; - } - ' ' => { - // Pause the application - self.should_pause = !self.should_pause; - } - 't' => { - // Switch display mode (total/bandwidth) - self.config.display.show_bandwidth = !self.config.display.show_bandwidth; - } + 'q' => self.should_quit = true, + ' ' => self.should_pause = !self.should_pause, + 't' => self.show_bandwidth = !self.show_bandwidth, _ => {} } } pub fn on_tick(&mut self, netstat_data: NetStatData) { - // Update the state of the application - self.netstat_data.merge( - netstat_data, - Duration::from_millis(self.config.display.tick_rate), - ); + let tick_rate = Duration::from_millis(self.config.display.tick_rate); + self.netstat_data.merge(netstat_data, tick_rate); self.remote_hosts = self.netstat_data.get_remote_hosts(None); - //self.top_processes = app.netstat_data.get_top_processes(); self.connections = self.netstat_data.get_connections(None); + self.processes = self.netstat_data.get_processes(None); } } diff --git a/src/tui/monitor/terminal.rs b/src/tui/monitor/terminal.rs index c7ea70b..e0f126b 100644 --- a/src/tui/monitor/terminal.rs +++ b/src/tui/monitor/terminal.rs @@ -1,94 +1,83 @@ -use anyhow::Result; -use std::{ - io, - time::{Duration, Instant}, +use crate::{ + config::AppConfig, net::stat::NetStatStrage, sys, tui::monitor::app::App, tui::monitor::ui, }; - -use crate::{config::AppConfig, net::stat::NetStatStrage}; -use crate::{sys, tui::monitor::app::App, tui::monitor::ui}; +use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + event::{Event, EventStream, KeyCode, KeyEventKind}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; +use futures::StreamExt; use ratatui::prelude::*; -use std::sync::Arc; +use std::{io, sync::Arc, time::Duration}; +use tokio::time::{self, Instant}; -pub fn run( +pub async fn run( app_config: AppConfig, - enhanced_graphics: bool, + _enhanced_graphics: bool, netstat_strage: &mut Arc, ) -> Result<()> { - // setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.clear()?; - // create app and run it + let title = sys::get_app_title(); - let app = App::new(&title, enhanced_graphics, app_config); - let res = run_app(&mut terminal, app, netstat_strage); + let mut app = App::new(title, app_config); + let result = run_app(&mut terminal, &mut app, netstat_strage).await; - // restore terminal disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; - if let Err(err) = res { - eprintln!("{err:?}"); - } - - Ok(()) + Ok(result?) } -fn run_app( +async fn run_app( terminal: &mut Terminal, - mut app: App, + app: &mut App, netstat_strage: &mut Arc, ) -> io::Result<()> { let tick_rate = Duration::from_millis(app.config.display.tick_rate); let entry_ttl = Duration::from_millis(app.config.network.entry_ttl); - let mut last_tick = Instant::now(); - let mut last_clear = Instant::now(); - loop { - if last_clear.elapsed() >= entry_ttl { - app.netstat_data.remove_old_entries(entry_ttl); - last_clear = Instant::now(); - } + let mut tick = time::interval(tick_rate); + let mut clear_tick = time::interval(entry_ttl); + let mut events = EventStream::new(); + let mut last_draw = Instant::now(); - if last_tick.elapsed() >= tick_rate { - if !app.should_pause { - app.on_tick(netstat_strage.clone_data_and_reset()); + loop { + tokio::select! { + _ = tick.tick() => { + if !app.should_pause { + app.on_tick(netstat_strage.clone_data_and_reset()); + } } - last_tick = Instant::now(); - } - - terminal.draw(|f| ui::draw(f, &mut app))?; - - let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Left | KeyCode::Char('a') => app.on_left(), - KeyCode::Up | KeyCode::Char('w') => app.on_up(), - KeyCode::Right | KeyCode::Char('d') => app.on_right(), - KeyCode::Down | KeyCode::Char('s') => app.on_down(), - KeyCode::Tab => app.on_tab(), - KeyCode::BackTab => app.on_shift_tab(), - KeyCode::Char(c) => app.on_key(c), - _ => {} + _ = clear_tick.tick() => { + app.netstat_data.remove_old_entries(entry_ttl); + } + maybe_event = events.next() => { + if let Some(Ok(Event::Key(key))) = maybe_event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Tab | KeyCode::Right => app.switch_next_tab(), + KeyCode::BackTab | KeyCode::Left => app.switch_prev_tab(), + KeyCode::Char(c) => app.on_key(c), + _ => {} + } } } } } + if last_draw.elapsed() >= Duration::from_millis(16) { + terminal + .draw(|f| ui::draw(f, app)) + .map_err(|e| io::Error::other(e.to_string()))?; + last_draw = Instant::now(); + } + if app.should_quit { return Ok(()); } diff --git a/src/tui/monitor/ui.rs b/src/tui/monitor/ui.rs index 3eb92e1..011e73d 100644 --- a/src/tui/monitor/ui.rs +++ b/src/tui/monitor/ui.rs @@ -1,413 +1,342 @@ +use crate::tui::monitor::app::{App, FocusTab}; use ratatui::{prelude::*, widgets::*}; - -use crate::tui::monitor::app::App; +use std::collections::HashMap; pub fn draw(f: &mut Frame, app: &mut App) { - let chunks = Layout::default() + let layout = Layout::default() + .direction(Direction::Vertical) .constraints([ Constraint::Length(3), + Constraint::Length(4), Constraint::Min(0), Constraint::Length(1), ]) - .split(f.size()); - let titles = app - .tabs - .titles - .iter() - .map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green)))) - .collect(); - let tabs = if app.should_pause { - let pause_title = format!("{} [Paused] press to resume", app.title); - Tabs::new(titles) - .block( - Block::default() - .borders(Borders::ALL) - .title(pause_title) - .style(Style::default().fg(Color::Yellow)), - ) - .highlight_style(Style::default().fg(Color::LightBlue)) - .select(app.tabs.index) - } else { - Tabs::new(titles) - .block(Block::default().borders(Borders::ALL).title(app.title)) - .highlight_style(Style::default().fg(Color::LightBlue)) - .select(app.tabs.index) - }; - f.render_widget(tabs, chunks[0]); - match app.tabs.index { - 0 => draw_overview_tab(f, app, chunks[1]), - 1 => draw_remotehosts_tab(f, app, chunks[1]), - 2 => draw_connections_tab(f, app, chunks[1]), - _ => {} - }; - // Draw footer - let footer = format!("Press to quit, to switch tabs, to pause, to toggle bandwidth display, / to scroll"); - let footer = Paragraph::new(text::Line::from(Span::styled( - footer, - Style::default().fg(Color::DarkGray), - ))); - f.render_widget(footer, chunks[2]); + .split(f.area()); + + draw_header(f, app, layout[0]); + draw_summary(f, app, layout[1]); + + match app.selected_tab { + FocusTab::Overview => draw_overview(f, app, layout[2]), + FocusTab::Hosts => draw_hosts(f, app, layout[2]), + FocusTab::Connections => draw_connections(f, app, layout[2]), + FocusTab::Processes => draw_processes(f, app, layout[2]), + } + + let footer = Paragraph::new( + "/ switch tabs | pause | total/bandwidth | quit", + ) + .style(Style::default().fg(Color::DarkGray)); + f.render_widget(footer, layout[3]); } -fn draw_summary(f: &mut Frame, app: &mut App, area: Rect) { - // Draw network interface +fn draw_header(f: &mut Frame, app: &App, area: Rect) { + let tabs = Tabs::new( + FocusTab::ALL + .iter() + .map(|tab| Line::raw(tab.title())) + .collect::>(), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title(if app.should_pause { + format!("{} [PAUSED]", app.title) + } else { + app.title.clone() + }), + ) + .select( + FocusTab::ALL + .iter() + .position(|tab| *tab == app.selected_tab) + .unwrap_or(0), + ) + .highlight_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) + .divider(" | "); + f.render_widget(tabs, area); +} + +fn draw_summary(f: &mut Frame, app: &App, area: Rect) { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(area); - // Draw total ingress - let ingress_packets: String = if app.config.display.show_bandwidth { + let ingress_packets = if app.show_bandwidth { app.netstat_data.traffic.formatted_ingress_packets_per_sec() } else { app.netstat_data.traffic.packet_received.to_string() }; - let ingress_traffic: String = if app.config.display.show_bandwidth { + let ingress_bytes = if app.show_bandwidth { app.netstat_data.traffic.formatted_ingress_bytes_per_sec() } else { app.netstat_data.traffic.formatted_received_bytes() }; - let ingress_text = vec![ - text::Line::from(format!("Packets: {}", ingress_packets)), - text::Line::from(format!("Bytes: {}", ingress_traffic)), - ]; - let ingress_block = Block::default() - .borders(Borders::ALL) - .title("↓ Total Ingress"); - let ingress_paragraph = Paragraph::new(ingress_text) - .block(ingress_block) - .wrap(Wrap { trim: true }); - f.render_widget(ingress_paragraph, chunks[0]); - - // Draw total egress - let egress_packets: String = if app.config.display.show_bandwidth { + let egress_packets = if app.show_bandwidth { app.netstat_data.traffic.formatted_egress_packets_per_sec() } else { app.netstat_data.traffic.packet_sent.to_string() }; - let eggress_traffic: String = if app.config.display.show_bandwidth { + let egress_bytes = if app.show_bandwidth { app.netstat_data.traffic.formatted_egress_bytes_per_sec() } else { app.netstat_data.traffic.formatted_sent_bytes() }; - let eggress_text = vec![ - text::Line::from(format!("Packets: {}", egress_packets)), - text::Line::from(format!("Bytes: {}", eggress_traffic)), - ]; - let eggress_block = Block::default() - .borders(Borders::ALL) - .title("↑ Total Egress"); - let eggress_paragraph = Paragraph::new(eggress_text) - .block(eggress_block) - .wrap(Wrap { trim: true }); - f.render_widget(eggress_paragraph, chunks[1]); -} -fn draw_top_data(f: &mut Frame, app: &mut App, area: Rect) { - let area_chunks = Layout::default() - .constraints(vec![Constraint::Percentage(100)]) - .direction(Direction::Horizontal) - .split(area); - { - let inner_chunks = Layout::default() - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area_chunks[0]); + let ingress = Paragraph::new(vec![ + Line::raw(format!("Packets: {ingress_packets}")), + Line::raw(format!("Bytes: {ingress_bytes}")), + ]) + .block(Block::default().borders(Borders::ALL).title("Ingress")); - // Draw top Remote Address Table - let rows = app - .remote_hosts - .iter() - .take(app.config.display.top_remote_hosts) - .map(|host| { - let ingress_traffic: String = if app.config.display.show_bandwidth { - host.traffic.formatted_ingress_bytes_per_sec.clone() - } else { - host.traffic.formatted_received_bytes.clone() - }; - let egress_traffic: String = if app.config.display.show_bandwidth { - host.traffic.formatted_egress_bytes_per_sec.clone() - } else { - host.traffic.formatted_sent_bytes.clone() - }; - Row::new(vec![ - host.ip_addr.to_string(), - ingress_traffic, - egress_traffic, - host.country_code.clone(), - host.as_name.clone(), - host.hostname.clone(), - ]) - }) - .collect::>(); - let widths = [ - Constraint::Length(40), - Constraint::Length(11), - Constraint::Length(11), - Constraint::Length(8), - Constraint::Length(30), - Constraint::Length(40), - ]; + let egress = Paragraph::new(vec![ + Line::raw(format!("Packets: {egress_packets}")), + Line::raw(format!("Bytes: {egress_bytes}")), + ]) + .block(Block::default().borders(Borders::ALL).title("Egress")); - //let mut table_state = TableState::default(); - let table = Table::new(rows, widths) - .column_spacing(1) - .header( - Row::new(vec![ - "IP Address", - "↓ Bytes", - "↑ Bytes", - "Country", - "AS Name", - "Hostname", - ]) - .style(Style::new().bold()), //.bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Top Remote Addresses"), - ) - .highlight_style(Style::new().reversed()) - .highlight_symbol(">>"); + f.render_widget(ingress, chunks[0]); + f.render_widget(egress, chunks[1]); +} - f.render_widget(table, inner_chunks[0]); +fn draw_overview(f: &mut Frame, app: &App, area: Rect) { + draw_overview_flows(f, app, area); +} - let rows = app - .connections - .iter() - .take(app.config.display.connection_count) - .map(|conn| { - let remote_ip_string = if let Some(remote_ip_addr) = &conn.remote_ip_addr { - remote_ip_addr.to_string() - } else { - "".to_string() - }; - let remote_port_string = if let Some(remote_port) = &conn.remote_port { - remote_port.to_string() - } else { - "".to_string() - }; - let mut process_id_string = "".to_string(); - let mut process_name_string = "".to_string(); - if let Some(process) = &conn.process { - process_id_string = process.pid.to_string(); - process_name_string = process.name.clone(); - } - let ingress_traffic: String = if app.config.display.show_bandwidth { - conn.traffic.formatted_ingress_bytes_per_sec.clone() - } else { - conn.traffic.formatted_received_bytes.clone() - }; - let egress_traffic: String = if app.config.display.show_bandwidth { - conn.traffic.formatted_egress_bytes_per_sec.clone() - } else { - conn.traffic.formatted_sent_bytes.clone() - }; - Row::new(vec![ - conn.protocol.as_str().to_string(), - format!( - "{}:{}", - conn.local_ip_addr.to_string(), - conn.local_port.to_string() - ), - format!("{}:{}", remote_ip_string, remote_port_string), - ingress_traffic, - egress_traffic, - process_id_string, - process_name_string, - ]) - }) - .collect::>(); - let widths = [ +fn draw_hosts(f: &mut Frame, app: &App, area: Rect) { + draw_top_hosts(f, app, area, None); +} + +fn draw_connections(f: &mut Frame, app: &App, area: Rect) { + draw_top_connections(f, app, area, None); +} + +fn draw_processes(f: &mut Frame, app: &App, area: Rect) { + let rows = app.processes.iter().map(|process| { + let ingress = if app.show_bandwidth { + process.traffic.formatted_ingress_bytes_per_sec.clone() + } else { + process.traffic.formatted_received_bytes.clone() + }; + let egress = if app.show_bandwidth { + process.traffic.formatted_egress_bytes_per_sec.clone() + } else { + process.traffic.formatted_sent_bytes.clone() + }; + Row::new(vec![ + process.pid.to_string(), + process.name.clone(), + ingress, + egress, + ]) + }); + let table = Table::new( + rows, + [ Constraint::Length(8), - Constraint::Length(46), - Constraint::Length(46), - Constraint::Length(11), - Constraint::Length(11), - Constraint::Length(5), - Constraint::Length(20), - ]; - let table = Table::new(rows, widths) - .column_spacing(1) - .header( - Row::new(vec![ - "Protocol", - "Local Socket", - "Remote Socket", - "↓ Bytes", - "↑ Bytes", - "PID", - "Process Name", - ]) - .style(Style::new().bold()), //.bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Top Connections"), - ) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) - .highlight_symbol(">>"); - f.render_widget(table, inner_chunks[1]); - //f.render_stateful_widget(table, inner_chunks[1], &mut app.talbe_state); - } + Constraint::Min(20), + Constraint::Length(14), + Constraint::Length(14), + ], + ) + .header( + Row::new(["PID", "Process", "Ingress", "Egress"]) + .style(Style::default().add_modifier(Modifier::BOLD)), + ) + .block(Block::default().borders(Borders::ALL).title("Processes")) + .column_spacing(1); + f.render_widget(table, area); } -fn draw_remotehosts_table(f: &mut Frame, app: &mut App, area: Rect) { - // Draw top Remote Address Table +fn draw_top_hosts(f: &mut Frame, app: &App, area: Rect, limit: Option) { let rows = app .remote_hosts .iter() + .take(limit.unwrap_or(app.remote_hosts.len())) .map(|host| { - let ingress_traffic: String = if app.config.display.show_bandwidth { + let ingress = if app.show_bandwidth { host.traffic.formatted_ingress_bytes_per_sec.clone() } else { host.traffic.formatted_received_bytes.clone() }; - let egress_traffic: String = if app.config.display.show_bandwidth { + let egress = if app.show_bandwidth { host.traffic.formatted_egress_bytes_per_sec.clone() } else { host.traffic.formatted_sent_bytes.clone() }; Row::new(vec![ host.ip_addr.to_string(), - ingress_traffic, - egress_traffic, - host.country_code.clone(), - host.as_name.clone(), + ingress, + egress, host.hostname.clone(), ]) - }) - .collect::>(); - let widths = [ - Constraint::Length(40), - Constraint::Length(11), - Constraint::Length(11), - Constraint::Length(8), - Constraint::Length(30), - Constraint::Length(40), - ]; - - //let mut table_state = TableState::default(); - let table = Table::new(rows, widths) - .column_spacing(1) - //.style(Style::new().blue()) - .header( - Row::new(vec![ - "IP Address", - "↓ Bytes", - "↑ Bytes", - "Country", - "AS Name", - "Hostname", - ]) - .style(Style::new().bold()), //.bottom_margin(1), - ) - .block( - Block::default() - .borders(Borders::ALL) - .title("Remote Addresses"), - ) - .highlight_style(Style::new().reversed()) - .highlight_symbol(">>"); + }); - //f.render_widget(table, area); - f.render_stateful_widget(table, area, &mut app.talbe_state); + let table = Table::new( + rows, + [ + Constraint::Length(40), + Constraint::Length(14), + Constraint::Length(14), + Constraint::Min(16), + ], + ) + .header( + Row::new(["IP", "Ingress", "Egress", "Hostname"]) + .style(Style::default().add_modifier(Modifier::BOLD)), + ) + .block(Block::default().borders(Borders::ALL).title("Remote Hosts")) + .column_spacing(1); + f.render_widget(table, area); } -fn draw_connection_table(f: &mut Frame, app: &mut App, area: Rect) { +fn draw_top_connections(f: &mut Frame, app: &App, area: Rect, limit: Option) { let rows = app .connections .iter() + .take(limit.unwrap_or(app.connections.len())) .map(|conn| { - let remote_ip_string = if let Some(remote_ip_addr) = &conn.remote_ip_addr { - remote_ip_addr.to_string() + let remote_ip = conn + .remote_ip_addr + .map(|ip| ip.to_string()) + .unwrap_or_else(|| "-".to_string()); + let remote_port = conn + .remote_port + .map(|port| port.to_string()) + .unwrap_or_else(|| "-".to_string()); + let ingress = if app.show_bandwidth { + conn.traffic.formatted_ingress_bytes_per_sec.clone() } else { - "".to_string() + conn.traffic.formatted_received_bytes.clone() }; - let remote_port_string = if let Some(remote_port) = &conn.remote_port { - remote_port.to_string() + let egress = if app.show_bandwidth { + conn.traffic.formatted_egress_bytes_per_sec.clone() } else { - "".to_string() + conn.traffic.formatted_sent_bytes.clone() }; - let mut process_id_string = "".to_string(); - let mut process_name_string = "".to_string(); - if let Some(process) = &conn.process { - process_id_string = process.pid.to_string(); - process_name_string = process.name.clone(); + let process_name = conn + .process + .as_ref() + .map(|process| process.name.clone()) + .unwrap_or_else(|| "-".to_string()); + Row::new(vec![ + conn.protocol.as_str().to_string(), + format!("{}:{}", conn.local_ip_addr, conn.local_port), + format!("{remote_ip}:{remote_port}"), + ingress, + egress, + process_name, + ]) + }); + + let table = Table::new( + rows, + [ + Constraint::Length(6), + Constraint::Length(32), + Constraint::Length(32), + Constraint::Length(14), + Constraint::Length(14), + Constraint::Min(16), + ], + ) + .header( + Row::new(["Proto", "Local", "Remote", "Ingress", "Egress", "Process"]) + .style(Style::default().add_modifier(Modifier::BOLD)), + ) + .block(Block::default().borders(Borders::ALL).title("Connections")) + .column_spacing(1); + f.render_widget(table, area); +} + +fn draw_overview_flows(f: &mut Frame, app: &App, area: Rect) { + let hostnames: HashMap<_, _> = app + .remote_hosts + .iter() + .filter_map(|host| { + if host.hostname.is_empty() { + None + } else { + Some((host.ip_addr, host.hostname.as_str())) } - let ingress_traffic: String = if app.config.display.show_bandwidth { + }) + .collect(); + + let rows = app + .connections + .iter() + .take(app.config.display.connection_count) + .map(|conn| { + let remote_ip = conn + .remote_ip_addr + .map(|ip| ip.to_string()) + .unwrap_or_else(|| "-".to_string()); + let remote_port = conn + .remote_port + .map(|port| port.to_string()) + .unwrap_or_else(|| "-".to_string()); + let ingress = if app.show_bandwidth { conn.traffic.formatted_ingress_bytes_per_sec.clone() } else { conn.traffic.formatted_received_bytes.clone() }; - let egress_traffic: String = if app.config.display.show_bandwidth { + let egress = if app.show_bandwidth { conn.traffic.formatted_egress_bytes_per_sec.clone() } else { conn.traffic.formatted_sent_bytes.clone() }; + let hostname = conn + .remote_ip_addr + .and_then(|ip| hostnames.get(&ip).copied()) + .unwrap_or("-"); + let process = conn + .process + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or("-"); + Row::new(vec![ - conn.protocol.as_str().to_string(), - format!( - "{}:{}", - conn.local_ip_addr.to_string(), - conn.local_port.to_string() - ), - format!("{}:{}", remote_ip_string, remote_port_string), - ingress_traffic, - egress_traffic, - process_id_string, - process_name_string, - ]) - }) - .collect::>(); - let widths = [ - Constraint::Length(8), - Constraint::Length(46), - Constraint::Length(46), - Constraint::Length(11), - Constraint::Length(11), - Constraint::Length(5), - Constraint::Length(20), - ]; - let table = Table::new(rows, widths) - .column_spacing(1) - .header( - Row::new(vec![ - "Protocol", - "Local Socket", - "Remote Socket", - "↓ Bytes", - "↑ Bytes", - "PID", - "Process Name", + format!("{}:{}", conn.local_ip_addr, conn.local_port), + format!("{remote_ip}:{remote_port}"), + ingress, + egress, + hostname.to_string(), + process.to_string(), ]) - .style(Style::new().bold()), //.bottom_margin(1), - ) - .block(Block::default().borders(Borders::ALL).title("Connections")) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) - .highlight_symbol(">>"); - //f.render_widget(table, area); - f.render_stateful_widget(table, area, &mut app.talbe_state); -} - -fn draw_overview_tab(f: &mut Frame, app: &mut App, area: Rect) { - let chunks = Layout::default() - .constraints([Constraint::Length(4), Constraint::Min(8)]) - .split(area); - draw_summary(f, app, chunks[0]); - draw_top_data(f, app, chunks[1]); -} + }); -fn draw_remotehosts_tab(f: &mut Frame, app: &mut App, area: Rect) { - let chunks = Layout::default() - .constraints(vec![Constraint::Percentage(100)]) - .split(area); - draw_remotehosts_table(f, app, chunks[0]); -} + let table = Table::new( + rows, + [ + Constraint::Length(31), + Constraint::Length(31), + Constraint::Length(14), + Constraint::Length(14), + Constraint::Min(20), + Constraint::Min(16), + ], + ) + .header( + Row::new([ + "Local Address", + "Remote Address", + "Ingress", + "Egress", + "Hostname", + "Process", + ]) + .style(Style::default().add_modifier(Modifier::BOLD)), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title("Overview Connections"), + ) + .column_spacing(1); -fn draw_connections_tab(f: &mut Frame, app: &mut App, area: Rect) { - let chunks = Layout::default() - .constraints(vec![Constraint::Percentage(100)]) - .split(area); - draw_connection_table(f, app, chunks[0]); + f.render_widget(table, area); } diff --git a/src/util/tree.rs b/src/util/tree.rs index f4ca737..197db92 100644 --- a/src/util/tree.rs +++ b/src/util/tree.rs @@ -1,10 +1,7 @@ pub fn node_label(label: &str, value: Option<&str>, delimiter: Option<&str>) -> String { match value { Some(value) => { - let delimiter = match delimiter { - Some(delimiter) => delimiter, - None => ":", - }; + let delimiter = delimiter.unwrap_or(":"); format!("{}{} {}", label, delimiter, value) } None => label.to_string(),