Files
MetaCrate/crates/libremetaverse/src/asset_prim.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

775 lines
30 KiB
Rust

//! 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<PrimObject>,
pub parent: Option<PrimObject>,
asset_data: Vec<u8>,
asset_id: UUID,
}
fn escape(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
fn element(output: &mut String, name: &str, value: impl std::fmt::Display) {
let _ = write!(output, "<{name}>{}</{name}>", escape(&value.to_string()));
}
fn uuid_element(output: &mut String, name: &str, value: UUID) {
let _ = write!(output, "<{name}><UUID>{value}</UUID></{name}>");
}
fn vector_element(output: &mut String, name: &str, value: Vector3) {
let _ = write!(
output,
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z></{name}>",
value.x, value.y, value.z
);
}
fn quaternion_element(output: &mut String, name: &str, value: Quaternion) {
let _ = write!(
output,
"<{name}><X>{}</X><Y>{}</Y><Z>{}</Z><W>{}</W></{name}>",
value.x, value.y, value.z, value.w
);
}
fn color_element(output: &mut String, name: &str, value: Color4) {
let _ = write!(
output,
"<{name}><R>{}</R><G>{}</G><B>{}</B><A>{}</A></{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<'a, 'input>> {
node.children()
.find(|candidate| candidate.is_element() && candidate.tag_name().name() == name)
}
fn text(node: Node<'_, '_>, name: &str) -> Option<String> {
child(node, name)
.and_then(|value| value.text())
.map(str::trim)
.map(str::to_owned)
}
fn parsed<T>(node: Node<'_, '_>, name: &str, default: T) -> Result<T, Error>
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<bool, Error> {
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<UUID, Error> {
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<Vector3, Error> {
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<Quaternion, Error> {
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<Color4, Error> {
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("<TaskInventory>");
for item in &prim.inventory.items {
output.push_str("<TaskInventoryItem>");
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("</TaskInventoryItem>");
}
output.push_str("</TaskInventory>");
}
fn read_inventory(node: Node<'_, '_>) -> Result<Vec<PrimObjectInventoryBlockItemBlock>, 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("<SceneObjectPart xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">");
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("<Shape>");
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("<ExtraParams></ExtraParams>");
uuid_element(output, "SculptTexture", prim.sculpt.texture);
element(output, "SculptType", prim.sculpt.type_);
output.push_str("<SculptData></SculptData>");
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("</Shape>");
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("</SceneObjectPart>");
Ok(())
}
fn read_prim(node: Node<'_, '_>) -> Result<PrimObject, Error> {
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<Self, Error> {
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<PrimObject>,
) -> Result<Self, Error> {
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<u8>) -> Result<Self, Error> {
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<Self, Error> {
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<bool, Error> {
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<String, Error> {
let mut output =
String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><SceneObjectGroup>");
if let Some(parent) = &self.parent {
write_prim(&mut output, parent, None)?;
}
output.push_str("<OtherParts>");
for child in &self.children {
write_prim(&mut output, child, self.parent.as_ref())?;
}
output.push_str("</OtherParts></SceneObjectGroup>");
Ok(output)
}
pub fn decode_xml(&mut self, xml_data: String) -> Result<bool, Error> {
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::<Result<Vec<_>, _>>()?;
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<PrimObject, Error> {
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 & <fixture>".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("<not-a-group/>".into()).unwrap());
assert!(asset.decode_xml("<SceneObjectGroup>".into()).is_err());
assert!(asset.parent.is_none());
}
}