From a17b77a84081d4c100c2b598aef989f67860228f Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 11 Oct 2025 10:08:28 +0200 Subject: [PATCH] Ajoute la notion de reflexive value au statevariableinstance --- Cargo.lock | 15 +- pmoupnp/Cargo.toml | 1 + pmoupnp/src/services/service_instance.rs | 238 +++++++++++++++++- .../src/state_variables/instance_methods.rs | 79 +++++- pmoupnp/src/state_variables/mod.rs | 2 + pmoupnp/src/variable_types/mod.rs | 1 + pmoupnp/src/variable_types/reflect_impl.rs | 44 ++++ 7 files changed, 370 insertions(+), 10 deletions(-) create mode 100644 pmoupnp/src/variable_types/reflect_impl.rs diff --git a/Cargo.lock b/Cargo.lock index 657289ca..0844c37f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1958,7 +1958,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", "indexmap", - "quick-xml", + "quick-xml 0.38.3", "serde", "time", ] @@ -2022,7 +2022,7 @@ version = "0.1.0" dependencies = [ "bevy_reflect", "bevy_reflect_derive", - "quick-xml", + "quick-xml 0.38.3", "serde", "utoipa", "utoipa-swagger-ui", @@ -2066,6 +2066,7 @@ dependencies = [ "pmodidl", "pmoserver", "pmoutils", + "quick-xml 0.37.5", "reqwest", "serde", "serde_json", @@ -2189,6 +2190,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.38.3" diff --git a/pmoupnp/Cargo.toml b/pmoupnp/Cargo.toml index b6b761d3..ec4eef6b 100644 --- a/pmoupnp/Cargo.toml +++ b/pmoupnp/Cargo.toml @@ -19,6 +19,7 @@ axum = "0.8.4" tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +quick-xml = { version = "0.37.0", features = ["serialize"] } chrono = { version = "0.4.42", features = ["serde"] } once_cell = "1.20" parking_lot = "0.12" diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs index 19093c2e..f28763af 100644 --- a/pmoupnp/src/services/service_instance.rs +++ b/pmoupnp/src/services/service_instance.rs @@ -34,6 +34,7 @@ use axum::{ http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, }; +use bevy_reflect::Reflect; use std::{ collections::HashMap, future::Future, @@ -44,6 +45,7 @@ use std::{ use tokio::time; use tracing::{debug, error, info, warn}; use xmltree::{Element, EmitterConfig, XMLNode}; +use quick_xml::escape::escape; use crate::{ UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance, @@ -118,8 +120,8 @@ pub struct ServiceInstance { /// Abonnés aux événements (SID -> Callback URL) subscribers: Arc>>, - /// Buffer des changements en attente de notification - changed_buffer: Arc>>, + /// Buffer des changements en attente de notification (nom de variable -> valeur réflexive) + changed_buffer: Arc>>>, /// Compteurs de séquence par abonné seqid: Arc>>, @@ -849,14 +851,15 @@ impl ServiceInstance { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// # use pmoupnp::services::Service; /// # use pmoupnp::UpnpModel; /// let service = Service::new("AVTransport".to_string()); /// let instance = service.create_instance(); - /// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string()); + /// let value = Arc::new("PLAYING".to_string()) as Arc; + /// instance.event_to_be_sent("TransportState".to_string(), value); /// ``` - pub fn event_to_be_sent(&self, name: String, value: String) { + pub fn event_to_be_sent(&self, name: String, value: Arc) { let mut buffer = self.changed_buffer.lock().unwrap(); buffer.insert(name, value); } @@ -895,7 +898,10 @@ impl ServiceInstance { /// # async fn main() { /// # let service = Service::new("AVTransport".to_string()); /// # let instance = service.create_instance(); - /// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string()); + /// # use std::sync::Arc; + /// # use bevy_reflect::Reflect; + /// let value = Arc::new("PLAYING".to_string()) as Arc; + /// instance.event_to_be_sent("TransportState".to_string(), value); /// instance.notify_subscribers().await; /// # } /// ``` @@ -926,9 +932,11 @@ impl ServiceInstance { let mut body = r#""#.to_string(); for (name, val) in changed_clone { + // Convertir la valeur Reflect en String + let val_str = Self::reflect_to_string(&*val); body.push_str(&format!( "<{0}>{1}", - name, val + name, val_str )); } body.push_str(""); @@ -956,6 +964,131 @@ impl ServiceInstance { } } + /// Convertit une valeur Reflect en String pour la notification UPnP. + /// + /// Cette fonction gère plusieurs cas : + /// - Types primitifs : formatage direct + /// - Structures serde (pmodidl, etc.) : sérialisation XML + /// - Autres types : fallback sur Debug + /// + /// Le résultat est déjà échappé XML-safe selon les normes UPnP. + fn reflect_to_string(value: &dyn Reflect) -> String { + use std::any::Any; + use bevy_reflect::ReflectRef; + + // Essayer de downcaster vers des types primitifs courants + if let Some(v) = value.as_any().downcast_ref::() { + // Échapper les caractères XML spéciaux + return escape(v).to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return v.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return if *v { "1" } else { "0" }.to_string(); + } else if let Some(v) = value.as_any().downcast_ref::() { + return escape(&v.to_string()).to_string(); + } + + // Pour les structures complexes, essayer de sérialiser avec bevy_reflect + match value.reflect_ref() { + ReflectRef::Struct(s) => { + // Construire un XML simple pour la struct + Self::serialize_struct_to_xml(s) + } + ReflectRef::TupleStruct(ts) => { + // Pour les tuple structs, essayer d'extraire la valeur si c'est un wrapper + if ts.field_len() == 1 { + if let Some(inner) = ts.field(0) { + // Convertir PartialReflect en Reflect si possible + if let Some(reflect_val) = inner.try_as_reflect() { + Self::reflect_to_string(reflect_val) + } else { + format!("{:?}", value) + } + } else { + format!("{:?}", value) + } + } else { + format!("{:?}", value) + } + } + ReflectRef::Enum(e) => { + // Pour les enums, formater comme "Variant(value)" + let variant_name = e.variant_name(); + if e.field_len() == 1 { + if let Some(field) = e.field_at(0) { + // Convertir PartialReflect en Reflect si possible + if let Some(reflect_val) = field.try_as_reflect() { + format!("{}", Self::reflect_to_string(reflect_val)) + } else { + variant_name.to_string() + } + } else { + variant_name.to_string() + } + } else { + variant_name.to_string() + } + } + _ => { + // Fallback: utiliser Debug et échapper + let debug_str = format!("{:?}", value); + escape(&debug_str).to_string() + } + } + } + + /// Sérialise une structure Reflect en XML simple. + fn serialize_struct_to_xml(s: &dyn bevy_reflect::Struct) -> String { + use std::fmt::Write; + use bevy_reflect::TypeInfo; + + let mut xml = String::new(); + + // Commencer par ouvrir la balise avec le nom du type + let type_name = s.get_represented_type_info() + .and_then(|ti| { + if let TypeInfo::Struct(si) = ti { + Some(si.type_path_table().short_path()) + } else { + None + } + }) + .unwrap_or("struct"); + + let _ = write!(&mut xml, "<{}>", type_name); + + // Ajouter chaque champ + for i in 0..s.field_len() { + if let Some(field_name) = s.name_at(i) { + if let Some(field_value) = s.field_at(i) { + // Convertir PartialReflect en Reflect si possible + if let Some(reflect_val) = field_value.try_as_reflect() { + let value_str = Self::reflect_to_string(reflect_val); + let _ = write!(&mut xml, "<{}>{}", field_name, value_str, field_name); + } + } + } + } + + let _ = write!(&mut xml, "", type_name); + + xml + } + /// Démarre le notifier périodique. /// /// # Arguments @@ -1239,6 +1372,97 @@ async fn control_handler(State(instance): State>, body: Str mod tests { use super::*; use crate::services::Service; + use bevy_reflect::Reflect; + + #[test] + fn test_reflect_to_string_primitives() { + // Test des types primitifs + assert_eq!(ServiceInstance::reflect_to_string(&42i32), "42"); + assert_eq!(ServiceInstance::reflect_to_string(&3.14f64), "3.14"); + assert_eq!(ServiceInstance::reflect_to_string(&true), "1"); + assert_eq!(ServiceInstance::reflect_to_string(&false), "0"); + assert_eq!(ServiceInstance::reflect_to_string(&'a'), "a"); + } + + #[test] + fn test_reflect_to_string_xml_escaping() { + // Test de l'échappement XML + let test_str = "Test & \"quotes\"".to_string(); + let result = ServiceInstance::reflect_to_string(&test_str); + + // Vérifier que les caractères sont échappés + assert!(result.contains("<")); + assert!(result.contains(">")); + assert!(result.contains("&")); + assert!(result.contains(""")); + } + + #[test] + fn test_reflect_to_string_struct() { + #[derive(Debug, Clone, Reflect)] + struct TestStruct { + name: String, + value: i32, + } + + let test = TestStruct { + name: "Test".to_string(), + value: 42, + }; + + let result = ServiceInstance::reflect_to_string(&test); + + // Vérifier que c'est du XML + assert!(result.starts_with("<")); + assert!(result.ends_with(">")); + assert!(result.contains("name")); + assert!(result.contains("value")); + assert!(result.contains("Test")); + assert!(result.contains("42")); + + println!("Serialized struct: {}", result); + } + + #[test] + fn test_reflect_to_string_nested_struct() { + #[derive(Debug, Clone, Reflect)] + struct Address { + street: String, + city: String, + } + + #[derive(Debug, Clone, Reflect)] + struct Person { + name: String, + age: u32, + address: Address, + } + + let person = Person { + name: "John ".to_string(), // Test XML escaping + age: 30, + address: Address { + street: "123 Main St & Ave".to_string(), + city: "Springfield".to_string(), + }, + }; + + let result = ServiceInstance::reflect_to_string(&person); + + // Vérifier la structure XML + assert!(result.contains("")); + assert!(result.contains("")); + assert!(result.contains("name")); + assert!(result.contains("age")); + assert!(result.contains("address")); + + // Vérifier l'échappement XML dans les valeurs imbriquées + assert!(result.contains("<")); + assert!(result.contains(">")); + assert!(result.contains("&")); + + println!("Nested struct XML: {}", result); + } #[test] fn test_service_instance_creation() { diff --git a/pmoupnp/src/state_variables/instance_methods.rs b/pmoupnp/src/state_variables/instance_methods.rs index de079a6a..b2987986 100644 --- a/pmoupnp/src/state_variables/instance_methods.rs +++ b/pmoupnp/src/state_variables/instance_methods.rs @@ -1,5 +1,7 @@ use std::fmt; +use std::sync::Arc; +use bevy_reflect::Reflect; use chrono::{DateTime, Utc}; use std::sync::RwLock; use xmltree::Element; @@ -44,6 +46,7 @@ impl UpnpInstance for StateVarInstance { last_modified: RwLock::new(Utc::now()), last_notification: RwLock::new(Utc::now()), service: RwLock::new(None), + reflexive_cache: RwLock::new(None), } } @@ -85,6 +88,7 @@ impl Clone for StateVarInstance { last_modified: RwLock::new(self.last_modified.read().unwrap().clone()), last_notification: RwLock::new(self.last_notification.read().unwrap().clone()), service: RwLock::new(self.service.read().unwrap().clone()), + reflexive_cache: RwLock::new(None), // Le cache n'est pas cloné, il sera recalculé si nécessaire } } } @@ -131,6 +135,12 @@ impl StateVarInstance { *val = new_value.clone(); *modified = Utc::now(); + // Invalider le cache réflexif + { + let mut cache = self.reflexive_cache.write().unwrap(); + *cache = None; + } + // Notifier le service parent si la variable envoie des événements if self.is_sending_notification() { // Relâcher les locks avant d'appeler le service @@ -140,7 +150,10 @@ impl StateVarInstance { if let Some(weak_service) = self.service.read().unwrap().as_ref() { if let Some(service) = weak_service.upgrade() { - service.event_to_be_sent(self.get_name().to_string(), new_value.to_string()); + // Obtenir la valeur réflexive (sans propager l'erreur car on est dans une notification) + if let Ok(reflected_value) = self.reflexive_value() { + service.event_to_be_sent(self.get_name().to_string(), reflected_value); + } } } } @@ -156,4 +169,68 @@ impl StateVarInstance { pub fn last_modified(&self) -> DateTime { self.last_modified.read().unwrap().clone() } + + /// Retourne la valeur sous forme réflexive (Reflect). + /// + /// Cette méthode utilise un cache pour optimiser les performances lorsqu'un parser + /// est défini. Si la variable a un parser, la valeur String sera parsée et le résultat + /// sera mis en cache. Sinon, la StateValue brute est retournée directement comme Reflect. + /// + /// Le cache est invalidé automatiquement lors de `set_value()`. + /// + /// # Returns + /// + /// Un `Arc` contenant soit: + /// - La valeur parsée (si un parser est défini) + /// - La StateValue brute (sinon) + /// + /// # Examples + /// + /// ```rust,ignore + /// let var = StateVarInstance::new(&variable); + /// let reflected = var.reflexive_value(); + /// // reflected peut maintenant être inspecté avec l'API Reflect + /// ``` + pub fn reflexive_value(&self) -> Result, crate::state_variables::StateVariableError> { + // Vérifier si on a un cache valide + { + let cache = self.reflexive_cache.read().unwrap(); + if let Some(cached) = cache.as_ref() { + return Ok(Arc::clone(cached)); + } + } + + // Pas de cache, il faut calculer la valeur + let value = self.value.read().unwrap().clone(); + + // Si la variable a un parser, l'utiliser + if let Some(parser) = &self.model.parse { + // La valeur doit être une String pour être parsée + if let crate::variable_types::StateValue::String(s) = &value { + match parser(s) { + Ok(parsed) => { + // Convertir Box en Arc + let arc_reflect: Arc = Arc::from(parsed); + + // Mettre en cache + let mut cache = self.reflexive_cache.write().unwrap(); + *cache = Some(Arc::clone(&arc_reflect)); + + return Ok(arc_reflect); + } + Err(e) => return Err(e), + } + } + } + + // Pas de parser ou la valeur n'est pas une String: convertir la StateValue en Reflect + let reflected = value.to_reflect(); + let arc_reflect: Arc = Arc::from(reflected); + + // Mettre en cache + let mut cache = self.reflexive_cache.write().unwrap(); + *cache = Some(Arc::clone(&arc_reflect)); + + Ok(arc_reflect) + } } diff --git a/pmoupnp/src/state_variables/mod.rs b/pmoupnp/src/state_variables/mod.rs index 389fda28..a7d58d58 100644 --- a/pmoupnp/src/state_variables/mod.rs +++ b/pmoupnp/src/state_variables/mod.rs @@ -60,6 +60,8 @@ pub struct StateVarInstance { last_notification: RwLock>, /// Pointeur vers le service parent (interior mutability) service: RwLock>>, + /// Cache pour la valeur réflexive (utilisé quand un parser est défini) + reflexive_cache: RwLock>>, } pub type StateVarInstanceSet = UpnpObjectSet; diff --git a/pmoupnp/src/variable_types/mod.rs b/pmoupnp/src/variable_types/mod.rs index ee60afa4..64d1832c 100644 --- a/pmoupnp/src/variable_types/mod.rs +++ b/pmoupnp/src/variable_types/mod.rs @@ -4,6 +4,7 @@ mod display_type; mod display_value; mod errors; mod fromstr; +mod reflect_impl; mod type_methods; mod type_trait; mod value_methods; diff --git a/pmoupnp/src/variable_types/reflect_impl.rs b/pmoupnp/src/variable_types/reflect_impl.rs new file mode 100644 index 00000000..524e2c35 --- /dev/null +++ b/pmoupnp/src/variable_types/reflect_impl.rs @@ -0,0 +1,44 @@ +// Ce module permet de convertir StateValue en valeurs Reflect +// +// Étant donné que StateValue contient des types qui n'implémentent pas tous Reflect +// (comme Uuid, Url, et certains types chrono), nous fournissons des méthodes de conversion +// vers des types primitifs qui supportent Reflect. + +use bevy_reflect::Reflect; +use crate::variable_types::StateValue; + +impl StateValue { + /// Convertit la StateValue en une valeur Reflect. + /// + /// Cette méthode crée un Box contenant la valeur sous-jacente. + /// Pour les types qui n'implémentent pas Reflect nativement (Uuid, Url, dates complexes), + /// on retourne leur représentation String. + pub fn to_reflect(&self) -> Box { + match self { + StateValue::UI1(v) => Box::new(*v), + StateValue::UI2(v) => Box::new(*v), + StateValue::UI4(v) => Box::new(*v), + StateValue::I1(v) => Box::new(*v), + StateValue::I2(v) => Box::new(*v), + StateValue::I4(v) => Box::new(*v), + StateValue::Int(v) => Box::new(*v), + StateValue::R4(v) => Box::new(*v), + StateValue::R8(v) => Box::new(*v), + StateValue::Number(v) => Box::new(*v), + StateValue::Fixed14_4(v) => Box::new(*v), + StateValue::Char(v) => Box::new(*v), + StateValue::String(v) => Box::new(v.clone()), + StateValue::Boolean(v) => Box::new(*v), + StateValue::BinBase64(v) => Box::new(v.clone()), + StateValue::BinHex(v) => Box::new(v.clone()), + // Pour les types complexes, on utilise leur représentation String + StateValue::Date(v) => Box::new(v.to_string()), + StateValue::DateTime(v) => Box::new(v.to_string()), + StateValue::DateTimeTZ(v) => Box::new(v.to_string()), + StateValue::Time(v) => Box::new(v.to_string()), + StateValue::TimeTZ(v) => Box::new(v.to_string()), + StateValue::UUID(v) => Box::new(v.to_string()), + StateValue::URI(v) => Box::new(v.to_string()), + } + } +}