Files
MetaCrate/crates/libremetaverse/src/message_decoder.rs
Chili Palmer c9a1170a27
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

1269 lines
47 KiB
Rust

//! 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<String>,
display_name: Option<String>,
legacy_first_name: Option<String>,
legacy_last_name: Option<String>,
is_default_display_name: bool,
next_update: SystemTime,
updated: SystemTime,
}
impl AgentDisplayName {
pub fn new() -> Result<Self, Error> {
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<Self, Error> {
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<OSD, Error> {
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<String> {
self.display_name.clone()
}
pub fn set_display_name(&mut self, value: Option<String>) {
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<String> {
self.legacy_first_name.clone()
}
pub fn set_legacy_first_name(&mut self, value: Option<String>) {
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<String> {
self.legacy_last_name.clone()
}
pub fn set_legacy_last_name(&mut self, value: Option<String>) {
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<String> {
self.user_name.clone()
}
pub fn set_user_name(&mut self, value: Option<String>) {
self.user_name = value;
}
}
fn optional_string(value: Option<&OSD>) -> Result<Option<String>, 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<Self, Error> {
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<Self, Error> {
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<OSD, Error> {
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<String>,
pub width: i32,
}
impl MediaEntry {
pub fn new() -> Result<Self, Error> {
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<Self, Error> {
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::<Result<Vec<_>, _>>()?;
}
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, Error> {
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<String, OSD>, key: &str) -> Result<bool, Error> {
map.get(key).unwrap_or(&OSD::Undefined).as_boolean()
}
fn osd_integer(map: &HashMap<String, OSD>, key: &str) -> Result<i32, Error> {
map.get(key).unwrap_or(&OSD::Undefined).as_integer()
}
fn osd_string(map: &HashMap<String, OSD>, key: &str) -> Result<String, Error> {
map.get(key).unwrap_or(&OSD::Undefined).as_string()
}
/// Resource-usage response for the agent's attachments.
pub struct AttachmentResourcesMessage {
pub attachments: HashMap<AttachmentPoint, Vec<ObjectResourcesDetail>>,
pub summary_available: HashMap<String, i32>,
pub summary_used: HashMap<String, i32>,
}
impl AttachmentResourcesMessage {
pub fn new() -> Result<Self, Error> {
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<Self, Error> {
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<Option<Box<dyn IMessage>>, 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<OSDMap, Error> {
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<String, i32>,
) -> 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<String, i32>) -> 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<ObjectResourcesDetail, Error> {
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::<Result<HashMap<_, _>, 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<T>(map: &OSDMap) -> Option<Box<dyn IMessage>>
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> {
<Self as GeneratedMessage>::deserialize_generated(self, &map)
}
fn serialize(&self) -> Result<OSDMap, Error> {
<Self as GeneratedMessage>::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<Option<Box<dyn IMessage>>, Error> {
let decoded = match event_name {
"AgentGroupDataUpdate" | "AvatarGroupsReply" => {
generated::<AgentGroupDataUpdateMessage>(map)
}
"ParcelProperties" => generated::<ParcelPropertiesMessage>(map),
"ParcelObjectOwnersReply" => generated::<ParcelObjectOwnersReplyMessage>(map),
"TeleportFinish" => generated::<TeleportFinishMessage>(map),
"EnableSimulator" => generated::<EnableSimulatorMessage>(map),
"ParcelPropertiesUpdate" => generated::<ParcelPropertiesUpdateMessage>(map),
"EstablishAgentCommunication" => generated::<EstablishAgentCommunicationMessage>(map),
"ChatterBoxInvitation" => generated::<ChatterBoxInvitationMessage>(map),
"ChatterBoxSessionEventReply" => generated::<ChatterboxSessionEventReplyMessage>(map),
"ChatterBoxSessionStartReply" => generated::<ChatterBoxSessionStartReplyMessage>(map),
"ChatterBoxSessionAgentListUpdates" => {
generated::<ChatterBoxSessionAgentListUpdatesMessage>(map)
}
"RequiredVoiceVersion" => generated::<RequiredVoiceVersionMessage>(map),
"MapLayer" if map.get("LayerData").is_some() => generated::<MapLayerReplyVariant>(map),
"ChatSessionRequest" => match map
.get("method")
.unwrap_or_default()
.as_string()
.unwrap_or_default()
.as_str()
{
"start conference" => generated::<ChatSessionRequestStartConference>(map),
"mute update" => generated::<ChatSessionRequestMuteUpdate>(map),
"accept invitation" => generated::<ChatSessionAcceptInvitation>(map),
_ => None,
},
"CopyInventoryFromNotecard" => generated::<CopyInventoryFromNotecardMessage>(map),
"ProvisionVoiceAccountRequest" => generated::<ProvisionVoiceAccountRequestMessage>(map),
"Viewerstats" | "ViewerStats" => generated::<ViewerStatsMessage>(map),
"UpdateAgentLanguage" => generated::<UpdateAgentLanguageMessage>(map),
"RemoteParcelRequest" if map.get("parcel_id").is_some() => {
generated::<RemoteParcelRequestReply>(map)
}
"UpdateScriptTask" if map.get("task_id").is_some() => {
generated::<UpdateScriptTaskUpdateMessage>(map)
}
"UpdateScriptAgent" if map.get("item_id").is_some() => {
generated::<UpdateScriptAgentRequestMessage>(map)
}
"UpdateGestureAgentInventory" | "UpdateNotecardAgentInventory"
if map.get("item_id").is_some() =>
{
generated::<UpdateAgentInventoryRequestMessage>(map)
}
"SendPostcard" => generated::<SendPostcardMessage>(map),
"LandStatReply" => generated::<LandStatReplyMessage>(map),
"ParcelVoiceInfoRequest" => generated::<ParcelVoiceInfoRequestMessage>(map),
"EventQueueGet" => {
let mut message = crate::event_queue::EventQueueGetMessage::new()?;
match message.deserialize(map.clone()) {
Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
Err(_) => None,
}
}
"CrossedRegion" => generated::<CrossedRegionMessage>(map),
"SimConsoleResponse" => generated::<SimConsoleResponseMessage>(map),
"TeleportFailed" => generated::<TeleportFailedMessage>(map),
"PlacesReply" => generated::<PlacesReplyMessage>(map),
"UpdateAgentInformation" => generated::<UpdateAgentInformationMessage>(map),
"DirLandReply" => generated::<DirLandReplyMessage>(map),
"ScriptRunningReply" => generated::<ScriptRunningReplyMessage>(map),
"AgentDropGroup" => generated::<AgentDropGroupMessage>(map),
"AgentStateUpdate" => generated::<AgentStateUpdateMessage>(map),
"NavMeshStatusUpdate" => generated::<NavMeshStatusUpdateMessage>(map),
"ForceCloseChatterBoxSession" => generated::<ForceCloseChatterBoxSessionMessage>(map),
"RegionInfo" => generated::<RegionInfoMessage>(map),
"ObjectMediaNavigate" => generated::<ObjectMediaNavigateMessage>(map),
"ObjectMedia" => match map
.get("verb")
.unwrap_or_default()
.as_string()
.unwrap_or_default()
.as_str()
{
"GET" => generated::<ObjectMediaRequest>(map),
"UPDATE" => generated::<ObjectMediaUpdate>(map),
_ if map.get("object_media_version").is_some() => generated::<ObjectMediaResponse>(map),
_ => None,
},
"SetDisplayName" => generated::<SetDisplayNameMessage>(map),
"SetDisplayNameReply" => generated::<SetDisplayNameReplyMessage>(map),
"DisplayNameUpdate" => generated::<DisplayNameUpdateMessage>(map),
"GetDisplayNames" => generated::<GetDisplayNamesMessage>(map),
"ObjectPhysicsProperties" => generated::<ObjectPhysicsPropertiesMessage>(map),
"RenderMaterials" => generated::<RenderMaterialsMessage>(map),
"GetObjectCost" => {
if map.get("object_ids").is_some() {
generated::<GetObjectCostRequest>(map)
} else {
generated::<GetObjectCostMessage>(map)
}
}
"LandResources" => {
if map.get("parcel_id").is_some() {
generated::<LandResourcesRequest>(map)
} else if map.get("ScriptResourceSummary").is_some() {
generated::<LandResourcesMessage>(map)
} else {
None
}
}
"SearchStatRequest" => {
if map.get("map_clicks").is_some() {
generated::<SearchStatRequestReply>(map)
} else if map.get("classified_id").is_some() {
generated::<SearchStatRequestRequest>(map)
} else {
None
}
}
"UploadBakedTexture" => match map
.get("state")
.unwrap_or_default()
.as_string()
.unwrap_or_default()
.as_str()
{
"upload" => generated::<UploaderRequestUpload>(map),
"complete" => generated::<UploaderRequestComplete>(map),
_ => None,
},
"AttachmentResources" => {
let mut message = AttachmentResourcesMessage::new()?;
match message.deserialize(map.clone()) {
Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
Err(_) => None,
}
}
"BulkUpdateInventory" => generated::<BulkUpdateInventoryMessage>(map),
_ => None,
};
Ok(decoded)
}
#[allow(clippy::needless_pass_by_value)] // Preserves MessageUtils.ToIP's generated signature.
pub(crate) fn to_ip(osd: OSD) -> Result<IpAddr, Error> {
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<OSD, Error> {
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<HashMap<String, String>, 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<HashMap<Uri, Uri>, 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<String, String>) -> Result<OSDMap, Error> {
OSDMap::new_with_dictionary(
dict.into_iter()
.map(|(key, value)| (key, OSD::String(value)))
.collect(),
)
}
pub(crate) fn from_dictionary_uri(dict: HashMap<Uri, Uri>) -> Result<OSDMap, Error> {
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<Item = (&'static str, OSD)>) -> 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::<UpdateAgentLanguageMessage>()
.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::<RemoteParcelRequestReply>()
.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::<ChatSessionAcceptInvitation>()
.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::<RenderMaterialsMessage>()
.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::<IpAddr>().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());
}
}