//! Native skeleton records, traversal helpers, and custom XML loading. #![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the compatibility catalog. #![allow(clippy::must_use_candidate)] // Getters mirror C# properties. #![allow(clippy::needless_pass_by_value)] // Owned strings are fixed by the compatibility API. #![allow(clippy::unnecessary_wraps)] // Constructors mirror fallible mapped signatures. use roxmltree::{Document, Node}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; const MAX_SKELETON_BYTES: u64 = 8 * 1024 * 1024; const MAX_SKELETON_BONES: usize = 512; const MAX_COLLISION_VOLUMES: usize = 512; const MAX_SKELETON_DEPTH: usize = 128; #[derive(Clone, Debug, Default, PartialEq)] pub struct JointBase { name: String, pos: Vec, rot: Vec, scale: Vec, group: String, support: String, end: Vec, reposition: bool, } impl JointBase { pub fn new() -> Result { Ok(Self::default()) } pub fn support_category(&self) -> crate::rendering::JointSupportCategory { if self.support.eq_ignore_ascii_case("extended") { crate::rendering::JointSupportCategory::Extended } else { crate::rendering::JointSupportCategory::Base } } pub fn end(&self) -> Vec { self.end.clone() } pub fn set_end(&mut self, value: Vec) { self.end = value; } pub fn group(&self) -> String { self.group.clone() } pub fn set_group(&mut self, value: String) { self.group = value; } pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; } pub fn pos(&self) -> Vec { self.pos.clone() } pub fn set_pos(&mut self, value: Vec) { self.pos = value; } pub fn reposition(&self) -> bool { self.reposition } pub fn set_reposition(&mut self, value: bool) { self.reposition = value; } pub fn rot(&self) -> Vec { self.rot.clone() } pub fn set_rot(&mut self, value: Vec) { self.rot = value; } pub fn scale(&self) -> Vec { self.scale.clone() } pub fn set_scale(&mut self, value: Vec) { self.scale = value; } pub fn support(&self) -> String { self.support.clone() } pub fn set_support(&mut self, value: String) { self.support = value; } #[allow(clippy::too_many_arguments)] pub(crate) fn from_parts( name: String, pos: Vec, rot: Vec, scale: Vec, group: String, support: String, end: Vec, reposition: bool, ) -> Self { Self { name, pos, rot, scale, group, support, end, reposition, } } } #[derive(Clone, Debug, Default, PartialEq)] pub struct CollisionVolume { pub base: JointBase, } impl CollisionVolume { pub fn new() -> Result { Ok(Self::default()) } pub(crate) fn from_base(base: JointBase) -> Self { Self { base } } } #[derive(Clone, Debug, PartialEq)] pub struct Joint { pub base: JointBase, collision_volume: Vec, bone: Option>, pivot: Vec, aliases: String, connected: bool, } impl Default for Joint { fn default() -> Self { Self { base: JointBase::default(), collision_volume: Vec::new(), bone: Some(Vec::new()), pivot: Vec::new(), aliases: String::new(), connected: false, } } } impl Joint { pub fn new() -> Result { Ok(Self::default()) } pub fn get_aliases_list(&self) -> Result, crate::Error> { Ok(self .aliases .split_ascii_whitespace() .map(str::to_owned) .collect()) } pub fn aliases(&self) -> String { self.aliases.clone() } pub fn set_aliases(&mut self, value: String) { self.aliases = value; } pub fn bone(&self) -> Option> { self.bone.clone() } pub fn set_bone(&mut self, value: Option>) { self.bone = value; } pub fn collision_volume(&self) -> Vec { self.collision_volume.clone() } pub fn set_collision_volume(&mut self, value: Vec) { self.collision_volume = value; } pub fn connected(&self) -> bool { self.connected } pub fn set_connected(&mut self, value: bool) { self.connected = value; } pub fn pivot(&self) -> Vec { self.pivot.clone() } pub fn set_pivot(&mut self, value: Vec) { self.pivot = value; } pub(crate) fn from_parts( base: JointBase, collision_volume: Vec, bone: Vec, pivot: Vec, aliases: String, connected: bool, ) -> Self { Self { base, collision_volume, bone: Some(bone), pivot, aliases, connected, } } } #[derive(Clone, Debug, PartialEq)] pub struct LindenSkeleton { bone: Joint, version: f32, version_specified: bool, num_bones: String, num_collision_volumes: String, } impl Default for LindenSkeleton { fn default() -> Self { Self { bone: Joint::default(), version: 0.0, version_specified: false, num_bones: String::new(), num_collision_volumes: String::new(), } } } impl LindenSkeleton { pub fn new() -> Result { Ok(Self::default()) } pub fn build_expanded_joint_list( &self, joints_filter: Box>, ) -> Result, crate::Error> { let filter = joints_filter.collect::>(); let mut expanded = Vec::new(); if let Some(children) = &self.bone.bone { for child in children { expand_joint(child, &self.bone.base.name, &mut expanded, &filter); } } Ok(expanded) } pub fn build_joint_dictionary(&self) -> Result>, crate::Error> { let mut dictionary = HashMap::new(); for joint in collect_joints(&self.bone) { if !joint.base.name.is_empty() { dictionary.insert(joint.base.name.clone(), Arc::clone(&joint)); } for alias in joint.aliases.split_ascii_whitespace() { if !alias.is_empty() { dictionary.insert(alias.to_owned(), Arc::clone(&joint)); } } } Ok(dictionary) } pub fn get_all_joints(&self) -> Result>>, crate::Error> { Ok(Box::new(collect_joints(&self.bone).into_iter())) } pub fn get_bone(&self, name_or_alias: String) -> Result>, crate::Error> { if name_or_alias.is_empty() { return Ok(None); } Ok(self.build_joint_dictionary()?.remove(&name_or_alias)) } pub fn get_default() -> Result { Ok(crate::skeleton_catalog::default_skeleton()) } pub fn load_with_method() -> Result { Self::get_default() } pub fn load_with_string(file_name: Option) -> Result { let Some(file_name) = file_name else { return Self::get_default(); }; if std::fs::metadata(&file_name) .map_err(|_| crate::Error::Argument)? .len() > MAX_SKELETON_BYTES { return Err(crate::Error::Argument); } let text = std::fs::read_to_string(file_name).map_err(|_| crate::Error::Argument)?; if text.len() as u64 > MAX_SKELETON_BYTES { return Err(crate::Error::Argument); } parse_skeleton_xml(&text) } pub fn bone(&self) -> Joint { self.bone.clone() } pub fn set_bone(&mut self, value: Joint) { self.bone = value; } pub fn num_bones(&self) -> String { self.num_bones.clone() } pub fn set_num_bones(&mut self, value: String) { self.num_bones = value; } pub fn num_collision_volumes(&self) -> String { self.num_collision_volumes.clone() } pub fn set_num_collision_volumes(&mut self, value: String) { self.num_collision_volumes = value; } pub fn version(&self) -> f32 { self.version } pub fn set_version(&mut self, value: f32) { self.version = value; } pub fn version_specified(&self) -> bool { self.version_specified } pub fn set_version_specified(&mut self, value: bool) { self.version_specified = value; } pub(crate) fn from_parts( bone: Joint, version: f32, version_specified: bool, num_bones: String, num_collision_volumes: String, ) -> Self { Self { bone, version, version_specified, num_bones, num_collision_volumes, } } } fn collect_joints(root: &Joint) -> Vec> { fn visit(joint: &Joint, output: &mut Vec>) { output.push(Arc::new(joint.clone())); if let Some(children) = &joint.bone { for child in children { visit(child, output); } } } let mut output = Vec::new(); visit(root, &mut output); output } fn expand_joint( current: &Joint, effective_parent: &str, expanded: &mut Vec, filter: &[String], ) { let mut next_effective_parent = effective_parent; if filter.iter().any(|name| name == ¤t.base.name) { let parent_matches_last = expanded.last().is_some_and(|last| last == effective_parent); if !parent_matches_last && filter.iter().any(|name| name == effective_parent) { expanded.push(effective_parent.to_owned()); } expanded.push(current.base.name.clone()); next_effective_parent = ¤t.base.name; } if let Some(children) = ¤t.bone { for child in children { expand_joint(child, next_effective_parent, expanded, filter); } } } fn parse_error(position: usize) -> crate::Error { crate::Error::Parse { position, context: "avatar skeleton XML", } } fn node_position(node: Node<'_, '_>) -> usize { node.range().start } fn required_attribute<'a>(node: Node<'a, '_>, name: &str) -> Result<&'a str, crate::Error> { node.attribute(name) .filter(|value| !value.is_empty()) .ok_or_else(|| parse_error(node_position(node))) } fn vector_attribute(node: Node<'_, '_>, name: &str) -> Result, crate::Error> { let Some(value) = node.attribute(name) else { return Ok(Vec::new()); }; let values = value .split_ascii_whitespace() .map(str::parse::) .collect::, _>>() .map_err(|_| parse_error(node_position(node)))?; if values.len() != 3 || values.iter().any(|value| !value.is_finite()) { return Err(parse_error(node_position(node))); } Ok(values) } fn boolean_attribute(node: Node<'_, '_>, name: &str) -> Result { match node.attribute(name) { None => Ok(false), Some(value) if value.eq_ignore_ascii_case("true") => Ok(true), Some(value) if value.eq_ignore_ascii_case("false") => Ok(false), Some(_) => Err(parse_error(node_position(node))), } } fn parse_base(node: Node<'_, '_>) -> Result { let support = node.attribute("support").unwrap_or_default(); if !support.is_empty() && !support.eq_ignore_ascii_case("base") && !support.eq_ignore_ascii_case("extended") { return Err(parse_error(node_position(node))); } Ok(JointBase::from_parts( required_attribute(node, "name")?.to_owned(), vector_attribute(node, "pos")?, vector_attribute(node, "rot")?, vector_attribute(node, "scale")?, node.attribute("group").unwrap_or_default().to_owned(), support.to_owned(), vector_attribute(node, "end")?, boolean_attribute(node, "reposition")?, )) } fn parse_joint( node: Node<'_, '_>, names: &mut HashSet, depth: usize, bone_count: &mut usize, collision_volume_count: &mut usize, ) -> Result { if !node.has_tag_name("bone") { return Err(parse_error(node_position(node))); } if depth > MAX_SKELETON_DEPTH || *bone_count >= MAX_SKELETON_BONES { return Err(parse_error(node_position(node))); } *bone_count += 1; let base = parse_base(node)?; if !names.insert(base.name.clone()) { return Err(parse_error(node_position(node))); } let aliases = node.attribute("aliases").unwrap_or_default().to_owned(); for alias in aliases.split_ascii_whitespace() { if !names.insert(alias.to_owned()) { return Err(parse_error(node_position(node))); } } let mut collision_volumes = Vec::new(); let mut bones = Vec::new(); for child in node.children().filter(Node::is_element) { if child.has_tag_name("collision_volume") { if *collision_volume_count >= MAX_COLLISION_VOLUMES { return Err(parse_error(node_position(child))); } if child.children().any(|node| node.is_element()) { return Err(parse_error(node_position(child))); } collision_volumes.push(CollisionVolume::from_base(parse_base(child)?)); *collision_volume_count += 1; } else if child.has_tag_name("bone") { bones.push(parse_joint( child, names, depth + 1, bone_count, collision_volume_count, )?); } else { return Err(parse_error(node_position(child))); } } Ok(Joint::from_parts( base, collision_volumes, bones, vector_attribute(node, "pivot")?, aliases, boolean_attribute(node, "connected")?, )) } pub(crate) fn parse_skeleton_xml(text: &str) -> Result { if text.len() as u64 > MAX_SKELETON_BYTES { return Err(parse_error(0)); } let document = Document::parse(text).map_err(|_| parse_error(0))?; let root = document.root_element(); if !root.has_tag_name("linden_skeleton") { return Err(parse_error(node_position(root))); } let version_text = required_attribute(root, "version")?; let version = version_text .parse::() .map_err(|_| parse_error(node_position(root)))?; if !version.is_finite() { return Err(parse_error(node_position(root))); } let num_bones = required_attribute(root, "num_bones")?.to_owned(); let expected_bones = num_bones .parse::() .map_err(|_| parse_error(node_position(root)))?; let num_collision_volumes = required_attribute(root, "num_collision_volumes")?.to_owned(); let expected_collision_volumes = num_collision_volumes .parse::() .map_err(|_| parse_error(node_position(root)))?; if expected_bones > MAX_SKELETON_BONES || expected_collision_volumes > MAX_COLLISION_VOLUMES { return Err(parse_error(node_position(root))); } let root_bones = root.children().filter(Node::is_element).collect::>(); if root_bones.len() != 1 || !root_bones[0].has_tag_name("bone") { return Err(parse_error(node_position(root))); } let mut names = HashSet::new(); let mut bone_count = 0; let mut collision_volume_count = 0; let bone = parse_joint( root_bones[0], &mut names, 0, &mut bone_count, &mut collision_volume_count, )?; let (actual_bones, actual_collision_volumes) = skeleton_counts(&bone); if actual_bones != expected_bones || actual_collision_volumes != expected_collision_volumes { return Err(parse_error(node_position(root))); } Ok(LindenSkeleton::from_parts( bone, version, true, num_bones, num_collision_volumes, )) } pub(crate) fn skeleton_counts(root: &Joint) -> (usize, usize) { let mut bones = 1; let mut collision_volumes = root.collision_volume.len(); if let Some(children) = &root.bone { for child in children { let (child_bones, child_collision_volumes) = skeleton_counts(child); bones += child_bones; collision_volumes += child_collision_volumes; } } (bones, collision_volumes) } #[cfg(test)] mod tests { use super::*; #[test] fn runtime_parser_rejects_count_mismatches_and_unknown_children() { let wrong_count = ""; assert!(matches!( parse_skeleton_xml(wrong_count), Err(crate::Error::Parse { .. }) )); let unknown = ""; assert!(matches!( parse_skeleton_xml(unknown), Err(crate::Error::Parse { .. }) )); } }