commit bf180f2b9babef3437ae3571532f86b6c2199ad8 Author: Eric Coissac Date: Sat Feb 28 14:54:48 2026 +0100 Initial commit with core utilities Add core utilities for IP address handling, process information, and OS detection - Introduce `guess_local_ip` function to determine local IP address - Implement `list_all_ips` to list all non-loopback IP addresses - Add `find_process_using_port` to identify processes using specific ports - Include OS information utility with `get_os_string` - Setup project structure with Cargo.toml, .gitignore, and documentation - Add comprehensive tests for all utility functions - Configure code style guidelines in AGENTS.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d913617 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aa0d012 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ +# pmoutils - Agent Guidelines + +## Build & Test Commands + +```bash +# Build the library +cargo build + +# Run all tests (unit + doctests) +cargo test + +# Run a single unit test +cargo test --lib ip_utils::tests::test_guess_local_ip + +# Run tests for a specific module +cargo test --lib ip_utils::tests + +# Run only doctests +cargo test --doc + +# Lint with clippy +cargo clippy --all-targets -- -D warnings + +# Format code +cargo fmt +``` + +## Code Style Guidelines + +### General +- Rust 2024 edition +- Follow official Rust style guide and Clippy recommendations +- All code must be documented with Rustdoc comments +- Prefer `&str` over `String` for function parameters when possible + +### Imports +- Group imports by source: std → external crates → crate modules +- Use `use` statements at the top of each module +- Import specific items, not glob imports (`use std::net::IpAddr`, not `use std::net::*`) + +### Naming Conventions +- Types: `PascalCase` (e.g., `ProcessPortInfo`, `TransportProtocol`) +- Functions/methods: `snake_case` (e.g., `guess_local_ip`, `find_process_using_port`) +- Constants: `SCREAMING_SNAKE_CASE` +- Traits: prefix with `To` or descriptive noun (e.g., `ToXmlElement`) +- Tests: `test_functionality_what_it_does` format + +### Error Handling +- Use `Option` for operations that may not return a value (e.g., `find_process_using_port` returns `Option`) +- Use `?` operator for propagating errors +- Return default values (like `"127.0.0.1"`) only when appropriate fallback exists +- Document error conditions in function documentation + +### Types +- Prefer concrete types over generics unless abstraction is needed +- Use `u32` for PIDs, `u16` for ports +- Use `String` for process names and owners (owned, mutable) +- Derive `Debug` and `Clone` for data structures + +### Documentation +- All public items require Rustdoc comments +- Include `# Examples` section when useful +- Document return values and error conditions +- Use triple slashes `///` for module-level documentation + +### Formatting +- Run `cargo fmt` before committing +- Use 4 spaces for indentation (default Rust formatter) +- Keep lines under 100 characters when possible + +### Testing +- Unit tests in same file as code (in `mod tests {}` block) +- Test names follow pattern: `test__` +- Include tests for edge cases (e.g., fallback to localhost) +- Verify return value formats and constraints +- Test filtering: `cargo test --lib ip_utils::tests::test_list_all_ips_no_loopback` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a1726d5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,934 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[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", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[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 = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "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 = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-sock-diag" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a495cb1de50560a7cd12fdcf023db70eec00e340df81be31cedbbfd4aadd6b76" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", + "smallvec", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "netstat2" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496f264d3ead4870d6b366deb9d20597592d64aac2a907f3e7d07c2325ba4663" +dependencies = [ + "bindgen", + "bitflags 2.11.0", + "byteorder", + "netlink-packet-core", + "netlink-packet-sock-diag", + "netlink-packet-utils", + "netlink-sys", + "num-derive", + "num-traits", + "thiserror 2.0.18", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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 = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[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", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pmoutils" +version = "0.1.0" +dependencies = [ + "get_if_addrs", + "netstat2", + "os_info", + "quick-xml", + "sysinfo", + "users", + "xmltree", +] + +[[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.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ab3f283 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pmoutils" +version = "0.1.0" +edition = "2024" + +[dependencies] +get_if_addrs = "0.5.3" +os_info = "3.8" +netstat2 = "0.11" +sysinfo = "0.30" +users = "0.11" +quick-xml = "0.37" +xmltree = "0.10" diff --git a/src/ip_utils.rs b/src/ip_utils.rs new file mode 100644 index 0000000..c8ab423 --- /dev/null +++ b/src/ip_utils.rs @@ -0,0 +1,273 @@ +use get_if_addrs::get_if_addrs; +use std::net::UdpSocket; + +/// Devine l'adresse IP locale de la machine. +/// +/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP +/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface +/// réseau qui serait utilisée pour communiquer avec Internet. +/// +/// # Fonctionnement +/// +/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire) +/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80` +/// 3. Récupère l'adresse IP locale du socket +/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1` +/// +/// # Returns +/// +/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur. +/// +/// # Examples +/// +/// ``` +/// use pmoutils::guess_local_ip; +/// +/// let ip = guess_local_ip(); +/// println!("IP locale détectée: {}", ip); +/// // Affiche par exemple: "IP locale détectée: 192.168.1.42" +/// ``` +/// +/// # Note +/// +/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion), +/// elle demande simplement au système d'exploitation quelle interface serait utilisée +/// pour joindre l'adresse cible. +pub fn guess_local_ip() -> String { + match UdpSocket::bind("0.0.0.0:0") { + Ok(socket) => { + if socket.connect("8.8.8.8:80").is_ok() { + if let Ok(local_addr) = socket.local_addr() { + return local_addr.ip().to_string(); + } + } + "127.0.0.1".to_string() + } + Err(_) => "127.0.0.1".to_string(), + } +} + +/// Liste toutes les adresses IP non-loopback des interfaces réseau. +/// +/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4, +/// en excluant les adresses de loopback (127.0.0.1). +/// +/// # Returns +/// +/// Retourne une `HashMap` où : +/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`) +/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface +/// +/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap +/// contenant une entrée `"error"` avec un message d'erreur. +/// +/// # Examples +/// +/// ``` +/// use pmoutils::ip_utils::list_all_ips; +/// +/// let ips = list_all_ips(); +/// for (interface, addresses) in ips { +/// println!("Interface {}: {:?}", interface, addresses); +/// } +/// // Affiche par exemple: +/// // Interface eth0: ["192.168.1.42"] +/// // Interface wlan0: ["10.0.0.15"] +/// ``` +/// +/// # Note +/// +/// - Seules les adresses IPv4 sont retournées +/// - Les adresses de loopback (127.x.x.x) sont filtrées +/// - Les adresses IPv6 sont ignorées +pub fn list_all_ips() -> std::collections::HashMap> { + let mut result = std::collections::HashMap::new(); + + if let Ok(interfaces) = get_if_addrs() { + for iface in interfaces { + let ip = iface.ip(); + if ip.is_loopback() { + continue; + } + if ip.is_ipv4() { + result + .entry(iface.name) + .or_insert_with(Vec::new) + .push(ip.to_string()); + } + } + } else { + result.insert( + "error".to_string(), + vec!["Failed to get interfaces".to_string()], + ); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::IpAddr; + + #[test] + fn test_guess_local_ip_returns_valid_ip() { + let ip = guess_local_ip(); + + // Vérifie que le résultat est parsable comme une IP + assert!( + ip.parse::().is_ok(), + "Should return a valid IP address" + ); + } + + #[test] + fn test_guess_local_ip_not_empty() { + let ip = guess_local_ip(); + + assert!(!ip.is_empty(), "IP should not be empty"); + } + + #[test] + fn test_guess_local_ip_is_ipv4() { + let ip = guess_local_ip(); + + if let Ok(parsed_ip) = ip.parse::() { + assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address"); + } + } + + #[test] + fn test_guess_local_ip_fallback_is_localhost() { + // Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1 + // (difficile à tester sans mocker, mais on vérifie la cohérence) + let ip = guess_local_ip(); + let parsed = ip.parse::().unwrap(); + + // L'IP doit être soit locale (127.0.0.1) soit une IP privée valide + assert!( + parsed.is_loopback() || is_private_ip(&ip), + "IP should be either loopback or private" + ); + } + + #[test] + fn test_list_all_ips_no_loopback() { + let ips = list_all_ips(); + + // Vérifie qu'aucune adresse de loopback n'est présente + for (_, addresses) in ips.iter() { + for addr in addresses { + if let Ok(parsed_ip) = addr.parse::() { + assert!( + !parsed_ip.is_loopback(), + "Loopback addresses should be filtered out" + ); + } + } + } + } + + #[test] + fn test_list_all_ips_only_ipv4() { + let ips = list_all_ips(); + + // Vérifie que seules des adresses IPv4 sont retournées + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; // Skip error entries + } + + for addr in addresses { + if let Ok(parsed_ip) = addr.parse::() { + assert!( + parsed_ip.is_ipv4(), + "Only IPv4 addresses should be returned" + ); + } + } + } + } + + #[test] + fn test_list_all_ips_valid_format() { + let ips = list_all_ips(); + + // Vérifie que toutes les IPs sont dans un format valide + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; + } + + for addr in addresses { + assert!( + addr.parse::().is_ok(), + "Each IP should be in valid format: {}", + addr + ); + } + } + } + + #[test] + fn test_list_all_ips_interface_names_not_empty() { + let ips = list_all_ips(); + + // Vérifie que les noms d'interface ne sont pas vides + for (iface_name, _) in ips.iter() { + assert!( + !iface_name.is_empty(), + "Interface names should not be empty" + ); + } + } + + #[test] + fn test_list_all_ips_no_duplicate_ips_per_interface() { + let ips = list_all_ips(); + + // Vérifie qu'il n'y a pas de doublons par interface + for (iface_name, addresses) in ips.iter() { + if iface_name == "error" { + continue; + } + + let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect(); + assert_eq!( + addresses.len(), + unique_addresses.len(), + "No duplicate IPs should exist for interface {}", + iface_name + ); + } + } + + // Fonction helper pour les tests + fn is_private_ip(ip_str: &str) -> bool { + if let Ok(ip) = ip_str.parse::() { + match ip { + IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + // Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + octets[0] == 10 + || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) + || (octets[0] == 192 && octets[1] == 168) + } + IpAddr::V6(_) => false, + } + } else { + false + } + } + + #[test] + fn test_helper_is_private_ip() { + // Tests pour la fonction helper + assert!(is_private_ip("10.0.0.1")); + assert!(is_private_ip("172.16.0.1")); + assert!(is_private_ip("192.168.1.1")); + assert!(!is_private_ip("8.8.8.8")); + assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0e0a027 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,76 @@ +/// Utilitaires pour la gestion des adresses IP réseau. +/// +/// Ce module fournit des fonctions pour détecter et lister les adresses IP +/// des interfaces réseau locales de la machine. +/// +/// # Fonctions principales +/// +/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes +/// +/// # Examples +/// +/// ``` +/// use pmoutils::guess_local_ip; +/// +/// let ip = guess_local_ip(); +/// println!("Adresse IP locale: {}", ip); +/// ``` +pub mod ip_utils; + +pub use ip_utils::guess_local_ip; +pub mod process; +pub use process::{ProcessPortInfo, TransportProtocol, find_process_using_port}; +use xmltree::{Element, EmitterConfig}; + +/// Retourne une chaîne décrivant le système d'exploitation et sa version. +/// +/// Utilise la crate `os_info` pour obtenir de manière portable et fiable +/// les informations sur le système d'exploitation courant. +/// +/// # Format +/// - macOS: "macOS/15.1" ou "Mac OS/10.15.7" +/// - Linux: "Linux/6.5.0" ou "Ubuntu/22.04" +/// - Windows: "Windows/10.0.19045" +/// - Autre: "{OS}/Unknown" +/// +/// # Exemples +/// +/// ``` +/// use pmoutils::get_os_string; +/// +/// let os = get_os_string(); +/// println!("OS: {}", os); // Ex: "Linux/6.5.0" +/// ``` +pub fn get_os_string() -> String { + let info = os_info::get(); + let os_type = format!("{:?}", info.os_type()); + + // Obtenir la version si disponible + let version = info.version(); + if version != &os_info::Version::Unknown { + format!("{}/{}", os_type, version) + } else { + format!("{}/Unknown", os_type) + } +} + +/// Trait générique pour obtenir un élément XML (xmltree::Element). +/// +/// Aligné sur la signature utilisée dans pmoupnp (UpnpObject::to_xml_element), +/// afin de pouvoir factoriser la sérialisation XML entre crates. +pub trait ToXmlElement { + /// Convertit l'objet en élément XML. + fn to_xml_element(&self) -> Element; + + /// Sérialise en chaîne XML formatée. + fn to_xml(&self) -> String { + let elem = self.to_xml_element(); + let config = EmitterConfig::new() + .perform_indent(true) + .indent_string(" "); + let mut buf = Vec::new(); + elem.write_with_config(&mut buf, config) + .expect("Failed to write XML"); + String::from_utf8(buf).expect("Invalid UTF-8") + } +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..37b46f7 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,88 @@ +use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo, get_sockets_info}; +use sysinfo::{Pid, System}; + +/// Informations sur un processus utilisant un port réseau. +#[derive(Debug, Clone)] +pub struct ProcessPortInfo { + pub pid: u32, + pub process_name: String, + pub owner: String, + pub port: u16, +} + +/// Protocole de transport utilisé pour la recherche. +#[derive(Debug, Clone, Copy)] +pub enum TransportProtocol { + Tcp, + Udp, +} + +/// Tente de trouver le processus qui écoute sur `port` pour le protocole donné. +/// +/// Retourne `Some(ProcessPortInfo)` si un processus a pu être identifié, sinon `None`. +pub fn find_process_using_port(port: u16, protocol: TransportProtocol) -> Option { + let proto_flag = match protocol { + TransportProtocol::Tcp => ProtocolFlags::TCP, + TransportProtocol::Udp => ProtocolFlags::UDP, + }; + + let sockets = get_sockets_info( + AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6, + proto_flag, + ) + .ok()?; + + // Préparer l'inspection des processus. + let mut system = System::new_all(); + system.refresh_all(); + + for socket in sockets { + match socket.protocol_socket_info { + ProtocolSocketInfo::Tcp(ref tcp_info) + if matches!(protocol, TransportProtocol::Tcp) && tcp_info.local_port == port => + { + if let Some(info) = + build_process_info(&mut system, port, socket.associated_pids.first()) + { + return Some(info); + } + } + ProtocolSocketInfo::Udp(ref udp_info) + if matches!(protocol, TransportProtocol::Udp) && udp_info.local_port == port => + { + if let Some(info) = + build_process_info(&mut system, port, socket.associated_pids.first()) + { + return Some(info); + } + } + _ => continue, + } + } + + None +} + +fn build_process_info( + system: &mut System, + port: u16, + pid_opt: Option<&u32>, +) -> Option { + let pid = *pid_opt?; + let process = system.process(Pid::from_u32(pid))?; + let process_name = process.name().to_string(); + + let owner = process + .user_id() + .and_then(|uid| { + users::get_user_by_uid(**uid).map(|user| user.name().to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "unknown".to_string()); + + Some(ProcessPortInfo { + pid, + process_name, + owner, + port, + }) +}