//! Strongly typed decoding for capability event names. // These public methods mirror generated C# signatures, including Result-returning // constructors and value-taking parameters that are infallible in native Rust. #![allow(clippy::inherent_to_string)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] use crate::interfaces::IMessage; use crate::message_codec::GeneratedMessage; use crate::messages::linden::{ AgentDropGroupMessage, AgentGroupDataUpdateMessage, AgentStateUpdateMessage, BulkUpdateInventoryMessage, ChatSessionAcceptInvitation, ChatSessionRequestMuteUpdate, ChatSessionRequestStartConference, ChatterBoxInvitationMessage, ChatterBoxSessionAgentListUpdatesMessage, ChatterBoxSessionStartReplyMessage, ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage, DirLandReplyMessage, DisplayNameUpdateMessage, EnableSimulatorMessage, EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage, GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest, LandStatReplyMessage, MapLayerReplyVariant, NavMeshStatusUpdateMessage, ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate, ObjectPhysicsPropertiesMessage, ObjectResourcesDetail, ParcelObjectOwnersReplyMessage, ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage, PlacesReplyMessage, ProvisionVoiceAccountRequestMessage, RegionInfoMessage, RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage, ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest, SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage, SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage, UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage, UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete, UploaderRequestUpload, ViewerStatsMessage, }; use crate::{Error, MediaControls, MediaPermission}; use libremetaverse_structured_data::{OSD, OSDMap}; use libremetaverse_types::compat::Uri; use libremetaverse_types::{AttachmentPoint, PhysicsShapeType, UUID}; use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr}; use std::time::{SystemTime, UNIX_EPOCH}; /// Display-name data carried by the display-name capability messages. #[derive(Clone, Debug, Eq, PartialEq)] pub struct AgentDisplayName { id: UUID, user_name: Option, display_name: Option, legacy_first_name: Option, legacy_last_name: Option, is_default_display_name: bool, next_update: SystemTime, updated: SystemTime, } impl AgentDisplayName { pub fn new() -> Result { Ok(Self { id: UUID::zero(), user_name: None, display_name: None, legacy_first_name: None, legacy_last_name: None, is_default_display_name: false, next_update: UNIX_EPOCH, updated: UNIX_EPOCH, }) } pub fn from_osd(data: OSD) -> Result { let OSD::Map(map) = data else { return Err(Error::Argument); }; let mut value = Self::new()?; value.id = map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?; value.user_name = optional_string(map.get("username"))?; value.display_name = optional_string(map.get("display_name"))?; value.legacy_first_name = optional_string(map.get("legacy_first_name"))?; value.legacy_last_name = optional_string(map.get("legacy_last_name"))?; value.is_default_display_name = map .get("is_display_name_default") .unwrap_or(&OSD::Undefined) .as_boolean()?; value.next_update = map .get("display_name_next_update") .unwrap_or(&OSD::Undefined) .as_date()?; value.updated = map .get("last_updated") .unwrap_or(&OSD::Undefined) .as_date()?; Ok(value) } pub fn get_osd(&self) -> Result { Ok(OSD::Map(HashMap::from([ ("id".into(), OSD::UUID(self.id)), ( "username".into(), OSD::String(self.user_name.clone().unwrap_or_default()), ), ( "display_name".into(), OSD::String(self.display_name.clone().unwrap_or_default()), ), ( "legacy_first_name".into(), OSD::String(self.legacy_first_name.clone().unwrap_or_default()), ), ( "legacy_last_name".into(), OSD::String(self.legacy_last_name.clone().unwrap_or_default()), ), ( "is_display_name_default".into(), OSD::Boolean(self.is_default_display_name), ), ( "display_name_next_update".into(), OSD::Date(self.next_update), ), ("last_updated".into(), OSD::Date(self.updated)), ]))) } pub fn to_string(&self) -> String { format!( "AgentDisplayName {{ id: {}, user_name: {:?}, display_name: {:?}, legacy_first_name: {:?}, legacy_last_name: {:?}, is_default_display_name: {}, next_update: {:?}, updated: {:?} }}", self.id, self.user_name, self.display_name, self.legacy_first_name, self.legacy_last_name, self.is_default_display_name, self.next_update, self.updated ) } pub fn display_name(&self) -> Option { self.display_name.clone() } pub fn set_display_name(&mut self, value: Option) { self.display_name = value; } pub fn id(&self) -> UUID { self.id } pub fn set_id(&mut self, value: UUID) { self.id = value; } pub fn is_default_display_name(&self) -> bool { self.is_default_display_name } pub fn set_is_default_display_name(&mut self, value: bool) { self.is_default_display_name = value; } pub fn legacy_first_name(&self) -> Option { self.legacy_first_name.clone() } pub fn set_legacy_first_name(&mut self, value: Option) { self.legacy_first_name = value; } pub fn legacy_full_name(&self) -> String { format!( "{} {}", self.legacy_first_name.as_deref().unwrap_or_default(), self.legacy_last_name.as_deref().unwrap_or_default() ) } pub fn legacy_last_name(&self) -> Option { self.legacy_last_name.clone() } pub fn set_legacy_last_name(&mut self, value: Option) { self.legacy_last_name = value; } pub fn next_update(&self) -> SystemTime { self.next_update } pub fn set_next_update(&mut self, value: SystemTime) { self.next_update = value; } pub fn updated(&self) -> SystemTime { self.updated } pub fn set_updated(&mut self, value: SystemTime) { self.updated = value; } pub fn user_name(&self) -> Option { self.user_name.clone() } pub fn set_user_name(&mut self, value: Option) { self.user_name = value; } } fn optional_string(value: Option<&OSD>) -> Result, Error> { match value { None | Some(OSD::Undefined) => Ok(None), Some(value) => Ok(Some(value.as_string()?)), } } /// Physics attributes carried by `ObjectPhysicsProperties` events. #[derive(Clone, Debug, PartialEq)] pub struct PrimitivePhysicsProperties { pub density: f32, pub friction: f32, pub gravity_multiplier: f32, pub local_id: u32, pub physics_shape_type: PhysicsShapeType, pub restitution: f32, } impl PrimitivePhysicsProperties { pub fn new() -> Result { Ok(Self { density: 0.0, friction: 0.0, gravity_multiplier: 0.0, local_id: 0, physics_shape_type: PhysicsShapeType::Prim, restitution: 0.0, }) } #[allow(clippy::cast_possible_truncation)] // Matches the C# double-to-float LLSD conversion. pub fn from_osd(osd: OSD) -> Result { let OSD::Map(map) = osd else { return Self::new(); }; let shape = map .get("PhysicsShapeType") .unwrap_or(&OSD::Undefined) .as_integer()?; Ok(Self { local_id: map .get("LocalID") .unwrap_or(&OSD::Undefined) .as_u_integer()?, density: map.get("Density").unwrap_or(&OSD::Undefined).as_real()? as f32, friction: map.get("Friction").unwrap_or(&OSD::Undefined).as_real()? as f32, gravity_multiplier: map .get("GravityMultiplier") .unwrap_or(&OSD::Undefined) .as_real()? as f32, restitution: map .get("Restitution") .unwrap_or(&OSD::Undefined) .as_real()? as f32, physics_shape_type: match shape { 1 => PhysicsShapeType::None, 2 => PhysicsShapeType::ConvexHull, _ => PhysicsShapeType::Prim, }, }) } pub fn get_osd(&self) -> Result { Ok(OSD::Map(HashMap::from([ ("LocalID".into(), OSD::from_u_integer(self.local_id)?), ("Density".into(), OSD::Real(f64::from(self.density))), ("Friction".into(), OSD::Real(f64::from(self.friction))), ( "GravityMultiplier".into(), OSD::Real(f64::from(self.gravity_multiplier)), ), ("Restitution".into(), OSD::Real(f64::from(self.restitution))), ( "PhysicsShapeType".into(), OSD::Integer(self.physics_shape_type as i32), ), ]))) } } /// Per-face media settings used by object-media capability messages. #[allow(clippy::struct_excessive_bools)] // The flags are independent wire fields in the C# schema. #[derive(Clone, Debug, PartialEq)] pub struct MediaEntry { undefined: bool, enable_alternative_image: bool, pub auto_loop: bool, pub auto_play: bool, pub auto_scale: bool, pub auto_zoom: bool, pub control_permissions: MediaPermission, pub controls: MediaControls, pub current_url: String, pub enable_white_list: bool, pub height: i32, pub home_url: String, pub interact_on_first_click: bool, pub interact_permissions: MediaPermission, pub white_list: Vec, pub width: i32, } impl MediaEntry { pub fn new() -> Result { Ok(Self { undefined: false, enable_alternative_image: false, auto_loop: false, auto_play: false, auto_scale: false, auto_zoom: false, control_permissions: MediaPermission::NONE, controls: MediaControls::Standard, current_url: String::new(), enable_white_list: false, height: 0, home_url: String::new(), interact_on_first_click: false, interact_permissions: MediaPermission::NONE, white_list: Vec::new(), width: 0, }) } pub fn from_osd(osd: OSD) -> Result { if matches!(osd, OSD::Undefined) { let mut entry = Self::new()?; entry.undefined = true; return Ok(entry); } let OSD::Map(map) = osd else { return Err(Error::Argument); }; let mut entry = Self::new()?; entry.enable_alternative_image = osd_bool(&map, "alt_image_enable")?; entry.auto_loop = osd_bool(&map, "auto_loop")?; entry.auto_play = osd_bool(&map, "auto_play")?; entry.auto_scale = osd_bool(&map, "auto_scale")?; entry.auto_zoom = osd_bool(&map, "auto_zoom")?; entry.controls = match osd_integer(&map, "controls")? { 1 => MediaControls::Mini, _ => MediaControls::Standard, }; entry.current_url = osd_string(&map, "current_url")?; entry.interact_on_first_click = osd_bool(&map, "first_click_interact")?; entry.height = osd_integer(&map, "height_pixels")?; entry.home_url = osd_string(&map, "home_url")?; entry.control_permissions = MediaPermission( u8::try_from(osd_integer(&map, "perms_control")?).map_err(|_| Error::Argument)?, ); entry.interact_permissions = MediaPermission( u8::try_from(osd_integer(&map, "perms_interact")?).map_err(|_| Error::Argument)?, ); if let Some(OSD::Array(values)) = map.get("whitelist") { entry.white_list = values .iter() .map(OSD::as_string) .collect::, _>>()?; } entry.enable_white_list = osd_bool(&map, "whitelist_enable")?; entry.width = osd_integer(&map, "width_pixels")?; Ok(entry) } pub fn get_osd(&self) -> Result { OSDMap::new_with_dictionary(HashMap::from([ ( "alt_image_enable".into(), OSD::Boolean(self.enable_alternative_image), ), ("auto_loop".into(), OSD::Boolean(self.auto_loop)), ("auto_play".into(), OSD::Boolean(self.auto_play)), ("auto_scale".into(), OSD::Boolean(self.auto_scale)), ("auto_zoom".into(), OSD::Boolean(self.auto_zoom)), ("controls".into(), OSD::Integer(self.controls as i32)), ("current_url".into(), OSD::String(self.current_url.clone())), ( "first_click_interact".into(), OSD::Boolean(self.interact_on_first_click), ), ("height_pixels".into(), OSD::Integer(self.height)), ("home_url".into(), OSD::String(self.home_url.clone())), ( "perms_control".into(), OSD::Integer(i32::from(self.control_permissions.0)), ), ( "perms_interact".into(), OSD::Integer(i32::from(self.interact_permissions.0)), ), ( "whitelist".into(), OSD::Array(self.white_list.iter().cloned().map(OSD::String).collect()), ), ( "whitelist_enable".into(), OSD::Boolean(self.enable_white_list), ), ("width_pixels".into(), OSD::Integer(self.width)), ])) } pub fn enable_alternative_image(&self) -> bool { self.enable_alternative_image } pub fn set_enable_alternative_image(&mut self, value: bool) { self.enable_alternative_image = value; } pub(crate) const fn is_undefined(&self) -> bool { self.undefined } } fn osd_bool(map: &HashMap, key: &str) -> Result { map.get(key).unwrap_or(&OSD::Undefined).as_boolean() } fn osd_integer(map: &HashMap, key: &str) -> Result { map.get(key).unwrap_or(&OSD::Undefined).as_integer() } fn osd_string(map: &HashMap, key: &str) -> Result { map.get(key).unwrap_or(&OSD::Undefined).as_string() } /// Resource-usage response for the agent's attachments. pub struct AttachmentResourcesMessage { pub attachments: HashMap>, pub summary_available: HashMap, pub summary_used: HashMap, } impl AttachmentResourcesMessage { pub fn new() -> Result { Ok(Self { attachments: HashMap::new(), summary_available: HashMap::new(), summary_used: HashMap::new(), }) } #[allow(clippy::needless_pass_by_value)] // Preserves the generated C# API mapping. pub fn deserialize(&mut self, osd: OSDMap) -> Result<(), Error> { self.summary_available.clear(); self.summary_used.clear(); let summary = osd.get("summary").ok_or(Error::Argument)?; let OSD::Map(summary) = summary else { return Err(Error::Argument); }; decode_resource_summary(summary.get("available"), &mut self.summary_available)?; decode_resource_summary(summary.get("used"), &mut self.summary_used)?; self.attachments.clear(); let attachments = osd.get("attachments").ok_or(Error::Argument)?; let OSD::Array(attachments) = attachments else { return Err(Error::Argument); }; for attachment in attachments { let OSD::Map(attachment) = attachment else { return Err(Error::Argument); }; let location = attachment .get("location") .unwrap_or(&OSD::Undefined) .as_string()?; let point = attachment_point_from_string(&location); let Some(OSD::Array(objects)) = attachment.get("objects") else { return Err(Error::Argument); }; let mut decoded = Vec::with_capacity(objects.len()); for object in objects { decoded.push(decode_object_resources(object)?); } self.attachments.insert(point, decoded); } Ok(()) } pub fn from_osd(osd: OSD) -> Result { let OSD::Map(values) = osd else { return Err(Error::Argument); }; let mut message = Self::new()?; message.deserialize(OSDMap::new_with_dictionary(values)?)?; Ok(message) } pub fn get_message_handler(_map: OSDMap) -> Result>, Error> { Ok(Some(Box::new(Self::new()?))) } } impl IMessage for AttachmentResourcesMessage { fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { Self::deserialize(self, map) } fn serialize(&self) -> Result { let summary = HashMap::from([ ( "available".into(), encode_resource_summary(&self.summary_available), ), ("used".into(), encode_resource_summary(&self.summary_used)), ]); OSDMap::new_with_dictionary(HashMap::from([("summary".into(), OSD::Map(summary))])) } } fn decode_resource_summary( value: Option<&OSD>, target: &mut HashMap, ) -> Result<(), Error> { let Some(OSD::Array(values)) = value else { return Err(Error::Argument); }; for value in values { let OSD::Map(value) = value else { return Err(Error::Argument); }; target.insert(osd_string(value, "type")?, osd_integer(value, "amount")?); } Ok(()) } fn encode_resource_summary(values: &HashMap) -> OSD { let mut entries: Vec<_> = values.iter().collect(); entries.sort_unstable_by(|left, right| left.0.cmp(right.0)); OSD::Array( entries .into_iter() .map(|(name, amount)| { OSD::Map(HashMap::from([ ("type".into(), OSD::String(name.clone())), ("amount".into(), OSD::Integer(*amount)), ])) }) .collect(), ) } fn decode_object_resources(value: &OSD) -> Result { let OSD::Map(map) = value else { return Err(Error::Argument); }; let resources = match map.get("resources") { Some(OSD::Map(values)) => values .iter() .map(|(key, value)| Ok((key.clone(), value.as_integer()?))) .collect::, Error>>()?, _ => return Err(Error::Argument), }; Ok(ObjectResourcesDetail { group_owned: osd_bool(map, "is_group_owned")?, id: map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?, location: map .get("location") .unwrap_or(&OSD::Undefined) .as_vector3d()?, name: osd_string(map, "name")?, owner_id: map.get("owner_id").unwrap_or(&OSD::Undefined).as_uuid()?, resources, }) } fn attachment_point_from_string(value: &str) -> AttachmentPoint { const POINTS: &[AttachmentPoint] = &[ AttachmentPoint::AltLeftEar, AttachmentPoint::AltLeftEye, AttachmentPoint::AltRightEar, AttachmentPoint::AltRightEye, AttachmentPoint::Chest, AttachmentPoint::Chin, AttachmentPoint::Default, AttachmentPoint::Groin, AttachmentPoint::HUDBottom, AttachmentPoint::HUDBottomLeft, AttachmentPoint::HUDBottomRight, AttachmentPoint::HUDCenter, AttachmentPoint::HUDCenter2, AttachmentPoint::HUDTop, AttachmentPoint::HUDTopLeft, AttachmentPoint::HUDTopRight, AttachmentPoint::Jaw, AttachmentPoint::LeftEar, AttachmentPoint::LeftEyeball, AttachmentPoint::LeftFoot, AttachmentPoint::LeftForearm, AttachmentPoint::LeftHand, AttachmentPoint::LeftHandRing, AttachmentPoint::LeftHindFoot, AttachmentPoint::LeftHip, AttachmentPoint::LeftLowerLeg, AttachmentPoint::LeftPec, AttachmentPoint::LeftShoulder, AttachmentPoint::LeftUpperArm, AttachmentPoint::LeftUpperLeg, AttachmentPoint::LeftWing, AttachmentPoint::Mouth, AttachmentPoint::Neck, AttachmentPoint::Nose, AttachmentPoint::Pelvis, AttachmentPoint::RightEar, AttachmentPoint::RightEyeball, AttachmentPoint::RightFoot, AttachmentPoint::RightForearm, AttachmentPoint::RightHand, AttachmentPoint::RightHandRing, AttachmentPoint::RightHindFoot, AttachmentPoint::RightHip, AttachmentPoint::RightLowerLeg, AttachmentPoint::RightPec, AttachmentPoint::RightShoulder, AttachmentPoint::RightUpperArm, AttachmentPoint::RightUpperLeg, AttachmentPoint::RightWing, AttachmentPoint::Root, AttachmentPoint::Skull, AttachmentPoint::Spine, AttachmentPoint::Stomach, AttachmentPoint::TailBase, AttachmentPoint::TailTip, AttachmentPoint::Tongue, ]; let normalized: String = value .chars() .filter(char::is_ascii_alphanumeric) .flat_map(char::to_lowercase) .collect(); POINTS .iter() .copied() .find(|point| format!("{point:?}").to_ascii_lowercase() == normalized) .unwrap_or(AttachmentPoint::Default) } fn generated(map: &OSDMap) -> Option> where T: GeneratedMessage + IMessage + 'static, { let mut message = T::new_generated(); T::deserialize_generated(&mut message, map).ok()?; Some(Box::new(message)) } macro_rules! message_variant { ($type:ty) => { impl IMessage for $type { fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { ::deserialize_generated(self, &map) } fn serialize(&self) -> Result { ::serialize_generated(self) } } }; } // These classes are polymorphic blocks in C#. The generated Rust surface keeps // each concrete variant as a value type, so allowing it to cross the IMessage // boundary preserves all decoded fields without an allocation-only wrapper. message_variant!(ChatSessionAcceptInvitation); message_variant!(ChatSessionRequestMuteUpdate); message_variant!(ChatSessionRequestStartConference); message_variant!(MapLayerReplyVariant); message_variant!(ObjectMediaRequest); message_variant!(ObjectMediaResponse); message_variant!(ObjectMediaUpdate); message_variant!(RemoteParcelRequestReply); message_variant!(SearchStatRequestReply); message_variant!(SearchStatRequestRequest); message_variant!(UpdateAgentInventoryRequestMessage); message_variant!(UpdateScriptAgentRequestMessage); message_variant!(UpdateScriptTaskUpdateMessage); message_variant!(UploaderRequestComplete); message_variant!(UploaderRequestUpload); /// Decodes the event names handled by the C# `MessageUtils.DecodeEvent` /// switch. A malformed known message follows the reference behavior and is /// returned as `None`, allowing the caller to try packet translation next. #[allow(clippy::too_many_lines)] // Mirrors the reference decoder's explicit event-name switch. pub(crate) fn decode_event( event_name: &str, map: &OSDMap, ) -> Result>, Error> { let decoded = match event_name { "AgentGroupDataUpdate" | "AvatarGroupsReply" => { generated::(map) } "ParcelProperties" => generated::(map), "ParcelObjectOwnersReply" => generated::(map), "TeleportFinish" => generated::(map), "EnableSimulator" => generated::(map), "ParcelPropertiesUpdate" => generated::(map), "EstablishAgentCommunication" => generated::(map), "ChatterBoxInvitation" => generated::(map), "ChatterBoxSessionEventReply" => generated::(map), "ChatterBoxSessionStartReply" => generated::(map), "ChatterBoxSessionAgentListUpdates" => { generated::(map) } "RequiredVoiceVersion" => generated::(map), "MapLayer" if map.get("LayerData").is_some() => generated::(map), "ChatSessionRequest" => match map .get("method") .unwrap_or_default() .as_string() .unwrap_or_default() .as_str() { "start conference" => generated::(map), "mute update" => generated::(map), "accept invitation" => generated::(map), _ => None, }, "CopyInventoryFromNotecard" => generated::(map), "ProvisionVoiceAccountRequest" => generated::(map), "Viewerstats" | "ViewerStats" => generated::(map), "UpdateAgentLanguage" => generated::(map), "RemoteParcelRequest" if map.get("parcel_id").is_some() => { generated::(map) } "UpdateScriptTask" if map.get("task_id").is_some() => { generated::(map) } "UpdateScriptAgent" if map.get("item_id").is_some() => { generated::(map) } "UpdateGestureAgentInventory" | "UpdateNotecardAgentInventory" if map.get("item_id").is_some() => { generated::(map) } "SendPostcard" => generated::(map), "LandStatReply" => generated::(map), "ParcelVoiceInfoRequest" => generated::(map), "EventQueueGet" => { let mut message = crate::event_queue::EventQueueGetMessage::new()?; match message.deserialize(map.clone()) { Ok(()) => Some(Box::new(message) as Box), Err(_) => None, } } "CrossedRegion" => generated::(map), "SimConsoleResponse" => generated::(map), "TeleportFailed" => generated::(map), "PlacesReply" => generated::(map), "UpdateAgentInformation" => generated::(map), "DirLandReply" => generated::(map), "ScriptRunningReply" => generated::(map), "AgentDropGroup" => generated::(map), "AgentStateUpdate" => generated::(map), "NavMeshStatusUpdate" => generated::(map), "ForceCloseChatterBoxSession" => generated::(map), "RegionInfo" => generated::(map), "ObjectMediaNavigate" => generated::(map), "ObjectMedia" => match map .get("verb") .unwrap_or_default() .as_string() .unwrap_or_default() .as_str() { "GET" => generated::(map), "UPDATE" => generated::(map), _ if map.get("object_media_version").is_some() => generated::(map), _ => None, }, "SetDisplayName" => generated::(map), "SetDisplayNameReply" => generated::(map), "DisplayNameUpdate" => generated::(map), "GetDisplayNames" => generated::(map), "ObjectPhysicsProperties" => generated::(map), "RenderMaterials" => generated::(map), "GetObjectCost" => { if map.get("object_ids").is_some() { generated::(map) } else { generated::(map) } } "LandResources" => { if map.get("parcel_id").is_some() { generated::(map) } else if map.get("ScriptResourceSummary").is_some() { generated::(map) } else { None } } "SearchStatRequest" => { if map.get("map_clicks").is_some() { generated::(map) } else if map.get("classified_id").is_some() { generated::(map) } else { None } } "UploadBakedTexture" => match map .get("state") .unwrap_or_default() .as_string() .unwrap_or_default() .as_str() { "upload" => generated::(map), "complete" => generated::(map), _ => None, }, "AttachmentResources" => { let mut message = AttachmentResourcesMessage::new()?; match message.deserialize(map.clone()) { Ok(()) => Some(Box::new(message) as Box), Err(_) => None, } } "BulkUpdateInventory" => generated::(map), _ => None, }; Ok(decoded) } #[allow(clippy::needless_pass_by_value)] // Preserves MessageUtils.ToIP's generated signature. pub(crate) fn to_ip(osd: OSD) -> Result { let bytes = osd.as_binary()?; Ok(<[u8; 4]>::try_from(bytes) .map(Ipv4Addr::from) .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), IpAddr::V4)) } #[allow(clippy::unnecessary_wraps)] // Preserves MessageUtils.FromIP's generated signature. pub(crate) fn from_ip(address: IpAddr) -> Result { match address { IpAddr::V4(address) if !address.is_unspecified() => { Ok(OSD::Binary(address.octets().to_vec())) } IpAddr::V6(address) if !address.is_unspecified() => { Ok(OSD::Binary(address.octets().to_vec())) } IpAddr::V4(_) | IpAddr::V6(_) => Ok(OSD::Undefined), } } pub(crate) fn to_dictionary_string(osd: OSD) -> Result, Error> { let OSD::Map(map) = osd else { return Ok(HashMap::new()); }; map.into_iter() .map(|(key, value)| Ok((key, value.as_string()?))) .collect() } pub(crate) fn to_dictionary_uri(osd: OSD) -> Result, Error> { let OSD::Map(map) = osd else { return Ok(HashMap::new()); }; let mut dictionary = HashMap::with_capacity(map.len()); for (key, value) in map { if let Some(value) = value.as_uri()? { dictionary.insert(Uri(key), value); } } Ok(dictionary) } pub(crate) fn from_dictionary_string(dict: HashMap) -> Result { OSDMap::new_with_dictionary( dict.into_iter() .map(|(key, value)| (key, OSD::String(value))) .collect(), ) } pub(crate) fn from_dictionary_uri(dict: HashMap) -> Result { OSDMap::new_with_dictionary( dict.into_iter() .map(|(key, value)| (key.0, OSD::Uri(value))) .collect(), ) } #[cfg(test)] #[allow(clippy::too_many_lines)] // The exhaustive decoder matrix stays in one test. mod tests { use super::*; use flate2::Compression; use flate2::write::ZlibEncoder; use libremetaverse_structured_data::OSDParser; use libremetaverse_types::UUID; use std::io::Write as _; fn map(entries: impl IntoIterator) -> OSDMap { OSDMap::new_with_dictionary( entries .into_iter() .map(|(key, value)| (key.to_owned(), value)) .collect(), ) .unwrap() } #[test] fn known_event_and_alias_decode_to_typed_messages_with_working_trait_codec() { let language = map([ ("language", OSD::String("en-US".to_owned())), ("language_is_public", OSD::Boolean(true)), ]); let decoded = decode_event("UpdateAgentLanguage", &language) .unwrap() .expect("known message"); let typed = (decoded.as_ref() as &dyn std::any::Any) .downcast_ref::() .expect("typed language message"); assert_eq!(typed.language, "en-US"); assert!(typed.language_public); assert_eq!( decoded.serialize().unwrap().get("language"), language.get("language") ); let group = map([ ( "AgentData", OSD::Array(vec![OSD::Map(HashMap::from([( "AgentID".to_owned(), OSD::UUID(UUID::zero()), )]))]), ), ("GroupData", OSD::Array(Vec::new())), ("NewGroupData", OSD::Array(Vec::new())), ]); let alias = decode_event("AvatarGroupsReply", &group).unwrap(); assert!(alias.is_some()); } #[test] fn dynamic_reference_messages_select_the_matching_concrete_variant() { let parcel_id = UUID::random().unwrap(); let remote = map([("parcel_id", OSD::UUID(parcel_id))]); let decoded = decode_event("RemoteParcelRequest", &remote) .unwrap() .expect("remote reply"); let typed = (decoded.as_ref() as &dyn std::any::Any) .downcast_ref::() .expect("reply variant"); assert_eq!(typed.parcel_id, parcel_id); let chat = map([ ("method", OSD::String("accept invitation".to_owned())), ("session-id", OSD::UUID(UUID::random().unwrap())), ]); let decoded = decode_event("ChatSessionRequest", &chat) .unwrap() .expect("chat variant"); assert!( (decoded.as_ref() as &dyn std::any::Any) .downcast_ref::() .is_some() ); } #[test] fn unknown_or_malformed_schema_returns_none_for_packet_fallback() { let empty = OSDMap::new_with_constructor().unwrap(); assert!(decode_event("FutureGridEvent", &empty).unwrap().is_none()); assert!(decode_event("ObjectMedia", &empty).unwrap().is_none()); } #[test] fn every_reference_decoder_case_has_a_native_typed_path() { let display_name = OSD::Map(HashMap::from([ ("id".into(), OSD::UUID(UUID::zero())), ("username".into(), OSD::String("resident".into())), ("display_name".into(), OSD::String("Resident".into())), ("legacy_first_name".into(), OSD::String("Test".into())), ("legacy_last_name".into(), OSD::String("Resident".into())), ("is_display_name_default".into(), OSD::Boolean(false)), ("display_name_next_update".into(), OSD::Date(UNIX_EPOCH)), ("last_updated".into(), OSD::Date(UNIX_EPOCH)), ])); let cases = [ ( "SearchStatRequest", map([("classified_id", OSD::UUID(UUID::zero()))]), ), ( "UploadBakedTexture", map([ ("state", OSD::String("complete".into())), ("new_asset", OSD::UUID(UUID::zero())), ]), ), ( "ObjectMedia", map([ ("object_id", OSD::UUID(UUID::zero())), ("verb", OSD::String("GET".into())), ]), ), ( "AttachmentResources", map([ ( "summary", OSD::Map(HashMap::from([ ("available".into(), OSD::Array(Vec::new())), ("used".into(), OSD::Array(Vec::new())), ])), ), ("attachments", OSD::Array(Vec::new())), ]), ), ( "LandResources", map([("parcel_id", OSD::UUID(UUID::zero()))]), ), ( "GetDisplayNames", map([ ("agents", OSD::Array(vec![display_name.clone()])), ("bad_ids", OSD::Array(Vec::new())), ]), ), ( "SetDisplayNameReply", map([ ("content", display_name.clone()), ("reason", OSD::String("ok".into())), ("status", OSD::Integer(200)), ]), ), ( "DisplayNameUpdate", map([( "agent", match display_name.clone() { OSD::Map(mut agent) => { agent.insert("old_display_name".into(), OSD::String("Old".into())); OSD::Map(agent) } _ => unreachable!(), }, )]), ), ( "ObjectPhysicsProperties", map([( "ObjectData", OSD::Array(vec![OSD::Map(HashMap::from([ ("LocalID".into(), OSD::Integer(7)), ("Density".into(), OSD::Real(1000.0)), ("Friction".into(), OSD::Real(0.5)), ("GravityMultiplier".into(), OSD::Real(1.0)), ("Restitution".into(), OSD::Real(0.2)), ("PhysicsShapeType".into(), OSD::Integer(2)), ]))]), )]), ), ( "BulkUpdateInventory", map([ ( "AgentData", OSD::Array(vec![OSD::Map(HashMap::from([ ("AgentID".into(), OSD::UUID(UUID::zero())), ("TransactionID".into(), OSD::UUID(UUID::zero())), ]))]), ), ("FolderData", OSD::Array(Vec::new())), ("ItemData", OSD::Array(Vec::new())), ]), ), ( "GetObjectCost", map([("object_ids", OSD::Array(vec![OSD::UUID(UUID::zero())]))]), ), ]; for (name, fixture) in cases { let decoded = decode_event(name, &fixture) .unwrap() .unwrap_or_else(|| panic!("{name} must decode")); decoded.serialize().expect("typed codec must serialize"); } } #[test] fn object_media_preserves_null_face_slots_from_the_reference_shape() { let fixture = map([ ("object_id", OSD::UUID(UUID::zero())), ( "object_media_data", OSD::Array(vec![ OSD::Undefined, OSD::Map(HashMap::from([ ("alt_image_enable".into(), OSD::Boolean(false)), ("auto_loop".into(), OSD::Boolean(false)), ("auto_play".into(), OSD::Boolean(true)), ("auto_scale".into(), OSD::Boolean(false)), ("auto_zoom".into(), OSD::Boolean(false)), ("controls".into(), OSD::Integer(0)), ( "current_url".into(), OSD::String("https://example.test/".into()), ), ("first_click_interact".into(), OSD::Boolean(true)), ("height_pixels".into(), OSD::Integer(512)), ( "home_url".into(), OSD::String("https://example.test/".into()), ), ("perms_control".into(), OSD::Integer(7)), ("perms_interact".into(), OSD::Integer(7)), ("whitelist".into(), OSD::Array(Vec::new())), ("whitelist_enable".into(), OSD::Boolean(false)), ("width_pixels".into(), OSD::Integer(1024)), ])), ]), ), ("object_media_version", OSD::String("x-mv:1/0".into())), ]); let decoded = decode_event("ObjectMedia", &fixture) .unwrap() .expect("object media response"); let encoded = decoded.serialize().unwrap(); assert!(matches!( encoded.get("object_media_data"), Some(OSD::Array(values)) if matches!(values.first(), Some(OSD::Undefined)) )); } #[test] fn remaining_native_reference_switch_cases_decode_without_stubs() { let simple_cases = [ ( "SimConsoleResponse", map([("body", OSD::String("ok".into()))]), ), ( "ForceCloseChatterBoxSession", map([ ("reason", OSD::String("closed".into())), ("session_id", OSD::UUID(UUID::zero())), ]), ), ( "NavMeshStatusUpdate", map([ ("region_id", OSD::UUID(UUID::zero())), ("status", OSD::String("complete".into())), ("version", OSD::Integer(4)), ]), ), ( "ObjectMediaNavigate", map([ ("current_url", OSD::String("https://example.test/".into())), ("object_id", OSD::UUID(UUID::zero())), ("texture_index", OSD::Integer(2)), ]), ), ( "RegionInfo", map([ ("parcel_local_id", OSD::Integer(7)), ("region_name", OSD::String("Region".into())), ( "voice_credentials", OSD::Map(HashMap::from([( "channel_uri".to_owned(), OSD::String("sip:region@example.test".into()), )])), ), ]), ), ( "SetDisplayName", map([( "display_name", OSD::Array(vec![OSD::String("Old".into()), OSD::String("New".into())]), )]), ), ( "AgentDropGroup", map([( "AgentData", OSD::Array(vec![OSD::Map(HashMap::from([ ("AgentID".to_owned(), OSD::UUID(UUID::zero())), ("GroupID".to_owned(), OSD::UUID(UUID::zero())), ]))]), )]), ), ]; for (name, fixture) in simple_cases { assert!( decode_event(name, &fixture).unwrap().is_some(), "{name} must decode" ); } let object_id = UUID::random().unwrap(); let costs = OSDMap::new_with_dictionary(HashMap::from([( object_id.to_string(), OSD::Map(HashMap::from([ ("linked_set_resource_cost".into(), OSD::Real(1.0)), ("resource_cost".into(), OSD::Real(2.0)), ("physics_cost".into(), OSD::Real(3.0)), ("linked_set_physics_cost".into(), OSD::Real(4.0)), ])), )])) .unwrap(); assert!(decode_event("GetObjectCost", &costs).unwrap().is_some()); let binary = OSDParser::serialize_llsd_binary_with_osd(OSD::Map(HashMap::from([( "material".to_owned(), OSD::String("native".to_owned()), )]))) .unwrap(); let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); encoder.write_all(&binary).unwrap(); let render = map([("Zipped", OSD::Binary(encoder.finish().unwrap()))]); let decoded = decode_event("RenderMaterials", &render) .unwrap() .expect("render materials"); let typed = (decoded.as_ref() as &dyn std::any::Any) .downcast_ref::() .unwrap(); assert!( matches!(&typed.material_data, OSD::Map(values) if values.get("material") == Some(&OSD::String("native".into()))) ); } #[test] fn message_utils_ip_and_dictionary_conversions_match_reference_shapes() { assert_eq!( to_ip(OSD::Binary(vec![127, 0, 0, 1])).unwrap(), "127.0.0.1".parse::().unwrap() ); assert_eq!( to_ip(OSD::Binary(vec![1, 2, 3])).unwrap(), IpAddr::V4(Ipv4Addr::UNSPECIFIED) ); assert_eq!( from_ip("192.0.2.1".parse().unwrap()).unwrap(), OSD::Binary(vec![192, 0, 2, 1]) ); assert_eq!( from_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)).unwrap(), OSD::Undefined ); let strings = HashMap::from([ ("a".to_owned(), "one".to_owned()), ("b".to_owned(), "two".to_owned()), ]); let map = from_dictionary_string(strings.clone()).unwrap(); assert_eq!( to_dictionary_string(OSD::Map(map.snapshot())).unwrap(), strings ); let uris = HashMap::from([( Uri("https://key.example.test/".to_owned()), Uri("https://value.example.test/".to_owned()), )]); let map = from_dictionary_uri(uris.clone()).unwrap(); assert_eq!(to_dictionary_uri(OSD::Map(map.snapshot())).unwrap(), uris); assert!(to_dictionary_string(OSD::Integer(1)).unwrap().is_empty()); } }