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
This commit is contained in:
2026-02-28 14:54:48 +01:00
commit bf180f2b9b
7 changed files with 1462 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
target

76
AGENTS.md Normal file
View File

@@ -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<ProcessPortInfo>`)
- 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_<function>_<condition>`
- 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`

934
Cargo.lock generated Normal file
View File

@@ -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",
]

13
Cargo.toml Normal file
View File

@@ -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"

273
src/ip_utils.rs Normal file
View File

@@ -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<String, Vec<String>> {
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::<IpAddr>().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::<IpAddr>() {
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::<IpAddr>().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::<IpAddr>() {
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::<IpAddr>() {
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::<IpAddr>().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::<IpAddr>() {
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
}
}

76
src/lib.rs Normal file
View File

@@ -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")
}
}

88
src/process.rs Normal file
View File

@@ -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<ProcessPortInfo> {
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<ProcessPortInfo> {
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,
})
}