diff --git a/Cargo.lock b/Cargo.lock index fe7c03f8..e75089a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4047,7 +4047,6 @@ version = "0.1.0" dependencies = [ "bevy_reflect", "bevy_reflect_derive", - "pmoutils", "quick-xml", "serde", "utoipa", @@ -4362,15 +4361,15 @@ dependencies = [ [[package]] name = "pmoutils" -version = "0.1.0" +version = "0.1.2" +source = "sparse+https://gargoton.petite-maison-orange.fr/api/packages/pmo/cargo/" +checksum = "9ed68fb06ffdd1a30fc7792d883dcd6d61ad7705c3a744f54a0a5bcc341fc65e" dependencies = [ "get_if_addrs", "netstat2", "os_info", - "quick-xml", "sysinfo", "users", - "xmltree 0.10.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fd97a8da..135dbd7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "pmowebrenderer", "pmomediaserver", "pmoconfig", - "pmoutils", "pmodidl", "pmoserver", "pmoapp", diff --git a/pmoconfig/Cargo.toml b/pmoconfig/Cargo.toml index 81f85369..3a3790ad 100644 --- a/pmoconfig/Cargo.toml +++ b/pmoconfig/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" edition = "2021" [dependencies] -pmoutils ={ path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } serde = { workspace = true } serde_yaml = { workspace = true } diff --git a/pmocontrol/Cargo.toml b/pmocontrol/Cargo.toml index e91fb791..7b062861 100644 --- a/pmocontrol/Cargo.toml +++ b/pmocontrol/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] pmoupnp = { path = "../pmoupnp" } pmodidl = { path = "../pmodidl" } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } quick-xml = { workspace = true } thiserror = { workspace = true } ureq = "3.1.4" diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index efc929ed..0dfa758b 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -1562,7 +1562,7 @@ pub(crate) fn build_didl_lite_metadata( protocol_info: &str, ) -> String { use pmodidl::{DIDLLite, Item, Resource}; - use pmoutils::ToXmlElement; + use pmodidl::ToXmlElement; // Construire l'Item DIDL avec toutes les métadonnées let item = Item { diff --git a/pmodidl/Cargo.toml b/pmodidl/Cargo.toml index baf5a6bc..99f0ca59 100644 --- a/pmodidl/Cargo.toml +++ b/pmodidl/Cargo.toml @@ -10,5 +10,4 @@ utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } quick-xml = { workspace = true } bevy_reflect = "0.17.1" bevy_reflect_derive = "0.17.1" -pmoutils = { path = "../pmoutils" } xmltree = "0.10" diff --git a/pmodidl/src/lib.rs b/pmodidl/src/lib.rs index 3779add2..0f6af2c6 100644 --- a/pmodidl/src/lib.rs +++ b/pmodidl/src/lib.rs @@ -3,14 +3,31 @@ //! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. use bevy_reflect::Reflect; -use pmoutils::ToXmlElement; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashSet; use std::fmt::Write; use std::io::Cursor; use std::time::SystemTime; -use xmltree::{Element, XMLNode}; +use xmltree::{Element, EmitterConfig, XMLNode}; + +/// Trait générique pour obtenir un élément XML (xmltree::Element). +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") + } +} // ============= Couche d'abstraction générique ============= diff --git a/pmomediaserver/Cargo.toml b/pmomediaserver/Cargo.toml index 27f46c4b..72dc6a97 100644 --- a/pmomediaserver/Cargo.toml +++ b/pmomediaserver/Cargo.toml @@ -18,7 +18,7 @@ quick-xml = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } # Optional dependencies axum = { version = "0.8", optional = true } diff --git a/pmomediaserver/src/content_handler.rs b/pmomediaserver/src/content_handler.rs index 652d241f..80ebffec 100644 --- a/pmomediaserver/src/content_handler.rs +++ b/pmomediaserver/src/content_handler.rs @@ -13,7 +13,7 @@ use pmodidl::{Container, DIDLLite}; use pmosource::api::{get_source as get_source_from_registry, list_all_sources}; use pmosource::{BrowseResult, MusicSource, MusicSourceError}; -use pmoutils::ToXmlElement; +use pmodidl::ToXmlElement; use std::collections::HashSet; use std::sync::Arc; diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index ab545720..844d6dd6 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] pmoconfig = { path = "../pmoconfig" } pmodidl = { path = "../pmodidl"} -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } pmoserver = { path = "../pmoserver" } pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"] } diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index 467755b8..8baeb70e 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -430,7 +430,7 @@ impl UpnpServerExt for Server { let kind = e.kind(); if kind == std::io::ErrorKind::AddrInUse { let port = crate::ssdp::SSDP_PORT; - if let Some(process) = find_process_using_port(port, TransportProtocol::Udp) { + if let Some(process) = find_process_using_port(TransportProtocol::Udp, port) { error!( "❌ SSDP initialization failed: port {} is already in use by \ PID {} ({}) owned by {}: {}", diff --git a/pmoutils/Cargo.toml b/pmoutils/Cargo.toml deleted file mode 100644 index dc3e22e3..00000000 --- a/pmoutils/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[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 = { workspace = true } -xmltree = "0.10" diff --git a/pmoutils/Cargo.toml.backup b/pmoutils/Cargo.toml.backup deleted file mode 100644 index 49e86a59..00000000 --- a/pmoutils/Cargo.toml.backup +++ /dev/null @@ -1,13 +0,0 @@ -[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.38.3" -xmltree = "0.10" diff --git a/pmoutils/src/ip_utils.rs b/pmoutils/src/ip_utils.rs deleted file mode 100644 index c8ab423f..00000000 --- a/pmoutils/src/ip_utils.rs +++ /dev/null @@ -1,273 +0,0 @@ -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/pmoutils/src/lib.rs b/pmoutils/src/lib.rs deleted file mode 100644 index 0e0a0279..00000000 --- a/pmoutils/src/lib.rs +++ /dev/null @@ -1,76 +0,0 @@ -/// 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/pmoutils/src/process.rs b/pmoutils/src/process.rs deleted file mode 100644 index 37b46f75..00000000 --- a/pmoutils/src/process.rs +++ /dev/null @@ -1,88 +0,0 @@ -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, - }) -} diff --git a/pmowebrenderer/Cargo.toml b/pmowebrenderer/Cargo.toml index 6809822b..71df801d 100644 --- a/pmowebrenderer/Cargo.toml +++ b/pmowebrenderer/Cargo.toml @@ -40,7 +40,7 @@ thiserror = { workspace = true } tracing = { workspace = true } pmodidl = { path = "../pmodidl" } -pmoutils = { path = "../pmoutils" } +pmoutils = { version = "0.1.2", registry = "pmo" } [features] default = [] diff --git a/pmowebrenderer/src/handlers.rs b/pmowebrenderer/src/handlers.rs index 48fd08d2..c29d8152 100644 --- a/pmowebrenderer/src/handlers.rs +++ b/pmowebrenderer/src/handlers.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use pmodidl::DIDLLite; use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value}; use pmoupnp::{get, set}; -use pmoutils::ToXmlElement; +use pmodidl::ToXmlElement; use crate::messages::PlaybackState; use crate::pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds};