//! OpenSim-compatible XML2 linkset assets. #![allow(clippy::missing_errors_doc)] #![allow(clippy::too_many_lines)] #![allow(clippy::format_push_string)] use std::fmt::Write as _; use std::str::FromStr; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::Engine as _; use libremetaverse_types::compat::XmlTextReader; use libremetaverse_types::{AssetType, Color4, InventoryType, Quaternion, UUID, Vector3}; use roxmltree::Node; use crate::prim_object::{PrimObject, PrimObjectInventoryBlockItemBlock, asset_type}; use crate::{Error, Primitive, PrimitiveTextureEntry}; const MAX_XML_BYTES: usize = 64 * 1024 * 1024; #[derive(Clone, Debug, PartialEq)] pub struct AssetPrim { pub children: Vec, pub parent: Option, asset_data: Vec, asset_id: UUID, } fn escape(value: &str) -> String { value .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn element(output: &mut String, name: &str, value: impl std::fmt::Display) { let _ = write!(output, "<{name}>{}", escape(&value.to_string())); } fn uuid_element(output: &mut String, name: &str, value: UUID) { let _ = write!(output, "<{name}>{value}"); } fn vector_element(output: &mut String, name: &str, value: Vector3) { let _ = write!( output, "<{name}>{}{}{}", value.x, value.y, value.z ); } fn quaternion_element(output: &mut String, name: &str, value: Quaternion) { let _ = write!( output, "<{name}>{}{}{}{}", value.x, value.y, value.z, value.w ); } fn color_element(output: &mut String, name: &str, value: Color4) { let _ = write!( output, "<{name}>{}{}{}{}", value.r, value.g, value.b, value.a ); } fn unix_seconds(value: SystemTime) -> u64 { value .duration_since(UNIX_EPOCH) .unwrap_or(Duration::ZERO) .as_secs() } fn child<'a, 'input>(node: Node<'a, 'input>, name: &str) -> Option> { node.children() .find(|candidate| candidate.is_element() && candidate.tag_name().name() == name) } fn text(node: Node<'_, '_>, name: &str) -> Option { child(node, name) .and_then(|value| value.text()) .map(str::trim) .map(str::to_owned) } fn parsed(node: Node<'_, '_>, name: &str, default: T) -> Result where T: FromStr, { text(node, name).map_or(Ok(default), |value| { value.parse().map_err(|_| Error::Argument) }) } fn boolean(node: Node<'_, '_>, name: &str, default: bool) -> Result { text(node, name).map_or(Ok(default), |value| { match value.to_ascii_lowercase().as_str() { "true" | "1" => Ok(true), "false" | "0" => Ok(false), _ => Err(Error::Argument), } }) } fn uuid(node: Node<'_, '_>, name: &str) -> Result { let Some(container) = child(node, name) else { return Ok(UUID::zero()); }; let value = container .descendants() .find(|candidate| { candidate.is_element() && matches!(candidate.tag_name().name(), "UUID" | "Guid") }) .and_then(|candidate| candidate.text()) .or_else(|| container.text()) .map(str::trim) .unwrap_or_default(); if value.is_empty() { Ok(UUID::zero()) } else { UUID::new_with_string(value.to_owned()) } } fn vector(node: Node<'_, '_>, name: &str) -> Result { let Some(value) = child(node, name) else { return Ok(Vector3::zero()); }; Ok(Vector3 { x: parsed(value, "X", 0.0_f32)?, y: parsed(value, "Y", 0.0_f32)?, z: parsed(value, "Z", 0.0_f32)?, }) } fn quaternion(node: Node<'_, '_>, name: &str) -> Result { let Some(value) = child(node, name) else { return Ok(Quaternion::identity()); }; Quaternion::new_with_single_single_single_single( parsed(value, "X", 0.0_f32)?, parsed(value, "Y", 0.0_f32)?, parsed(value, "Z", 0.0_f32)?, parsed(value, "W", 1.0_f32)?, ) } fn color(node: Node<'_, '_>, name: &str) -> Result { let Some(value) = child(node, name) else { return Ok(Color4::default()); }; Ok(Color4 { r: parsed(value, "R", 0.0_f32)?.clamp(0.0, 1.0), g: parsed(value, "G", 0.0_f32)?.clamp(0.0, 1.0), b: parsed(value, "B", 0.0_f32)?.clamp(0.0, 1.0), a: parsed(value, "A", 0.0_f32)?.clamp(0.0, 1.0), }) } fn write_inventory(output: &mut String, prim: &PrimObject) { output.push_str(""); for item in &prim.inventory.items { output.push_str(""); uuid_element(output, "AssetID", item.asset_id); element(output, "BasePermissions", item.perms_base); element(output, "CreationDate", unix_seconds(item.creation_date)); uuid_element(output, "CreatorID", item.creator_id); element(output, "Description", &item.description); element(output, "EveryonePermissions", item.perms_everyone); element(output, "Flags", item.flags); uuid_element(output, "GroupID", item.group_id); element(output, "GroupPermissions", item.perms_group); element(output, "InvType", item.inv_type.raw()); uuid_element(output, "ItemID", item.id); uuid_element(output, "OldItemID", UUID::zero()); uuid_element(output, "LastOwnerID", item.last_owner_id); element(output, "Name", &item.name); element(output, "NextPermissions", item.perms_next_owner); uuid_element(output, "OwnerID", item.owner_id); element(output, "CurrentPermissions", item.perms_owner); uuid_element(output, "ParentID", prim.id); uuid_element(output, "ParentPartID", prim.id); uuid_element(output, "PermsGranter", item.perms_granter_id); element(output, "PermsMask", item.perms_base); element(output, "Type", item.type_ as i8); element(output, "OwnerChanged", false); output.push_str(""); } output.push_str(""); } fn read_inventory(node: Node<'_, '_>) -> Result, Error> { let Some(inventory) = child(node, "TaskInventory") else { return Ok(Vec::new()); }; inventory .children() .filter(|entry| entry.is_element() && entry.tag_name().name() == "TaskInventoryItem") .map(|entry| { let inv_type = parsed(entry, "InvType", -1_i32)?; Ok(PrimObjectInventoryBlockItemBlock { asset_id: uuid(entry, "AssetID")?, creation_date: UNIX_EPOCH .checked_add(Duration::from_secs(parsed(entry, "CreationDate", 0_u64)?)) .ok_or(Error::Argument)?, creator_id: uuid(entry, "CreatorID")?, description: text(entry, "Description").unwrap_or_default(), flags: parsed(entry, "Flags", 0_i32)?, group_id: uuid(entry, "GroupID")?, id: uuid(entry, "ItemID")?, inv_type: InventoryType::from_raw( i8::try_from(inv_type).map_err(|_| Error::Argument)?, ), last_owner_id: uuid(entry, "LastOwnerID")?, name: text(entry, "Name").unwrap_or_default(), owner_id: uuid(entry, "OwnerID")?, perms_base: parsed(entry, "BasePermissions", 0_u32)?, perms_everyone: parsed(entry, "EveryonePermissions", 0_u32)?, perms_granter_id: uuid(entry, "PermsGranter")?, perms_group: parsed(entry, "GroupPermissions", 0_u32)?, perms_next_owner: parsed(entry, "NextPermissions", 0_u32)?, perms_owner: parsed(entry, "CurrentPermissions", 0_u32)?, type_: asset_type(parsed(entry, "Type", -1_i32)?)?, }) }) .collect() } fn write_prim( output: &mut String, prim: &PrimObject, parent: Option<&PrimObject>, ) -> Result<(), Error> { output.push_str(""); element(output, "AllowedDrop", prim.allowed_drop); uuid_element(output, "CreatorID", prim.creator_id); uuid_element(output, "FolderID", prim.folder_id); element(output, "InventorySerial", prim.inventory.serial); write_inventory(output, prim); let mut flags = 0_u32; for (enabled, flag) in [ (prim.use_physics, libremetaverse_types::PrimFlags::PHYSICS), (prim.phantom, libremetaverse_types::PrimFlags::PHANTOM), ( prim.die_at_edge, libremetaverse_types::PrimFlags::DIE_AT_EDGE, ), ( prim.return_at_edge, libremetaverse_types::PrimFlags::RETURN_AT_EDGE, ), (prim.temporary, libremetaverse_types::PrimFlags::TEMPORARY), (prim.sandbox, libremetaverse_types::PrimFlags::SANDBOX), ( prim.allowed_drop, libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP, ), ] { if enabled { flags |= flag.0; } } element(output, "ObjectFlags", flags); uuid_element(output, "UUID", prim.id); element(output, "LocalId", prim.local_id); element(output, "Name", &prim.name); element(output, "Material", prim.material); element(output, "PassTouches", prim.pass_touches); element(output, "RegionHandle", prim.region_handle); element(output, "ScriptAccessPin", prim.remote_script_access_pin); vector_element( output, "GroupPosition", parent.map_or(prim.position, |root| root.position), ); vector_element( output, "OffsetPosition", if parent.is_some() { prim.position } else { Vector3::zero() }, ); quaternion_element(output, "RotationOffset", prim.rotation); vector_element(output, "Velocity", prim.velocity); vector_element(output, "RotationalVelocity", Vector3::zero()); vector_element(output, "AngularVelocity", prim.angular_velocity); vector_element(output, "Acceleration", prim.acceleration); element(output, "Description", &prim.description); color_element(output, "Color", prim.text_color); element(output, "Text", &prim.text); element(output, "SitName", &prim.sit_name); element(output, "TouchName", &prim.touch_name); element(output, "LinkNum", prim.link_number); element(output, "ClickAction", prim.click_action); output.push_str(""); element( output, "PathBegin", Primitive::pack_begin_cut(prim.shape.path_begin)?, ); element(output, "PathCurve", prim.shape.path_curve); element( output, "PathEnd", Primitive::pack_end_cut(prim.shape.path_end)?, ); element( output, "PathRadiusOffset", Primitive::pack_path_twist(prim.shape.path_radius_offset)?, ); element( output, "PathRevolutions", Primitive::pack_path_revolutions(prim.shape.path_revolutions)?, ); element( output, "PathScaleX", Primitive::pack_path_scale(prim.shape.path_scale_x)?, ); element( output, "PathScaleY", Primitive::pack_path_scale(prim.shape.path_scale_y)?, ); element( output, "PathShearX", Primitive::pack_path_shear(prim.shape.path_shear_x)?, ); element( output, "PathShearY", Primitive::pack_path_shear(prim.shape.path_shear_y)?, ); element( output, "PathSkew", Primitive::pack_path_twist(prim.shape.path_skew)?, ); element( output, "PathTaperX", Primitive::pack_path_taper(prim.shape.path_taper_x)?, ); element( output, "PathTaperY", Primitive::pack_path_taper(prim.shape.path_taper_y)?, ); element( output, "PathTwist", Primitive::pack_path_twist(prim.shape.path_twist)?, ); element( output, "PathTwistBegin", Primitive::pack_path_twist(prim.shape.path_twist_begin)?, ); element(output, "PCode", prim.p_code); element( output, "ProfileBegin", Primitive::pack_begin_cut(prim.shape.profile_begin)?, ); element( output, "ProfileEnd", Primitive::pack_end_cut(prim.shape.profile_end)?, ); element( output, "ProfileHollow", Primitive::pack_profile_hollow(prim.shape.profile_hollow)?, ); vector_element(output, "Scale", prim.scale); element(output, "State", prim.state); element(output, "ProfileShape", prim.shape.profile_curve & 0x0f); element(output, "HollowShape", prim.shape.profile_curve & 0xf0); element(output, "ProfileCurve", prim.shape.profile_curve); let texture_bytes = prim .textures .as_ref() .map(PrimitiveTextureEntry::get_bytes) .transpose()? .unwrap_or_default(); element( output, "TextureEntry", base64::engine::general_purpose::STANDARD.encode(texture_bytes), ); output.push_str(""); uuid_element(output, "SculptTexture", prim.sculpt.texture); element(output, "SculptType", prim.sculpt.type_); output.push_str(""); element(output, "FlexiSoftness", prim.flexible.softness); element(output, "FlexiTension", prim.flexible.tension); element(output, "FlexiDrag", prim.flexible.drag); element(output, "FlexiGravity", prim.flexible.gravity); element(output, "FlexiWind", prim.flexible.wind); element(output, "FlexiForceX", prim.flexible.force.x); element(output, "FlexiForceY", prim.flexible.force.y); element(output, "FlexiForceZ", prim.flexible.force.z); element(output, "LightColorR", prim.light.color.r); element(output, "LightColorG", prim.light.color.g); element(output, "LightColorB", prim.light.color.b); element(output, "LightColorA", prim.light.color.a); element(output, "LightRadius", prim.light.radius); element(output, "LightCutoff", prim.light.cutoff); element(output, "LightFalloff", prim.light.falloff); element(output, "LightIntensity", prim.light.intensity); element( output, "FlexiEntry", prim.flexible != crate::prim_object::PrimObjectFlexibleBlock::new()?, ); element( output, "LightEntry", prim.light != crate::prim_object::PrimObjectLightBlock::new()?, ); element(output, "SculptEntry", prim.sculpt.texture != UUID::zero()); output.push_str(""); vector_element(output, "Scale", prim.scale); element(output, "UpdateFlag", 0); quaternion_element(output, "SitTargetOrientation", prim.sit_rotation); vector_element(output, "SitTargetPosition", prim.sit_offset); vector_element(output, "SitTargetPositionLL", prim.sit_offset); quaternion_element(output, "SitTargetOrientationLL", prim.sit_rotation); element(output, "ParentID", prim.parent_id); element(output, "CreationDate", unix_seconds(prim.creation_date)); element(output, "RezDate", unix_seconds(prim.rez_date)); element(output, "Category", 0); element(output, "SalePrice", prim.sale_price); element(output, "ObjectSaleType", prim.sale_type); element(output, "OwnershipCost", 0); uuid_element(output, "GroupID", prim.group_id); uuid_element(output, "OwnerID", prim.owner_id); uuid_element(output, "LastOwnerID", prim.last_owner_id); element(output, "BaseMask", prim.perms_base); element(output, "OwnerMask", prim.perms_owner); element(output, "GroupMask", prim.perms_group); element(output, "EveryoneMask", prim.perms_everyone); element(output, "NextOwnerMask", prim.perms_next_owner); element(output, "Flags", "None"); uuid_element(output, "SitTargetAvatar", UUID::zero()); uuid_element(output, "CollisionSound", prim.collision_sound); element(output, "CollisionSoundVolume", prim.collision_sound_volume); element( output, "ScriptState", base64::engine::general_purpose::STANDARD.encode(&prim.script_state), ); output.push_str(""); Ok(()) } fn read_prim(node: Node<'_, '_>) -> Result { let mut prim = PrimObject::new()?; prim.allowed_drop = boolean(node, "AllowedDrop", true)?; prim.creator_id = uuid(node, "CreatorID")?; prim.folder_id = uuid(node, "FolderID")?; prim.inventory.serial = parsed(node, "InventorySerial", 0_i32)?; prim.inventory.items = read_inventory(node)?; let flags = parsed(node, "ObjectFlags", 0_u32)?; prim.use_physics = flags & libremetaverse_types::PrimFlags::PHYSICS.0 != 0; prim.phantom = flags & libremetaverse_types::PrimFlags::PHANTOM.0 != 0; prim.die_at_edge = flags & libremetaverse_types::PrimFlags::DIE_AT_EDGE.0 != 0; prim.return_at_edge = flags & libremetaverse_types::PrimFlags::RETURN_AT_EDGE.0 != 0; prim.temporary = flags & libremetaverse_types::PrimFlags::TEMPORARY.0 != 0; prim.sandbox = flags & libremetaverse_types::PrimFlags::SANDBOX.0 != 0; prim.allowed_drop |= flags & libremetaverse_types::PrimFlags::ALLOW_INVENTORY_DROP.0 != 0; prim.id = uuid(node, "UUID")?; prim.local_id = parsed(node, "LocalId", 0_u32)?; prim.name = text(node, "Name").unwrap_or_default(); prim.material = parsed(node, "Material", 0_i32)?; prim.pass_touches = boolean(node, "PassTouches", false)?; prim.region_handle = parsed(node, "RegionHandle", 0_u64)?; prim.remote_script_access_pin = parsed(node, "ScriptAccessPin", 0_i32)?; let group_position = vector(node, "GroupPosition")?; let offset_position = vector(node, "OffsetPosition")?; prim.rotation = quaternion(node, "RotationOffset")?; prim.velocity = vector(node, "Velocity")?; prim.angular_velocity = vector(node, "AngularVelocity")?; prim.acceleration = vector(node, "Acceleration")?; prim.description = text(node, "Description").unwrap_or_default(); prim.text_color = color(node, "Color")?; prim.text = text(node, "Text").unwrap_or_default(); prim.sit_name = text(node, "SitName").unwrap_or_default(); prim.touch_name = text(node, "TouchName").unwrap_or_default(); prim.link_number = parsed(node, "LinkNum", 0_i32)?; prim.click_action = parsed(node, "ClickAction", 0_i32)?; if let Some(shape) = child(node, "Shape") { prim.shape.path_begin = Primitive::unpack_begin_cut(parsed(shape, "PathBegin", 0_u16)?)?; prim.shape.path_curve = parsed(shape, "PathCurve", 0_i32)?; prim.shape.path_end = Primitive::unpack_end_cut(parsed(shape, "PathEnd", 0_u16)?)?; prim.shape.path_radius_offset = Primitive::unpack_path_twist(parsed(shape, "PathRadiusOffset", 0_i8)?)?; prim.shape.path_revolutions = Primitive::unpack_path_revolutions(parsed(shape, "PathRevolutions", 0_u8)?)?; prim.shape.path_scale_x = Primitive::unpack_path_scale(parsed(shape, "PathScaleX", 0_u8)?)?; prim.shape.path_scale_y = Primitive::unpack_path_scale(parsed(shape, "PathScaleY", 0_u8)?)?; prim.shape.path_shear_x = Primitive::unpack_path_shear(parsed(shape, "PathShearX", 0_i8)?)?; prim.shape.path_shear_y = Primitive::unpack_path_shear(parsed(shape, "PathShearY", 0_i8)?)?; prim.shape.path_skew = Primitive::unpack_path_twist(parsed(shape, "PathSkew", 0_i8)?)?; prim.shape.path_taper_x = Primitive::unpack_path_taper(parsed(shape, "PathTaperX", 0_i8)?)?; prim.shape.path_taper_y = Primitive::unpack_path_taper(parsed(shape, "PathTaperY", 0_i8)?)?; prim.shape.path_twist = Primitive::unpack_path_twist(parsed(shape, "PathTwist", 0_i8)?)?; prim.shape.path_twist_begin = Primitive::unpack_path_twist(parsed(shape, "PathTwistBegin", 0_i8)?)?; prim.p_code = parsed(shape, "PCode", 0_i32)?; prim.shape.profile_begin = Primitive::unpack_begin_cut(parsed(shape, "ProfileBegin", 0_u16)?)?; prim.shape.profile_end = Primitive::unpack_end_cut(parsed(shape, "ProfileEnd", 0_u16)?)?; prim.shape.profile_hollow = Primitive::unpack_profile_hollow(parsed(shape, "ProfileHollow", 0_u16)?)?; prim.scale = vector(shape, "Scale")?; prim.state = parsed(shape, "State", 0_i32)?; prim.shape.profile_curve = parsed(shape, "ProfileCurve", 0_i32)?; if let Some(encoded) = text(shape, "TextureEntry") { let bytes = base64::engine::general_purpose::STANDARD .decode(encoded) .map_err(|_| Error::Argument)?; if !bytes.is_empty() { prim.textures = Some(PrimitiveTextureEntry::new_with_bytes_int32_int32( bytes.clone(), 0, i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, )?); } } prim.sculpt.texture = uuid(shape, "SculptTexture")?; prim.sculpt.type_ = parsed(shape, "SculptType", 0_i32)?; prim.flexible.softness = parsed(shape, "FlexiSoftness", 0_i32)?; prim.flexible.tension = parsed(shape, "FlexiTension", 0.0_f32)?; prim.flexible.drag = parsed(shape, "FlexiDrag", 0.0_f32)?; prim.flexible.gravity = parsed(shape, "FlexiGravity", 0.0_f32)?; prim.flexible.wind = parsed(shape, "FlexiWind", 0.0_f32)?; prim.flexible.force = Vector3 { x: parsed(shape, "FlexiForceX", 0.0_f32)?, y: parsed(shape, "FlexiForceY", 0.0_f32)?, z: parsed(shape, "FlexiForceZ", 0.0_f32)?, }; prim.light.color = Color4 { r: parsed(shape, "LightColorR", 0.0_f32)?, g: parsed(shape, "LightColorG", 0.0_f32)?, b: parsed(shape, "LightColorB", 0.0_f32)?, a: parsed(shape, "LightColorA", 0.0_f32)?, }; prim.light.radius = parsed(shape, "LightRadius", 0.0_f32)?; prim.light.cutoff = parsed(shape, "LightCutoff", 0.0_f32)?; prim.light.falloff = parsed(shape, "LightFalloff", 0.0_f32)?; prim.light.intensity = parsed(shape, "LightIntensity", 0.0_f32)?; } prim.scale = child(node, "Scale").map_or(Ok(prim.scale), |_| vector(node, "Scale"))?; prim.sit_offset = vector(node, "SitTargetPositionLL")?; prim.sit_rotation = quaternion(node, "SitTargetOrientationLL")?; prim.parent_id = parsed(node, "ParentID", 0_u32)?; prim.position = if prim.parent_id == 0 { group_position } else { offset_position }; prim.creation_date = UNIX_EPOCH .checked_add(Duration::from_secs(parsed(node, "CreationDate", 0_u64)?)) .ok_or(Error::Argument)?; prim.rez_date = UNIX_EPOCH .checked_add(Duration::from_secs(parsed(node, "RezDate", 0_u64)?)) .ok_or(Error::Argument)?; prim.sale_price = parsed(node, "SalePrice", 0_i32)?; prim.sale_type = parsed(node, "ObjectSaleType", 0_i32)?; prim.group_id = uuid(node, "GroupID")?; prim.owner_id = uuid(node, "OwnerID")?; prim.last_owner_id = uuid(node, "LastOwnerID")?; prim.perms_base = parsed(node, "BaseMask", 0_u32)?; prim.perms_owner = parsed(node, "OwnerMask", 0_u32)?; prim.perms_group = parsed(node, "GroupMask", 0_u32)?; prim.perms_everyone = parsed(node, "EveryoneMask", 0_u32)?; prim.perms_next_owner = parsed(node, "NextOwnerMask", 0_u32)?; prim.collision_sound = uuid(node, "CollisionSound")?; prim.collision_sound_volume = parsed(node, "CollisionSoundVolume", 0.0_f32)?; if let Some(encoded) = text(node, "ScriptState") { prim.script_state = base64::engine::general_purpose::STANDARD .decode(encoded) .map_err(|_| Error::Argument)?; } Ok(prim) } impl AssetPrim { pub fn new_with_constructor() -> Result { Ok(Self { children: Vec::new(), parent: None, asset_data: Vec::new(), asset_id: UUID::zero(), }) } pub fn new_with_prim_object_list( parent: PrimObject, children: Vec, ) -> Result { Ok(Self { asset_id: parent.id, children, parent: Some(parent), asset_data: Vec::new(), }) } pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec) -> Result { if asset_data.len() > MAX_XML_BYTES { return Err(Error::Argument); } Ok(Self { children: Vec::new(), parent: None, asset_data, asset_id, }) } pub fn new_with_string(xml_data: String) -> Result { let mut value = Self::new_with_uuid_bytes(UUID::zero(), xml_data.as_bytes().to_vec())?; if !value.decode_xml(xml_data)? { return Err(Error::Argument); } Ok(value) } pub const fn asset_type(&self) -> AssetType { AssetType::Object } pub const fn asset_id(&self) -> UUID { self.asset_id } pub fn native_bytes(&self) -> &[u8] { &self.asset_data } pub fn encode(&mut self) -> Result<(), Error> { self.asset_data = self.encode_xml()?.into_bytes(); Ok(()) } pub fn decode(&mut self) -> Result { let Ok(xml) = String::from_utf8(self.asset_data.clone()) else { return Ok(false); }; match self.decode_xml(xml) { Ok(value) => Ok(value), Err(_) => Ok(false), } } pub fn encode_xml(&self) -> Result { let mut output = String::from(""); if let Some(parent) = &self.parent { write_prim(&mut output, parent, None)?; } output.push_str(""); for child in &self.children { write_prim(&mut output, child, self.parent.as_ref())?; } output.push_str(""); Ok(output) } pub fn decode_xml(&mut self, xml_data: String) -> Result { if xml_data.len() > MAX_XML_BYTES { return Err(Error::Argument); } let document = roxmltree::Document::parse(&xml_data).map_err(|_| Error::Argument)?; let root = document.root_element(); if root.tag_name().name() != "SceneObjectGroup" { return Ok(false); } let Some(parent_node) = root .children() .find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart") else { return Ok(false); }; let parent = read_prim(parent_node)?; let children = child(root, "OtherParts") .into_iter() .flat_map(|container| container.children()) .filter(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart") .map(read_prim) .collect::, _>>()?; if self.asset_id == UUID::zero() { self.asset_id = parent.id; } self.parent = Some(parent); self.children = children; self.asset_data = xml_data.into_bytes(); Ok(true) } pub fn load_prim(reader: XmlTextReader) -> Result { if reader.0.len() > MAX_XML_BYTES { return Err(Error::Argument); } let document = roxmltree::Document::parse(&reader.0).map_err(|_| Error::Argument)?; let node = document .descendants() .find(|node| node.is_element() && node.tag_name().name() == "SceneObjectPart") .ok_or(Error::Argument)?; read_prim(node) } } #[cfg(test)] mod tests { use super::*; #[test] fn xml2_round_trip_preserves_linkset_and_inventory() { let mut root = PrimObject::new().unwrap(); root.id = UUID::new_with_string("7bd3f7e3-2afc-44aa-a495-b741e4189f61".into()).unwrap(); root.name = "OpenSim root & ".into(); root.position = Vector3 { x: 128.0, y: 64.0, z: 25.0, }; root.p_code = 9; root.shape.path_end = 1.0; root.shape.path_scale_x = 1.0; root.shape.path_scale_y = 1.0; root.shape.path_revolutions = 1.0; root.shape.profile_end = 1.0; root.script_state = vec![1, 2, 3]; let mut item = PrimObjectInventoryBlockItemBlock::new().unwrap(); item.name = "script".into(); item.type_ = AssetType::LSLText; root.inventory.items.push(item); let mut child = root.clone(); child.id = UUID::new_with_string("ff6863c7-7acb-4e81-a28b-f0f162f06455".into()).unwrap(); child.name = "child".into(); child.parent_id = root.local_id.max(1); child.position = Vector3 { x: 1.0, y: 2.0, z: 3.0, }; let asset = AssetPrim::new_with_prim_object_list(root.clone(), vec![child.clone()]).unwrap(); let xml = asset.encode_xml().unwrap(); let decoded = AssetPrim::new_with_string(xml).unwrap(); assert_eq!(decoded.parent.as_ref().unwrap().name, root.name); assert_eq!(decoded.children.len(), 1); assert_eq!(decoded.children[0].position, child.position); assert_eq!(decoded.children[0].script_state, child.script_state); assert_eq!(decoded.asset_type(), AssetType::Object); } #[test] fn malformed_or_non_group_xml_is_rejected_without_partial_state() { let mut asset = AssetPrim::new_with_constructor().unwrap(); assert!(!asset.decode_xml("".into()).unwrap()); assert!(asset.decode_xml("".into()).is_err()); assert!(asset.parent.is_none()); } }