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
775 lines
30 KiB
Rust
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('&', "&")
|
|
.replace('<', "<")
|
|
.replace('>', ">")
|
|
.replace('"', """)
|
|
.replace('\'', "'")
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|