//! Native inventory models, hierarchy store, and bounded cache persistence. //! //! The store deliberately owns no transport behavior. Network inventory //! managers can populate it, but all mutation, indexing, notifications, and //! persistence remain deterministic local operations. #![allow(clippy::missing_errors_doc)] // Public signatures mirror the mapped C# API. #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] use crate::agent_manager::EventRegistry; use crate::{GridClient, InventoryItemFlags, InventoryObjectClass, PermissionMask}; use libremetaverse_structured_data::{OSD, OSDMap}; use libremetaverse_types::compat::{CancellationToken, EventHandler, Object, Subscription}; use libremetaverse_types::{ AssetType, AttachmentPoint, Error, FolderType, InventoryType, SaleType, UUID, WearableType, }; use std::collections::{HashMap, HashSet}; use std::fmt; use std::fs::{self, File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock, Weak}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const CACHE_MAGIC: &[u8; 8] = b"INVCACHE"; const CACHE_VERSION: u32 = 1; const MAX_CACHE_BYTES: u64 = 64 * 1024 * 1024; const MAX_CACHE_RECORDS: usize = 1_000_000; const MAX_STRING_BYTES: usize = 1024 * 1024; const MAX_HIERARCHY_DEPTH: usize = 512; fn read(value: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { value .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn write(value: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { value .write() .unwrap_or_else(std::sync::PoisonError::into_inner) } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Permissions { pub base_mask: PermissionMask, pub everyone_mask: PermissionMask, pub group_mask: PermissionMask, pub next_owner_mask: PermissionMask, pub owner_mask: PermissionMask, } impl Default for Permissions { fn default() -> Self { Self::no_permissions() } } impl Permissions { pub fn full_permissions() -> Self { Self { base_mask: PermissionMask::ALL, everyone_mask: PermissionMask::ALL, group_mask: PermissionMask::ALL, next_owner_mask: PermissionMask::ALL, owner_mask: PermissionMask::ALL, } } pub const fn no_permissions() -> Self { Self { base_mask: PermissionMask::NONE, everyone_mask: PermissionMask::NONE, group_mask: PermissionMask::NONE, next_owner_mask: PermissionMask::NONE, owner_mask: PermissionMask::NONE, } } pub const fn new( base_mask: u32, everyone_mask: u32, group_mask: u32, next_owner_mask: u32, owner_mask: u32, ) -> Result { Ok(Self { base_mask: PermissionMask(base_mask), everyone_mask: PermissionMask(everyone_mask), group_mask: PermissionMask(group_mask), next_owner_mask: PermissionMask(next_owner_mask), owner_mask: PermissionMask(owner_mask), }) } pub fn equals_with_permissions(&self, other: Self) -> bool { *self == other } pub fn equals_with_object(&self, obj: Option) -> bool { obj.as_ref() .and_then(Object::downcast_ref::) .is_some_and(|other| self == other) } pub fn from_osd(llsd: OSD) -> Result { let OSD::Map(map) = llsd else { return Ok(Self::default()); }; Ok(Self { base_mask: PermissionMask(osd_u32(&map, "base_mask")?), everyone_mask: PermissionMask(osd_u32(&map, "everyone_mask")?), group_mask: PermissionMask(osd_u32(&map, "group_mask")?), next_owner_mask: PermissionMask(osd_u32(&map, "next_owner_mask")?), owner_mask: PermissionMask(osd_u32(&map, "owner_mask")?), }) } pub fn get_hash_code(&self) -> i32 { (self.base_mask.0 ^ self.everyone_mask.0 ^ self.group_mask.0 ^ self.next_owner_mask.0 ^ self.owner_mask.0) .cast_signed() } pub fn get_next_permissions(&self) -> Result { let next = self.next_owner_mask.0; Self::new( self.base_mask.0 & next, self.everyone_mask.0 & next, self.group_mask.0 & next, next, self.owner_mask.0 & next, ) } pub fn get_osd(&self) -> Result { Ok(OSD::Map(HashMap::from([ ( "base_mask".into(), OSD::Integer(self.base_mask.0.cast_signed()), ), ( "everyone_mask".into(), OSD::Integer(self.everyone_mask.0.cast_signed()), ), ( "group_mask".into(), OSD::Integer(self.group_mask.0.cast_signed()), ), ( "next_owner_mask".into(), OSD::Integer(self.next_owner_mask.0.cast_signed()), ), ( "owner_mask".into(), OSD::Integer(self.owner_mask.0.cast_signed()), ), ]))) } pub fn has_permissions( perms: PermissionMask, check_perms: PermissionMask, ) -> Result { Ok(perms.0 & check_perms.0 == check_perms.0) } #[allow(clippy::should_implement_trait)] // C# operator mapping requires this name. pub fn eq(lhs: Self, rhs: Self) -> bool { lhs == rhs } pub fn ne(lhs: Self, rhs: Self) -> bool { lhs != rhs } } impl fmt::Display for Permissions { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, "Base: {:?}, Everyone: {:?}, Group: {:?}, NextOwner: {:?}, Owner: {:?}", self.base_mask, self.everyone_mask, self.group_mask, self.next_owner_mask, self.owner_mask ) } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct InventoryBase { uuid: UUID, parent_uuid: UUID, name: String, owner_id: UUID, } impl InventoryBase { fn new(uuid: UUID) -> Self { Self { uuid, parent_uuid: UUID::zero(), name: String::new(), owner_id: UUID::zero(), } } pub fn equals_with_inventory_base(&self, other: Self) -> bool { *self == other } pub fn equals_with_object(&self, obj: Option) -> bool { obj.as_ref() .and_then(Object::downcast_ref::) .is_some_and(|other| self == other) } pub fn get_hash_code(&self) -> i32 { self.uuid.get_hash_code() } pub fn get_osd(&self) -> Result { Ok(OSD::Map(HashMap::from([ ("item_id".into(), OSD::UUID(self.uuid)), ("parent_id".into(), OSD::UUID(self.parent_uuid)), ("name".into(), OSD::String(self.name.clone())), ("owner_id".into(), OSD::UUID(self.owner_id)), ]))) } pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; } pub const fn owner_id(&self) -> UUID { self.owner_id } pub fn set_owner_id(&mut self, value: UUID) { self.owner_id = value; } pub const fn parent_uuid(&self) -> UUID { self.parent_uuid } pub fn set_parent_uuid(&mut self, value: UUID) { self.parent_uuid = value; } pub const fn uuid(&self) -> UUID { self.uuid } pub fn set_uuid(&mut self, value: UUID) { self.uuid = value; } } #[derive(Clone, Debug)] pub struct InventoryItem { pub base: InventoryBase, asset_uuid: UUID, permissions: Permissions, asset_type: AssetType, inventory_type: InventoryType, creator_id: UUID, description: String, group_id: UUID, group_owned: bool, sale_price: i32, sale_type: SaleType, flags: u32, creation_date: SystemTime, transaction_id: UUID, last_owner_id: UUID, } impl PartialEq for InventoryItem { fn eq(&self, other: &Self) -> bool { self.base == other.base && self.asset_type == other.asset_type && self.asset_uuid == other.asset_uuid && self.creation_date == other.creation_date && self.description == other.description && self.flags == other.flags && self.group_id == other.group_id && self.group_owned == other.group_owned && self.inventory_type == other.inventory_type && self.permissions == other.permissions && self.sale_price == other.sale_price && self.sale_type == other.sale_type && self.last_owner_id == other.last_owner_id } } impl Eq for InventoryItem {} impl Hash for InventoryItem { fn hash(&self, state: &mut H) { self.base.hash(state); self.asset_uuid.hash(state); self.asset_type.hash(state); self.inventory_type.hash(state); self.description.hash(state); self.group_id.hash(state); self.group_owned.hash(state); self.sale_price.hash(state); self.sale_type.hash(state); self.flags.hash(state); self.creation_date.hash(state); self.last_owner_id.hash(state); self.permissions.hash(state); } } impl InventoryItem { fn with_type(inventory_type: InventoryType, uuid: UUID) -> Self { Self { base: InventoryBase::new(uuid), asset_uuid: UUID::zero(), permissions: Permissions::default(), asset_type: AssetType::Unknown, inventory_type, creator_id: UUID::zero(), description: String::new(), group_id: UUID::zero(), group_owned: false, sale_price: 0, sale_type: SaleType::Not, flags: 0, creation_date: UNIX_EPOCH, transaction_id: UUID::zero(), last_owner_id: UUID::zero(), } } pub fn new_with_inventory_type_uuid( type_: InventoryType, item_id: UUID, ) -> Result { Ok(Self::with_type(type_, item_id)) } pub fn new_with_uuid(uuid: UUID) -> Result { Ok(Self::with_type(InventoryType::UNKNOWN, uuid)) } pub fn equals_with_inventory_base(&self, other: InventoryBase) -> bool { self.base == other } pub fn equals_with_inventory_item(&self, other: Self) -> bool { *self == other } pub fn equals_with_object(&self, obj: Option) -> bool { obj.as_ref() .and_then(Object::downcast_ref::) .is_some_and(|other| self == other) } pub fn from_osd(data: OSD) -> Result { let OSD::Map(map) = data else { return Err(Error::Argument); }; let inventory_type = parse_inventory_type(map.get("inv_type"))?; let asset_type = parse_asset_type(map.get("type"))?; let corrected_type = if inventory_type == InventoryType::TEXTURE && matches!(asset_type, AssetType::Object | AssetType::Mesh) { InventoryType::ATTACHMENT } else { inventory_type }; let mut item = Self::with_type(corrected_type, osd_uuid(&map, "item_id")?); item.apply_map(&map)?; Ok(item) } pub fn get_hash_code(&self) -> i32 { self.base.get_hash_code() } pub fn get_osd(&self) -> Result { let OSD::Map(mut permissions) = self.permissions.get_osd()? else { unreachable!("Permissions always serializes as a map"); }; permissions.insert("creator_id".into(), OSD::UUID(self.creator_id)); permissions.insert("last_owner_id".into(), OSD::UUID(self.last_owner_id)); permissions.insert("is_owner_group".into(), OSD::Boolean(self.group_owned)); permissions.insert("group_id".into(), OSD::UUID(self.group_id)); Ok(OSD::Map(HashMap::from([ ("item_id".into(), OSD::UUID(self.base.uuid)), ("parent_id".into(), OSD::UUID(self.base.parent_uuid)), ( "type".into(), OSD::Integer(i32::from(self.asset_type as i8)), ), ( "inv_type".into(), OSD::Integer(i32::from(self.inventory_type.0)), ), ("flags".into(), OSD::Integer(self.flags.cast_signed())), ("name".into(), OSD::String(self.base.name.clone())), ("desc".into(), OSD::String(self.description.clone())), ("asset_id".into(), OSD::UUID(self.asset_uuid)), ("created_at".into(), OSD::Date(self.creation_date)), ("permissions".into(), OSD::Map(permissions)), ( "sale_info".into(), OSD::Map(HashMap::from([ ("sale_price".into(), OSD::Integer(self.sale_price)), ( "sale_type".into(), OSD::Integer(i32::from(self.sale_type as u8)), ), ])), ), ]))) } pub fn is_link(&self) -> Result { Ok(matches!( self.asset_type, AssetType::Link | AssetType::LinkFolder )) } pub fn update(&mut self, data: OSDMap) -> Result<(), Error> { self.apply_map(&data.snapshot()) } fn apply_map(&mut self, map: &HashMap) -> Result<(), Error> { if let Some(value) = map.get("item_id") { self.base.uuid = value.as_uuid()?; } if let Some(value) = map.get("parent_id") { self.base.parent_uuid = value.as_uuid()?; } if let Some(value) = map.get("agent_id") { self.base.owner_id = value.as_uuid()?; } if let Some(value) = map.get("name") { self.base.name = value.as_string()?; } if let Some(value) = map.get("desc") { self.description = value.as_string()?; } if let Some(value) = map.get("permissions") { self.permissions = Permissions::from_osd(value.clone())?; if let OSD::Map(perms) = value { self.base.owner_id = optional_uuid(perms, "owner_id")?.unwrap_or(self.base.owner_id); self.creator_id = optional_uuid(perms, "creator_id")?.unwrap_or(self.creator_id); self.last_owner_id = optional_uuid(perms, "last_owner_id")?.unwrap_or(self.last_owner_id); self.group_id = optional_uuid(perms, "group_id")?.unwrap_or(self.group_id); if let Some(group_owned) = perms.get("is_owner_group") { self.group_owned = group_owned.as_boolean()?; } } } if let Some(OSD::Map(sale)) = map.get("sale_info") { self.sale_price = osd_i32(sale, "sale_price")?; self.sale_type = sale_type_from_i32(osd_i32(sale, "sale_type")?)?; } if let Some(value) = map.get("shadow_id") { self.asset_uuid = decrypt_shadow_id(value.as_uuid()?)?; } if let Some(value) = map.get("asset_id") { self.asset_uuid = value.as_uuid()?; } if let Some(value) = map.get("linked_id") { self.asset_uuid = value.as_uuid()?; } if let Some(value) = map.get("type") { let value = parse_asset_type(Some(value))?; if value != AssetType::Unknown { self.asset_type = value; } } if let Some(value) = map.get("inv_type") { let value = parse_inventory_type(Some(value))?; if value != InventoryType::UNKNOWN { self.inventory_type = value; } } if let Some(value) = map.get("flags") { self.flags = value.as_u_integer()?; } if let Some(value) = map.get("created_at") { self.creation_date = match value { OSD::Date(value) => *value, _ => UNIX_EPOCH + Duration::from_secs(u64::from(value.as_u_integer()?)), }; } Ok(()) } pub const fn asset_type(&self) -> AssetType { self.asset_type } pub fn set_asset_type(&mut self, value: AssetType) { self.asset_type = value; } pub const fn asset_uuid(&self) -> UUID { self.asset_uuid } pub fn set_asset_uuid(&mut self, value: UUID) { self.asset_uuid = value; } pub fn creation_date(&self) -> SystemTime { self.creation_date } pub fn set_creation_date(&mut self, value: SystemTime) { self.creation_date = value; } pub const fn creator_id(&self) -> UUID { self.creator_id } pub fn set_creator_id(&mut self, value: UUID) { self.creator_id = value; } pub fn description(&self) -> String { self.description.clone() } pub fn set_description(&mut self, value: String) { self.description = value; } pub const fn flags(&self) -> u32 { self.flags } pub fn set_flags(&mut self, value: u32) { self.flags = value; } pub const fn group_id(&self) -> UUID { self.group_id } pub fn set_group_id(&mut self, value: UUID) { self.group_id = value; } pub const fn group_owned(&self) -> bool { self.group_owned } pub fn set_group_owned(&mut self, value: bool) { self.group_owned = value; } pub const fn inventory_type(&self) -> InventoryType { self.inventory_type } pub fn set_inventory_type(&mut self, value: InventoryType) { self.inventory_type = value; } pub const fn last_owner_id(&self) -> UUID { self.last_owner_id } pub fn set_last_owner_id(&mut self, value: UUID) { self.last_owner_id = value; } pub const fn permissions(&self) -> Permissions { self.permissions } pub fn set_permissions(&mut self, value: Permissions) { self.permissions = value; } pub fn resolved_asset_id(&self) -> UUID { if self.is_link().unwrap_or(false) { UUID::zero() } else { self.asset_uuid } } pub fn resolved_item_id(&self) -> UUID { if self.is_link().unwrap_or(false) { self.asset_uuid } else { self.base.uuid } } pub const fn sale_price(&self) -> i32 { self.sale_price } pub fn set_sale_price(&mut self, value: i32) { self.sale_price = value; } pub const fn sale_type(&self) -> SaleType { self.sale_type } pub fn set_sale_type(&mut self, value: SaleType) { self.sale_type = value; } pub const fn transaction_id(&self) -> UUID { self.transaction_id } pub fn set_transaction_id(&mut self, value: UUID) { self.transaction_id = value; } } impl fmt::Display for InventoryItem { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, "{:?} {} ({} {}) '{}'/'{}' {}", self.asset_type, self.asset_uuid, inventory_type_name(self.inventory_type), self.base.uuid, self.base.name, self.description, self.permissions ) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InventoryFolder { pub base: InventoryBase, descendent_count: i32, preferred_type: FolderType, version: i32, } impl InventoryFolder { pub const VERSION_UNKNOWN: i32 = -1; pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryBase::new(uuid), descendent_count: 0, preferred_type: FolderType::None, version: 1, }) } pub fn equals_with_inventory_base(&self, other: InventoryBase) -> bool { self.base == other } pub fn equals_with_inventory_folder(&self, other: Self) -> bool { *self == other } pub fn equals_with_object(&self, obj: Option) -> bool { obj.as_ref() .and_then(Object::downcast_ref::) .is_some_and(|other| self == other) } pub fn from_osd(data: OSD) -> Result { let OSD::Map(map) = data else { return Err(Error::Argument); }; let id = if map.contains_key("category_id") { osd_uuid(&map, "category_id")? } else { osd_uuid(&map, "folder_id")? }; let mut folder = Self::new(id)?; folder.version = Self::VERSION_UNKNOWN; folder.apply_map(&map)?; Ok(folder) } pub fn get_hash_code(&self) -> i32 { self.base.get_hash_code() } pub fn get_osd(&self) -> Result { Ok(OSD::Map(HashMap::from([ ("item_id".into(), OSD::UUID(self.base.uuid)), ("version".into(), OSD::Integer(self.version)), ("parent_id".into(), OSD::UUID(self.base.parent_uuid)), ( "type".into(), OSD::Integer(i32::from(self.preferred_type as i8)), ), ("name".into(), OSD::String(self.base.name.clone())), ]))) } pub fn update(&mut self, data: OSDMap) -> Result<(), Error> { self.apply_map(&data.snapshot()) } fn apply_map(&mut self, map: &HashMap) -> Result<(), Error> { if let Some(value) = map.get("category_id") { self.base.uuid = value.as_uuid()?; } if let Some(value) = map.get("folder_id") { self.base.uuid = value.as_uuid()?; } if let Some(value) = map.get("version") { self.version = value.as_integer()?; } if let Some(value) = map.get("parent_id") { self.base.parent_uuid = value.as_uuid()?; } if let Some(value) = map.get("type_default").or_else(|| map.get("type")) { self.preferred_type = folder_type_from_i32(value.as_integer()?)?; } if let Some(value) = map.get("descendents") { self.descendent_count = value.as_integer()?; } if let Some(value) = map.get("owner_id").or_else(|| map.get("agent_id")) { self.base.owner_id = value.as_uuid()?; } if let Some(value) = map.get("name") { self.base.name = value.as_string()?; } Ok(()) } pub const fn descendent_count(&self) -> i32 { self.descendent_count } pub fn set_descendent_count(&mut self, value: i32) { self.descendent_count = value; } pub const fn preferred_type(&self) -> FolderType { self.preferred_type } pub fn set_preferred_type(&mut self, value: FolderType) { self.preferred_type = value; } pub const fn version(&self) -> i32 { self.version } pub fn set_version(&mut self, value: i32) { self.version = value; } } impl fmt::Display for InventoryFolder { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.base.name) } } macro_rules! simple_inventory_item { ($name:ident, $inventory_type:expr) => { #[derive(Clone, Debug, Eq, PartialEq)] pub struct $name { pub base: InventoryItem, } impl $name { pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryItem::with_type($inventory_type, uuid), }) } } }; } simple_inventory_item!(InventoryAnimation, InventoryType::ANIMATION); simple_inventory_item!(InventoryCallingCard, InventoryType::CALLING_CARD); simple_inventory_item!(InventoryCategory, InventoryType::CATEGORY); simple_inventory_item!(InventoryGesture, InventoryType::GESTURE); simple_inventory_item!(InventoryLSL, InventoryType::LSL); simple_inventory_item!(InventoryMaterial, InventoryType::MATERIAL); simple_inventory_item!(InventoryNotecard, InventoryType::NOTECARD); simple_inventory_item!(InventorySettings, InventoryType::SETTINGS); simple_inventory_item!(InventorySnapshot, InventoryType::SNAPSHOT); simple_inventory_item!(InventorySound, InventoryType::SOUND); simple_inventory_item!(InventoryTexture, InventoryType::TEXTURE); #[derive(Clone, Debug, Eq, PartialEq)] pub struct InventoryLandmark { pub base: InventoryItem, } impl InventoryLandmark { pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryItem::with_type(InventoryType::LANDMARK, uuid), }) } pub const fn landmark_visited(&self) -> bool { self.base.flags & 1 != 0 } pub fn set_landmark_visited(&mut self, value: bool) { if value { self.base.flags |= 1; } else { self.base.flags &= !1; } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InventoryObject { pub base: InventoryItem, } impl InventoryObject { pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryItem::with_type(InventoryType::OBJECT, uuid), }) } pub const fn attach_point(&self) -> AttachmentPoint { attachment_point_from_u8(self.base.flags.to_le_bytes()[0]) } pub fn set_attach_point(&mut self, value: AttachmentPoint) { self.base.flags = u32::from(value as u8) | (self.base.flags & 0xffff_ff00); } pub const fn item_flags(&self) -> InventoryItemFlags { InventoryItemFlags(self.base.flags & !0xff) } pub fn set_item_flags(&mut self, value: InventoryItemFlags) { self.base.flags = value.0 | (self.base.flags & 0xff); } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InventoryAttachment { pub base: InventoryItem, } impl InventoryAttachment { pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryItem::with_type(InventoryType::ATTACHMENT, uuid), }) } pub const fn attachment_point(&self) -> AttachmentPoint { attachment_point_from_u8(self.base.flags.to_le_bytes()[0]) } pub fn set_attachment_point(&mut self, value: AttachmentPoint) { self.base.flags = u32::from(value as u8); } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InventoryWearable { pub base: InventoryItem, } impl InventoryWearable { pub fn new(uuid: UUID) -> Result { Ok(Self { base: InventoryItem::with_type(InventoryType::WEARABLE, uuid), }) } pub const fn wearable_type(&self) -> WearableType { wearable_type_from_u8(self.base.flags.to_le_bytes()[0]) } pub fn set_wearable_type(&mut self, value: WearableType) { self.base.flags = u32::from(value as u8); } } #[derive(Clone, Debug)] enum InventoryValue { Base(InventoryBase), Folder(InventoryFolder), Item(InventoryItem), Animation(InventoryAnimation), Attachment(InventoryAttachment), CallingCard(InventoryCallingCard), Category(InventoryCategory), Gesture(InventoryGesture), Lsl(InventoryLSL), Landmark(InventoryLandmark), Material(InventoryMaterial), Notecard(InventoryNotecard), Object(InventoryObject), Settings(InventorySettings), Snapshot(InventorySnapshot), Sound(InventorySound), Texture(InventoryTexture), Wearable(InventoryWearable), } impl InventoryValue { fn from_item_typed(item: InventoryItem) -> Self { macro_rules! typed { ($kind:ty, $variant:ident) => {{ let mut value = <$kind>::new(item.base.uuid) .expect("known inventory item constructor cannot fail"); value.base = item; Self::$variant(value) }}; } match item.inventory_type { InventoryType::TEXTURE => typed!(InventoryTexture, Texture), InventoryType::SOUND => typed!(InventorySound, Sound), InventoryType::CALLING_CARD => typed!(InventoryCallingCard, CallingCard), InventoryType::LANDMARK => typed!(InventoryLandmark, Landmark), InventoryType::OBJECT => typed!(InventoryObject, Object), InventoryType::NOTECARD => typed!(InventoryNotecard, Notecard), InventoryType::CATEGORY => typed!(InventoryCategory, Category), InventoryType::LSL => typed!(InventoryLSL, Lsl), InventoryType::SNAPSHOT => typed!(InventorySnapshot, Snapshot), InventoryType::ATTACHMENT => typed!(InventoryAttachment, Attachment), InventoryType::WEARABLE => typed!(InventoryWearable, Wearable), InventoryType::ANIMATION => typed!(InventoryAnimation, Animation), InventoryType::GESTURE => typed!(InventoryGesture, Gesture), InventoryType::SETTINGS => typed!(InventorySettings, Settings), InventoryType::MATERIAL => typed!(InventoryMaterial, Material), _ => Self::Item(item), } } fn from_class(value: &dyn InventoryObjectClass) -> Option { macro_rules! downcast { ($type:ty, $variant:ident) => { if let Some(value) = value.as_any().downcast_ref::<$type>() { return Some(Self::$variant(value.clone())); } }; } downcast!(InventoryBase, Base); downcast!(InventoryFolder, Folder); downcast!(InventoryItem, Item); downcast!(InventoryAnimation, Animation); downcast!(InventoryAttachment, Attachment); downcast!(InventoryCallingCard, CallingCard); downcast!(InventoryCategory, Category); downcast!(InventoryGesture, Gesture); downcast!(InventoryLSL, Lsl); downcast!(InventoryLandmark, Landmark); downcast!(InventoryMaterial, Material); downcast!(InventoryNotecard, Notecard); downcast!(InventoryObject, Object); downcast!(InventorySettings, Settings); downcast!(InventorySnapshot, Snapshot); downcast!(InventorySound, Sound); downcast!(InventoryTexture, Texture); downcast!(InventoryWearable, Wearable); None } fn base(&self) -> &InventoryBase { match self { Self::Base(value) => value, Self::Folder(value) => &value.base, Self::Item(value) => &value.base, Self::Animation(value) => &value.base.base, Self::Attachment(value) => &value.base.base, Self::CallingCard(value) => &value.base.base, Self::Category(value) => &value.base.base, Self::Gesture(value) => &value.base.base, Self::Lsl(value) => &value.base.base, Self::Landmark(value) => &value.base.base, Self::Material(value) => &value.base.base, Self::Notecard(value) => &value.base.base, Self::Object(value) => &value.base.base, Self::Settings(value) => &value.base.base, Self::Snapshot(value) => &value.base.base, Self::Sound(value) => &value.base.base, Self::Texture(value) => &value.base.base, Self::Wearable(value) => &value.base.base, } } fn item(&self) -> Option<&InventoryItem> { match self { Self::Item(value) => Some(value), Self::Animation(value) => Some(&value.base), Self::Attachment(value) => Some(&value.base), Self::CallingCard(value) => Some(&value.base), Self::Category(value) => Some(&value.base), Self::Gesture(value) => Some(&value.base), Self::Lsl(value) => Some(&value.base), Self::Landmark(value) => Some(&value.base), Self::Material(value) => Some(&value.base), Self::Notecard(value) => Some(&value.base), Self::Object(value) => Some(&value.base), Self::Settings(value) => Some(&value.base), Self::Snapshot(value) => Some(&value.base), Self::Sound(value) => Some(&value.base), Self::Texture(value) => Some(&value.base), Self::Wearable(value) => Some(&value.base), Self::Base(_) | Self::Folder(_) => None, } } fn folder(&self) -> Option<&InventoryFolder> { match self { Self::Folder(value) => Some(value), _ => None, } } fn folder_mut(&mut self) -> Option<&mut InventoryFolder> { match self { Self::Folder(value) => Some(value), _ => None, } } fn boxed(&self) -> Box { match self { Self::Base(value) => Box::new(value.clone()), Self::Folder(value) => Box::new(value.clone()), Self::Item(value) => Box::new(value.clone()), Self::Animation(value) => Box::new(value.clone()), Self::Attachment(value) => Box::new(value.clone()), Self::CallingCard(value) => Box::new(value.clone()), Self::Category(value) => Box::new(value.clone()), Self::Gesture(value) => Box::new(value.clone()), Self::Lsl(value) => Box::new(value.clone()), Self::Landmark(value) => Box::new(value.clone()), Self::Material(value) => Box::new(value.clone()), Self::Notecard(value) => Box::new(value.clone()), Self::Object(value) => Box::new(value.clone()), Self::Settings(value) => Box::new(value.clone()), Self::Snapshot(value) => Box::new(value.clone()), Self::Sound(value) => Box::new(value.clone()), Self::Texture(value) => Box::new(value.clone()), Self::Wearable(value) => Box::new(value.clone()), } } fn clone_as(&self) -> Option { self.boxed().as_any().downcast_ref::().cloned() } fn tag(&self) -> u8 { match self { Self::Base(_) => 0, Self::Folder(_) => 1, Self::Item(_) => 2, Self::Animation(_) => 3, Self::Attachment(_) => 4, Self::CallingCard(_) => 5, Self::Category(_) => 6, Self::Gesture(_) => 7, Self::Lsl(_) => 8, Self::Landmark(_) => 9, Self::Material(_) => 10, Self::Notecard(_) => 11, Self::Object(_) => 12, Self::Settings(_) => 13, Self::Snapshot(_) => 14, Self::Sound(_) => 15, Self::Texture(_) => 16, Self::Wearable(_) => 17, } } } #[derive(Clone)] pub struct InventoryNode { inner: Arc>, } #[derive(Debug)] struct InventoryNodeInner { data: Option, parent: Option>>, children: HashMap, needs_update: bool, } impl fmt::Debug for InventoryNode { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let inner = read(&self.inner); formatter .debug_struct("InventoryNode") .field("uuid", &inner.data.as_ref().map(|value| value.base().uuid)) .field("children", &inner.children.len()) .field("needs_update", &inner.needs_update) .finish() } } impl InventoryNode { fn empty() -> Self { Self { inner: Arc::new(RwLock::new(InventoryNodeInner { data: None, parent: None, children: HashMap::new(), needs_update: true, })), } } fn from_value(data: InventoryValue) -> Self { let node = Self::empty(); write(&node.inner).data = Some(data); node } pub fn new_with_constructor() -> Result { Ok(Self::empty()) } pub fn new_with_inventory_base(data: &dyn InventoryObjectClass) -> Result { Ok(Self::from_value( InventoryValue::from_class(data).ok_or(Error::Argument)?, )) } pub fn new_with_inventory_base_inventory_node( data: InventoryBase, parent: Self, ) -> Result { let mut node = Self::from_value(InventoryValue::Base(data)); node.set_parent(Some(parent.clone())); let uuid = node.uuid().ok_or(Error::Argument)?; write(&parent.inner).children.insert(uuid, node.clone()); Ok(node) } fn uuid(&self) -> Option { read(&self.inner) .data .as_ref() .map(|value| value.base().uuid) } fn value(&self) -> Option { read(&self.inner).data.clone() } fn set_value(&self, value: InventoryValue) { write(&self.inner).data = Some(value); } pub fn data(&self) -> Option> { self.value().map(|value| value.boxed()) } pub fn set_data(&mut self, value: Option>) { write(&self.inner).data = value.as_deref().and_then(InventoryValue::from_class); } pub fn modify_time(&self) -> SystemTime { let inner = read(&self.inner); let Some(data) = inner.data.as_ref() else { return UNIX_EPOCH; }; if let Some(item) = data.item() { return item.creation_date; } if data.folder().is_some() { let children: Vec<_> = inner.children.values().cloned().collect(); drop(inner); return children .into_iter() .filter_map(|child| child.value()) .filter_map(|value| value.item().map(InventoryItem::creation_date)) .max() .unwrap_or(UNIX_EPOCH); } UNIX_EPOCH } pub fn needs_update(&self) -> bool { read(&self.inner).needs_update } pub fn set_needs_update(&mut self, value: bool) { write(&self.inner).needs_update = value; } pub fn nodes(&self) -> InventoryNodeDictionary { InventoryNodeDictionary { parent: self.clone(), } } pub fn set_nodes(&mut self, value: InventoryNodeDictionary) { let children = read(&value.parent.inner).children.clone(); write(&self.inner).children = children; self.reparent_children(); } pub fn parent(&self) -> Option { read(&self.inner) .parent .as_ref() .and_then(Weak::upgrade) .map(|inner| Self { inner }) } pub fn set_parent(&mut self, value: Option) { write(&self.inner).parent = value.as_ref().map(|parent| Arc::downgrade(&parent.inner)); } fn reparent_children(&self) { let children: Vec<_> = read(&self.inner).children.values().cloned().collect(); for mut child in children { child.set_parent(Some(self.clone())); } } } impl fmt::Display for InventoryNode { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let Some(value) = self.value() else { return formatter.write_str("[Empty Node]"); }; if let Some(folder) = value.folder() { write!(formatter, "{folder}") } else if let Some(item) = value.item() { write!(formatter, "{item}") } else { formatter.write_str(&value.base().name()) } } } #[derive(Clone, Debug)] pub struct InventoryNodeDictionary { parent: InventoryNode, } impl InventoryNodeDictionary { pub fn new(parent: InventoryNode) -> Result { Ok(Self { parent }) } pub fn add(&self, key: UUID, mut value: InventoryNode) -> Result<(), Error> { value.set_parent(Some(self.parent.clone())); write(&self.parent.inner).children.insert(key, value); Ok(()) } pub fn contains(&self, key: UUID) -> Result { Ok(read(&self.parent.inner).children.contains_key(&key)) } pub fn remove(&self, key: UUID) -> Result<(), Error> { if let Some(mut removed) = write(&self.parent.inner).children.remove(&key) { removed.set_parent(None); } Ok(()) } pub fn count(&self) -> i32 { i32::try_from(read(&self.parent.inner).children.len()).unwrap_or(i32::MAX) } pub fn item(&self, key: UUID) -> InventoryNode { read(&self.parent.inner).children[&key].clone() } pub fn set_item(&mut self, key: UUID, value: InventoryNode) { let _ = self.add(key, value); } pub fn keys(&self) -> Vec { read(&self.parent.inner).children.keys().copied().collect() } pub fn parent(&self) -> InventoryNode { self.parent.clone() } pub fn set_parent(&mut self, value: InventoryNode) { self.parent = value; self.parent.reparent_children(); } pub fn sync_root(&self) -> Object { Object::opaque(self.clone()) } pub fn values(&self) -> Vec { read(&self.parent.inner) .children .values() .cloned() .collect() } } #[derive(Clone, Debug)] pub struct InventoryObjectAddedEventArgs { value: InventoryValue, } impl InventoryObjectAddedEventArgs { pub fn new(obj: InventoryBase) -> Result { Ok(Self { value: InventoryValue::Base(obj), }) } fn from_value(value: InventoryValue) -> Self { Self { value } } pub fn obj(&self) -> InventoryBase { self.value.base().clone() } pub fn object(&self) -> Box { self.value.boxed() } } #[derive(Clone, Debug)] pub struct InventoryObjectRemovedEventArgs { value: InventoryValue, } impl InventoryObjectRemovedEventArgs { pub fn new(obj: InventoryBase) -> Result { Ok(Self { value: InventoryValue::Base(obj), }) } fn from_value(value: InventoryValue) -> Self { Self { value } } pub fn obj(&self) -> InventoryBase { self.value.base().clone() } pub fn object(&self) -> Box { self.value.boxed() } } #[derive(Clone, Debug)] pub struct InventoryObjectUpdatedEventArgs { old: InventoryValue, new: InventoryValue, } impl InventoryObjectUpdatedEventArgs { pub fn new(old_object: InventoryBase, new_object: InventoryBase) -> Result { Ok(Self { old: InventoryValue::Base(old_object), new: InventoryValue::Base(new_object), }) } fn from_values(old: InventoryValue, new: InventoryValue) -> Self { Self { old, new } } pub fn old_object(&self) -> InventoryBase { self.old.base().clone() } pub fn new_object(&self) -> InventoryBase { self.new.base().clone() } pub fn old_value(&self) -> Box { self.old.boxed() } pub fn new_value(&self) -> Box { self.new.boxed() } } #[derive(Default)] struct InventoryState { items: HashMap, links: HashMap>, root: Option, library_root: Option, root_node: Option, library_root_node: Option, } struct InventoryInner { owner: UUID, _client: Weak, state: RwLock, added: EventRegistry, removed: EventRegistry, updated: EventRegistry, } #[derive(Clone)] pub struct Inventory { inner: Arc, } impl fmt::Debug for Inventory { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("Inventory") .field("owner", &self.owner()) .field("count", &self.count()) .finish() } } impl Inventory { pub fn subscribe_inventory_object_added( &self, handler: EventHandler, ) -> Subscription { self.inner.added.subscribe(handler) } pub fn subscribe_inventory_object_removed( &self, handler: EventHandler, ) -> Subscription { self.inner.removed.subscribe(handler) } pub fn subscribe_inventory_object_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.updated.subscribe(handler) } pub fn new_with_grid_client(client: Arc) -> Result { let owner = client.network().native_agent_id(); Self::new_with_grid_client_uuid(client, owner) } pub fn new_with_grid_client_uuid(client: Arc, owner: UUID) -> Result { Ok(Self { inner: Arc::new(InventoryInner { owner, _client: Arc::downgrade(&client), state: RwLock::new(InventoryState::default()), added: EventRegistry::default(), removed: EventRegistry::default(), updated: EventRegistry::default(), }), }) } pub fn clear(&self) -> Result<(), Error> { let mut state = write(&self.inner.state); state.items.clear(); state.links.clear(); Ok(()) } pub fn contains_with_inventory_base( &self, obj: &dyn InventoryObjectClass, ) -> Result { self.contains_with_uuid(obj.inventory_base().uuid) } pub fn contains_with_uuid(&self, uuid: UUID) -> Result { Ok(read(&self.inner.state).items.contains_key(&uuid)) } pub fn find_all_links(&self, asset_id: UUID) -> Result, Error> { if asset_id == UUID::zero() { return Ok(Vec::new()); } let state = read(&self.inner.state); Ok(state .links .get(&asset_id) .into_iter() .flatten() .filter_map(|uuid| state.items.get(uuid).cloned()) .collect()) } pub fn get_contents_with_inventory_folder( &self, folder: &InventoryFolder, ) -> Result>, Error> { self.get_contents_with_uuid(folder.base.uuid) } pub fn get_contents_with_uuid( &self, folder: UUID, ) -> Result>, Error> { let state = read(&self.inner.state); let node = state.items.get(&folder).ok_or(Error::InvalidOperation)?; if node .value() .and_then(|value| value.folder().cloned()) .is_none() { return Err(Error::InvalidOperation); } Ok(read(&node.inner) .children .values() .filter_map(InventoryNode::value) .map(|value| value.boxed()) .collect()) } pub fn get_contents_sorted_with_uuid( &self, folder: UUID, order: crate::InventorySortOrder, ) -> Result>, Error> { let mut values = self.get_contents_with_uuid(folder)?; values.sort_by(|left, right| compare_inventory(left.as_ref(), right.as_ref(), order)); Ok(values) } pub fn find_folder_for_type(&self, folder_type: FolderType) -> Option { let state = read(&self.inner.state); let root = state.root?; state .items .values() .filter_map(InventoryNode::value) .filter_map(|value| value.folder().cloned()) .find(|folder| folder.base.parent_uuid == root && folder.preferred_type == folder_type) .map(|folder| folder.base.uuid) } pub fn get_node_for(&self, uuid: UUID) -> Result { read(&self.inner.state) .items .get(&uuid) .cloned() .ok_or(Error::InvalidOperation) } pub fn get_node_or_default(&self, uuid: UUID) -> Result, Error> { Ok(read(&self.inner.state).items.get(&uuid).cloned()) } pub fn try_get_node_for(&self, uuid: UUID, node: &mut Option) -> bool { *node = read(&self.inner.state).items.get(&uuid).cloned(); node.is_some() } pub fn get_value_or_default_with_uuid( &self, uuid: UUID, ) -> Result, Error> { Ok(read(&self.inner.state) .items .get(&uuid) .and_then(InventoryNode::value) .map(|value| value.base().clone())) } pub fn get_value_or_default_with_uuid_a7c63fbe( &self, uuid: UUID, ) -> Result, Error> { Ok(read(&self.inner.state) .items .get(&uuid) .and_then(InventoryNode::value) .and_then(|value| value.clone_as::())) } /// Returns the shared item portion regardless of the concrete inventory /// subclass stored for the UUID. pub(crate) fn native_item_value(&self, uuid: UUID) -> Option { read(&self.inner.state) .items .get(&uuid) .and_then(InventoryNode::value) .and_then(|value| value.item().cloned()) } /// Atomically applies one fully parsed AIS response. A detached candidate /// graph is built first, so validation or rebuilding failures cannot expose /// a partially reconciled inventory to readers. #[allow(clippy::too_many_lines)] // One lock-free candidate build followed by one state swap. pub(crate) fn native_apply_ais_batch( &self, folders: Vec, items: Vec, removed_items: &[UUID], removed_categories: &[UUID], broken_links: &[UUID], versions: &HashMap, ) -> Result<(), Error> { let mut incoming = HashSet::new(); for uuid in folders .iter() .map(|folder| folder.base.uuid) .chain(items.iter().map(|item| item.base.uuid)) { if uuid == UUID::zero() || !incoming.insert(uuid) { return Err(Error::Argument); } } if incoming.len() > MAX_CACHE_RECORDS || versions.keys().any(|uuid| *uuid == UUID::zero()) { return Err(Error::Argument); } let mut touched = incoming.clone(); touched.extend(versions.keys().copied()); let (mut candidate, old_values) = { let state = read(&self.inner.state); let old_values: HashMap<_, _> = state .items .iter() .filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value))) .collect(); let mut candidate = InventoryState { root: state.root, library_root: state.library_root, ..InventoryState::default() }; for (uuid, value) in &old_values { candidate .items .insert(*uuid, InventoryNode::from_value(value.clone())); } (candidate, old_values) }; for folder in folders { candidate.items.insert( folder.base.uuid, InventoryNode::from_value(InventoryValue::Folder(folder)), ); } for item in items { let uuid = item.base.uuid; let value = InventoryValue::from_item_typed(item); candidate .items .insert(uuid, InventoryNode::from_value(value)); } for (uuid, version) in versions { if let Some(node) = candidate.items.get(uuid) { let Some(mut value) = node.value() else { return Err(Error::InvalidOperation); }; let Some(folder) = value.folder_mut() else { return Err(Error::InvalidOperation); }; folder.version = *version; node.set_value(value); } } let direct_removals: HashSet<_> = removed_items .iter() .chain(broken_links) .copied() .filter(|uuid| *uuid != UUID::zero()) .collect(); for uuid in direct_removals { candidate.items.remove(&uuid); } rebuild_indexes_and_counts(&mut candidate); for category in removed_categories .iter() .copied() .filter(|uuid| *uuid != UUID::zero()) { let mut pending = vec![category]; let mut descendants = HashSet::new(); while let Some(uuid) = pending.pop() { if descendants.len() >= MAX_CACHE_RECORDS || !descendants.insert(uuid) { continue; } if let Some(node) = candidate.items.get(&uuid) { pending.extend(read(&node.inner).children.keys().copied()); } } for uuid in descendants { candidate.items.remove(&uuid); } if candidate.root == Some(category) { candidate.root = None; } if candidate.library_root == Some(category) { candidate.library_root = None; } } if candidate.items.len() > MAX_CACHE_RECORDS { return Err(Error::InvalidOperation); } rebuild_indexes_and_counts(&mut candidate); candidate.root_node = candidate .root .and_then(|uuid| candidate.items.get(&uuid).cloned()); candidate.library_root_node = candidate .library_root .and_then(|uuid| candidate.items.get(&uuid).cloned()); let new_values: HashMap<_, _> = candidate .items .iter() .filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value))) .collect(); *write(&self.inner.state) = candidate; for (uuid, old) in &old_values { match new_values.get(uuid) { None => self .inner .removed .emit(InventoryObjectRemovedEventArgs::from_value(old.clone())), Some(new) if touched.contains(uuid) => { self.inner .updated .emit(InventoryObjectUpdatedEventArgs::from_values( old.clone(), new.clone(), )); } Some(_) => {} } } for (uuid, new) in new_values { if !old_values.contains_key(&uuid) { self.inner .added .emit(InventoryObjectAddedEventArgs::from_value(new)); } } Ok(()) } pub fn try_get_value_with_uuid_inventory_base( &self, uuid: UUID, item: &mut Option>, ) -> Result { *item = read(&self.inner.state) .items .get(&uuid) .and_then(InventoryNode::value) .map(|value| value.boxed()); Ok(item.is_some()) } pub fn try_get_value_with_uuid_t( &self, uuid: UUID, item: &mut Option, ) -> bool { *item = read(&self.inner.state) .items .get(&uuid) .and_then(InventoryNode::value) .and_then(|value| value.clone_as::()); item.is_some() } pub fn update_node_for(&self, item: &dyn InventoryObjectClass) -> Result<(), Error> { let value = InventoryValue::from_class(item).ok_or(Error::Argument)?; let uuid = value.base().uuid; let parent_uuid = value.base().parent_uuid; let event = { let mut state = write(&self.inner.state); // Root-level records have no hierarchy edges or ancestor counts to // rebuild. They are common during the initial flat inventory feed // and for incremental item refreshes, so update their node and link // index directly instead of walking and relocking the entire store. // The general path below remains responsible for moves, folders, // cycles, placeholder parents, and descendant counts. let old_root_value = state .items .get(&uuid) .and_then(InventoryNode::value) .filter(|old| old.base().parent_uuid == UUID::zero()); if parent_uuid == UUID::zero() && (old_root_value.is_some() || !state.items.contains_key(&uuid)) { if let Some(old) = old_root_value.as_ref() { update_link_index(&mut state.links, uuid, old, false); } update_link_index(&mut state.links, uuid, &value, true); let event = if let Some(node) = state.items.get(&uuid) { let old = node.value(); node.set_value(value.clone()); old.map(|old| InventoryObjectUpdatedEventArgs::from_values(old, value.clone())) } else { state .items .insert(uuid, InventoryNode::from_value(value.clone())); None }; drop(state); if let Some(event) = event { self.inner.updated.emit(event); } else { self.inner .added .emit(InventoryObjectAddedEventArgs::from_value(value)); } return Ok(()); } if parent_uuid != UUID::zero() && !state.items.contains_key(&parent_uuid) { let mut fake = InventoryFolder::new(parent_uuid)?; fake.version = InventoryFolder::VERSION_UNKNOWN; state.items.insert( parent_uuid, InventoryNode::from_value(InventoryValue::Folder(fake)), ); } let event = if let Some(node) = state.items.get(&uuid) { let old = node.value(); node.set_value(value.clone()); old.map(|old| InventoryObjectUpdatedEventArgs::from_values(old, value.clone())) } else { state .items .insert(uuid, InventoryNode::from_value(value.clone())); None }; rebuild_indexes_and_counts(&mut state); event }; if let Some(event) = event { self.inner.updated.emit(event); } else { self.inner .added .emit(InventoryObjectAddedEventArgs::from_value(value)); } Ok(()) } pub fn remove_node_for(&self, item: &dyn InventoryObjectClass) -> Result<(), Error> { let uuid = item.inventory_base().uuid; let removed = { let mut state = write(&self.inner.state); let Some(root) = state.items.get(&uuid).and_then(InventoryNode::value) else { return Ok(()); }; let mut pending = vec![uuid]; let mut ids = HashSet::new(); while let Some(current) = pending.pop() { if ids.len() >= MAX_CACHE_RECORDS || !ids.insert(current) { continue; } if let Some(node) = state.items.get(¤t) { pending.extend(read(&node.inner).children.keys().copied()); } } for id in ids { state.items.remove(&id); } if state.root == Some(uuid) { state.root = None; state.root_node = None; } if state.library_root == Some(uuid) { state.library_root = None; state.library_root_node = None; } rebuild_indexes_and_counts(&mut state); root }; self.inner .removed .emit(InventoryObjectRemovedEventArgs::from_value(removed)); Ok(()) } pub fn count(&self) -> i32 { i32::try_from(read(&self.inner.state).items.len()).unwrap_or(i32::MAX) } /// Returns the item with the requested identifier. /// /// # Panics /// /// Panics when the identifier is not present, matching the indexed C# /// inventory accessor. pub fn item(&self, uuid: UUID) -> InventoryBase { self.get_value_or_default_with_uuid(uuid) .ok() .flatten() .unwrap_or_else(|| panic!("unknown inventory item: {uuid}")) } pub fn set_item(&mut self, _uuid: UUID, value: InventoryBase) { let _ = self.update_node_for(&value); } pub fn owner(&self) -> UUID { self.inner.owner } pub fn root_folder(&self) -> Option { read(&self.inner.state) .root_node .as_ref() .and_then(InventoryNode::value) .and_then(|value| value.folder().cloned()) } pub fn set_root_folder(&mut self, value: Option) { let Some(value) = value else { return; }; let uuid = value.base.uuid; if self.update_node_for(&value).is_ok() { let mut state = write(&self.inner.state); state.root = Some(uuid); state.root_node = state.items.get(&uuid).cloned(); } } pub fn root_node(&self) -> InventoryNode { read(&self.inner.state) .root_node .clone() .unwrap_or_else(InventoryNode::empty) } pub fn set_root_node(&mut self, value: InventoryNode) { if let Some(uuid) = value.uuid() { let mut state = write(&self.inner.state); state.items.insert(uuid, value.clone()); state.root = Some(uuid); state.root_node = Some(value); rebuild_indexes_and_counts(&mut state); } } pub fn library_folder(&self) -> Option { read(&self.inner.state) .library_root_node .as_ref() .and_then(InventoryNode::value) .and_then(|value| value.folder().cloned()) } pub fn set_library_folder(&mut self, value: Option) { let Some(value) = value else { return; }; let uuid = value.base.uuid; if self.update_node_for(&value).is_ok() { let mut state = write(&self.inner.state); state.library_root = Some(uuid); state.library_root_node = state.items.get(&uuid).cloned(); } } pub fn library_root_node(&self) -> InventoryNode { read(&self.inner.state) .library_root_node .clone() .unwrap_or_else(InventoryNode::empty) } pub fn set_library_root_node(&mut self, value: InventoryNode) { if let Some(uuid) = value.uuid() { let mut state = write(&self.inner.state); state.items.insert(uuid, value.clone()); state.library_root = Some(uuid); state.library_root_node = Some(value); rebuild_indexes_and_counts(&mut state); } } } fn update_link_index( links: &mut HashMap>, uuid: UUID, value: &InventoryValue, insert: bool, ) { let Some(item) = value.item() else { return; }; if !item.is_link().unwrap_or(false) || item.asset_uuid == UUID::zero() { return; } if insert { links.entry(item.asset_uuid).or_default().insert(uuid); } else if let Some(records) = links.get_mut(&item.asset_uuid) { records.remove(&uuid); if records.is_empty() { links.remove(&item.asset_uuid); } } } fn rebuild_indexes_and_counts(state: &mut InventoryState) { state.links.clear(); let nodes: Vec<_> = state.items.values().cloned().collect(); for node in &nodes { let mut inner = write(&node.inner); inner.children.clear(); inner.parent = None; if let Some(folder) = inner.data.as_mut().and_then(InventoryValue::folder_mut) { folder.descendent_count = 0; } } for node in &nodes { let Some(value) = node.value() else { continue; }; if let Some(item) = value.item() && item.is_link().unwrap_or(false) && item.asset_uuid != UUID::zero() { state .links .entry(item.asset_uuid) .or_default() .insert(item.base.uuid); } let parent_uuid = value.base().parent_uuid; if parent_uuid == UUID::zero() || parent_uuid == value.base().uuid || would_create_cycle(state, value.base().uuid, parent_uuid) { continue; } if let Some(parent) = state.items.get(&parent_uuid) { write(&parent.inner) .children .insert(value.base().uuid, node.clone()); write(&node.inner).parent = Some(Arc::downgrade(&parent.inner)); } } for node in &nodes { let Some(value) = node.value() else { continue; }; if value.item().is_none() { continue; } let mut parent = value.base().parent_uuid; let mut visited = HashSet::new(); for _ in 0..MAX_HIERARCHY_DEPTH { if parent == UUID::zero() || !visited.insert(parent) { break; } let Some(parent_node) = state.items.get(&parent) else { break; }; let next = { let mut inner = write(&parent_node.inner); let Some(parent_value) = inner.data.as_mut() else { break; }; if let Some(folder) = parent_value.folder_mut() { folder.descendent_count = folder.descendent_count.saturating_add(1); } parent_value.base().parent_uuid }; parent = next; } } } fn would_create_cycle(state: &InventoryState, child: UUID, mut parent: UUID) -> bool { let mut visited = HashSet::new(); for _ in 0..MAX_HIERARCHY_DEPTH { if parent == child { return true; } if parent == UUID::zero() || !visited.insert(parent) { return false; } let Some(value) = state.items.get(&parent).and_then(InventoryNode::value) else { return false; }; parent = value.base().parent_uuid; } true } fn compare_inventory( left: &dyn InventoryObjectClass, right: &dyn InventoryObjectClass, order: crate::InventorySortOrder, ) -> std::cmp::Ordering { let left_folder = left.as_any().downcast_ref::(); let right_folder = right.as_any().downcast_ref::(); let system_top = order.0 & crate::InventorySortOrder::SYSTEM_FOLDERS_TO_TOP.0 != 0; if system_top { let left_system = left_folder.is_some_and(|folder| folder.preferred_type != FolderType::None); let right_system = right_folder.is_some_and(|folder| folder.preferred_type != FolderType::None); if left_system != right_system { return right_system.cmp(&left_system); } } if left_folder.is_some() != right_folder.is_some() { return right_folder.is_some().cmp(&left_folder.is_some()); } let by_date = order.0 & crate::InventorySortOrder::BY_DATE.0 != 0 && !(order.0 & crate::InventorySortOrder::FOLDERS_BY_NAME.0 != 0 && left_folder.is_some()); if by_date { let date = |value: &dyn InventoryObjectClass| { InventoryValue::from_class(value) .and_then(|value| value.item().map(InventoryItem::creation_date)) .unwrap_or(UNIX_EPOCH) }; date(right).cmp(&date(left)).then_with(|| { left.inventory_base() .name .to_lowercase() .cmp(&right.inventory_base().name.to_lowercase()) }) } else { left.inventory_base() .name .to_lowercase() .cmp(&right.inventory_base().name.to_lowercase()) } } impl Inventory { pub fn save_to_disk_with_string(&self, filename: String) -> Result<(), Error> { let snapshot = self.cache_snapshot()?; save_cache_atomic(Path::new(&filename), &snapshot) } pub async fn save_to_disk_with_string_cancellation_token( &self, filename: String, cancellation_token: Option, ) -> Result<(), Error> { check_cancelled(cancellation_token.as_ref())?; let snapshot = self.cache_snapshot()?; check_cancelled(cancellation_token.as_ref())?; run_blocking(move || save_cache_atomic(Path::new(&filename), &snapshot)).await?; check_cancelled(cancellation_token.as_ref()) } pub fn restore_from_disk_with_string(&self, filename: String) -> Result { match load_cache(Path::new(&filename)) { Ok(snapshot) => self.install_snapshot(snapshot), Err(_) => Ok(-1), } } pub async fn restore_from_disk_with_string_cancellation_token( &self, filename: String, cancellation_token: Option, ) -> Result { check_cancelled(cancellation_token.as_ref())?; let snapshot = run_blocking(move || load_cache(Path::new(&filename))).await?; check_cancelled(cancellation_token.as_ref())?; self.install_snapshot(snapshot) } fn cache_snapshot(&self) -> Result { let state = read(&self.inner.state); if state.items.len() > MAX_CACHE_RECORDS { return Err(Error::InvalidOperation); } let mut values: Vec<_> = state .items .values() .filter_map(InventoryNode::value) .collect(); values.sort_unstable_by_key(|value| value.base().uuid); Ok(CacheSnapshot { owner: self.inner.owner, root: state.root, library_root: state.library_root, values, }) } fn install_snapshot(&self, snapshot: CacheSnapshot) -> Result { if snapshot.owner != UUID::zero() && self.inner.owner != UUID::zero() && snapshot.owner != self.inner.owner { return Err(Error::InvalidOperation); } let restored_items = snapshot .values .iter() .filter(|value| value.item().is_some()) .count(); let mut state = InventoryState::default(); for value in snapshot.values { let uuid = value.base().uuid; if uuid == UUID::zero() || state .items .insert(uuid, InventoryNode::from_value(value)) .is_some() { return Err(Error::Parse { position: 0, context: "duplicate or zero inventory UUID", }); } } state.root = snapshot.root.filter(|uuid| state.items.contains_key(uuid)); state.library_root = snapshot .library_root .filter(|uuid| state.items.contains_key(uuid)); state.root_node = state.root.and_then(|uuid| state.items.get(&uuid).cloned()); state.library_root_node = state .library_root .and_then(|uuid| state.items.get(&uuid).cloned()); rebuild_indexes_and_counts(&mut state); *write(&self.inner.state) = state; i32::try_from(restored_items).map_err(|_| Error::InvalidOperation) } } struct CacheSnapshot { owner: UUID, root: Option, library_root: Option, values: Vec, } async fn run_blocking(operation: F) -> Result where T: Send + 'static, F: FnOnce() -> Result + Send + 'static, { let (sender, receiver) = futures_channel::oneshot::channel(); std::thread::Builder::new() .name("libremetaverse-inventory-io".into()) .spawn(move || { let _ = sender.send(operation()); }) .map_err(|_| Error::InvalidOperation)?; receiver.await.map_err(|_| Error::InvalidOperation)? } fn check_cancelled(token: Option<&CancellationToken>) -> Result<(), Error> { token.map_or(Ok(()), CancellationToken::throw_if_cancellation_requested) } fn save_cache_atomic(path: &Path, snapshot: &CacheSnapshot) -> Result<(), Error> { let bytes = encode_cache(snapshot)?; if bytes.len() as u64 > MAX_CACHE_BYTES { return Err(Error::InvalidOperation); } let parent = path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); let name = path .file_name() .and_then(|name| name.to_str()) .ok_or(Error::Argument)?; let nonce = UUID::random()?.to_string(); let temporary = parent.join(format!(".{name}.{nonce}.tmp")); let result = (|| { let mut file = OpenOptions::new() .create_new(true) .write(true) .open(&temporary) .map_err(|_| Error::InvalidOperation)?; file.write_all(&bytes) .map_err(|_| Error::InvalidOperation)?; file.sync_all().map_err(|_| Error::InvalidOperation)?; drop(file); replace_file(&temporary, path)?; if let Ok(directory) = File::open(parent) { let _ = directory.sync_all(); } Ok(()) })(); if result.is_err() { let _ = fs::remove_file(&temporary); } result } fn replace_file(temporary: &Path, destination: &Path) -> Result<(), Error> { match fs::rename(temporary, destination) { Ok(()) => Ok(()), Err(_) if destination.exists() => { let backup = backup_path(destination)?; fs::rename(destination, &backup).map_err(|_| Error::InvalidOperation)?; if fs::rename(temporary, destination).is_ok() { let _ = fs::remove_file(backup); Ok(()) } else { let _ = fs::rename(backup, destination); Err(Error::InvalidOperation) } } Err(_) => Err(Error::InvalidOperation), } } fn backup_path(destination: &Path) -> Result { let parent = destination.parent().unwrap_or_else(|| Path::new(".")); let name = destination .file_name() .and_then(|name| name.to_str()) .ok_or(Error::Argument)?; Ok(parent.join(format!(".{name}.{}.bak", UUID::random()?.to_string()))) } fn load_cache(path: &Path) -> Result { let metadata = fs::metadata(path).map_err(|_| Error::InvalidOperation)?; if metadata.len() > MAX_CACHE_BYTES { return Err(Error::InvalidOperation); } let capacity = usize::try_from(metadata.len()).map_err(|_| Error::InvalidOperation)?; let mut bytes = Vec::with_capacity(capacity); File::open(path) .map(|file| file.take(MAX_CACHE_BYTES + 1)) .and_then(|mut file| file.read_to_end(&mut bytes)) .map_err(|_| Error::InvalidOperation)?; if bytes.len() as u64 > MAX_CACHE_BYTES { return Err(Error::InvalidOperation); } decode_cache(&bytes) } fn encode_cache(snapshot: &CacheSnapshot) -> Result, Error> { let mut output = Vec::new(); cache_extend(&mut output, CACHE_MAGIC)?; cache_extend(&mut output, &CACHE_VERSION.to_le_bytes())?; write_uuid(&mut output, snapshot.owner)?; write_optional_uuid(&mut output, snapshot.root)?; write_optional_uuid(&mut output, snapshot.library_root)?; cache_extend( &mut output, &u32::try_from(snapshot.values.len()) .map_err(|_| Error::InvalidOperation)? .to_le_bytes(), )?; for value in &snapshot.values { encode_value(&mut output, value)?; } Ok(output) } fn encode_value(output: &mut Vec, value: &InventoryValue) -> Result<(), Error> { cache_push(output, value.tag())?; let base = value.base(); write_uuid(output, base.uuid)?; write_uuid(output, base.parent_uuid)?; write_uuid(output, base.owner_id)?; write_string(output, &base.name)?; if let Some(folder) = value.folder() { cache_push(output, (folder.preferred_type as i8).cast_unsigned())?; cache_extend(output, &folder.version.to_le_bytes())?; cache_extend(output, &folder.descendent_count.to_le_bytes())?; } else if let Some(item) = value.item() { write_uuid(output, item.asset_uuid)?; for mask in [ item.permissions.base_mask, item.permissions.everyone_mask, item.permissions.group_mask, item.permissions.next_owner_mask, item.permissions.owner_mask, ] { cache_extend(output, &mask.0.to_le_bytes())?; } cache_push(output, (item.asset_type as i8).cast_unsigned())?; cache_push(output, item.inventory_type.0.cast_unsigned())?; write_uuid(output, item.creator_id)?; write_string(output, &item.description)?; write_uuid(output, item.group_id)?; cache_push(output, u8::from(item.group_owned))?; cache_extend(output, &item.sale_price.to_le_bytes())?; cache_push(output, item.sale_type as u8)?; cache_extend(output, &item.flags.to_le_bytes())?; let (seconds, nanos) = system_time_parts(item.creation_date)?; cache_extend(output, &seconds.to_le_bytes())?; cache_extend(output, &nanos.to_le_bytes())?; write_uuid(output, item.transaction_id)?; write_uuid(output, item.last_owner_id)?; } Ok(()) } fn decode_cache(bytes: &[u8]) -> Result { let mut input = CacheReader { bytes, position: 0 }; if input.take(CACHE_MAGIC.len())? != CACHE_MAGIC { return input.error("invalid inventory cache magic"); } if input.u32()? != CACHE_VERSION { return input.error("unsupported inventory cache version"); } let owner = input.uuid()?; let root = input.optional_uuid()?; let library_root = input.optional_uuid()?; let count = usize::try_from(input.u32()?).map_err(|_| Error::InvalidOperation)?; if count > MAX_CACHE_RECORDS { return input.error("inventory cache record limit exceeded"); } let mut values = Vec::with_capacity(count); for _ in 0..count { values.push(decode_value(&mut input)?); } if input.position != bytes.len() { return input.error("trailing inventory cache bytes"); } Ok(CacheSnapshot { owner, root, library_root, values, }) } fn decode_value(input: &mut CacheReader<'_>) -> Result { let tag = input.u8()?; let base = InventoryBase { uuid: input.uuid()?, parent_uuid: input.uuid()?, owner_id: input.uuid()?, name: input.string()?, }; if tag == 0 { return Ok(InventoryValue::Base(base)); } if tag == 1 { return Ok(InventoryValue::Folder(InventoryFolder { base, preferred_type: folder_type_from_i32(i32::from(input.i8()?))?, version: input.i32()?, descendent_count: input.i32()?, })); } let item = InventoryItem { base, asset_uuid: input.uuid()?, permissions: Permissions { base_mask: PermissionMask(input.u32()?), everyone_mask: PermissionMask(input.u32()?), group_mask: PermissionMask(input.u32()?), next_owner_mask: PermissionMask(input.u32()?), owner_mask: PermissionMask(input.u32()?), }, asset_type: asset_type_from_i32(i32::from(input.i8()?))?, inventory_type: InventoryType(input.i8()?), creator_id: input.uuid()?, description: input.string()?, group_id: input.uuid()?, group_owned: input.u8()? != 0, sale_price: input.i32()?, sale_type: sale_type_from_i32(i32::from(input.u8()?))?, flags: input.u32()?, creation_date: system_time_from_parts(input.i64()?, input.u32()?)?, transaction_id: input.uuid()?, last_owner_id: input.uuid()?, }; Ok(match tag { 2 => InventoryValue::Item(item), 3 => InventoryValue::Animation(InventoryAnimation { base: item }), 4 => InventoryValue::Attachment(InventoryAttachment { base: item }), 5 => InventoryValue::CallingCard(InventoryCallingCard { base: item }), 6 => InventoryValue::Category(InventoryCategory { base: item }), 7 => InventoryValue::Gesture(InventoryGesture { base: item }), 8 => InventoryValue::Lsl(InventoryLSL { base: item }), 9 => InventoryValue::Landmark(InventoryLandmark { base: item }), 10 => InventoryValue::Material(InventoryMaterial { base: item }), 11 => InventoryValue::Notecard(InventoryNotecard { base: item }), 12 => InventoryValue::Object(InventoryObject { base: item }), 13 => InventoryValue::Settings(InventorySettings { base: item }), 14 => InventoryValue::Snapshot(InventorySnapshot { base: item }), 15 => InventoryValue::Sound(InventorySound { base: item }), 16 => InventoryValue::Texture(InventoryTexture { base: item }), 17 => InventoryValue::Wearable(InventoryWearable { base: item }), _ => return input.error("unknown inventory cache record tag"), }) } struct CacheReader<'a> { bytes: &'a [u8], position: usize, } impl<'a> CacheReader<'a> { fn error(&self, context: &'static str) -> Result { Err(Error::Parse { position: self.position, context, }) } fn take(&mut self, count: usize) -> Result<&'a [u8], Error> { let end = self .position .checked_add(count) .ok_or(Error::InvalidOperation)?; let result = self.bytes.get(self.position..end).ok_or(Error::Parse { position: self.position, context: "truncated inventory cache", })?; self.position = end; Ok(result) } fn u8(&mut self) -> Result { Ok(self.take(1)?[0]) } fn i8(&mut self) -> Result { Ok(self.u8()?.cast_signed()) } fn u32(&mut self) -> Result { Ok(u32::from_le_bytes( self.take(4)? .try_into() .map_err(|_| Error::InvalidOperation)?, )) } fn i32(&mut self) -> Result { Ok(i32::from_le_bytes( self.take(4)? .try_into() .map_err(|_| Error::InvalidOperation)?, )) } fn i64(&mut self) -> Result { Ok(i64::from_le_bytes( self.take(8)? .try_into() .map_err(|_| Error::InvalidOperation)?, )) } fn uuid(&mut self) -> Result { UUID::new_with_bytes_int32(self.take(16)?.to_vec(), 0) } fn optional_uuid(&mut self) -> Result, Error> { Ok(if self.u8()? == 0 { None } else { Some(self.uuid()?) }) } fn string(&mut self) -> Result { let count = usize::try_from(self.u32()?).map_err(|_| Error::InvalidOperation)?; if count > MAX_STRING_BYTES { return self.error("inventory cache string limit exceeded"); } String::from_utf8(self.take(count)?.to_vec()).map_err(|_| Error::Parse { position: self.position, context: "invalid inventory cache UTF-8", }) } } fn write_uuid(output: &mut Vec, value: UUID) -> Result<(), Error> { cache_extend(output, &value.get_bytes()?) } fn write_optional_uuid(output: &mut Vec, value: Option) -> Result<(), Error> { cache_push(output, u8::from(value.is_some()))?; if let Some(value) = value { write_uuid(output, value)?; } Ok(()) } fn write_string(output: &mut Vec, value: &str) -> Result<(), Error> { if value.len() > MAX_STRING_BYTES { return Err(Error::InvalidOperation); } cache_extend( output, &u32::try_from(value.len()) .map_err(|_| Error::InvalidOperation)? .to_le_bytes(), )?; cache_extend(output, value.as_bytes()) } fn cache_push(output: &mut Vec, value: u8) -> Result<(), Error> { cache_extend(output, &[value]) } fn cache_extend(output: &mut Vec, value: &[u8]) -> Result<(), Error> { let length = output .len() .checked_add(value.len()) .ok_or(Error::InvalidOperation)?; if length > usize::try_from(MAX_CACHE_BYTES).map_err(|_| Error::InvalidOperation)? { return Err(Error::InvalidOperation); } output.extend_from_slice(value); Ok(()) } fn system_time_parts(value: SystemTime) -> Result<(i64, u32), Error> { match value.duration_since(UNIX_EPOCH) { Ok(duration) => Ok(( i64::try_from(duration.as_secs()).map_err(|_| Error::InvalidOperation)?, duration.subsec_nanos(), )), Err(error) => { let duration = error.duration(); let seconds = i64::try_from(duration.as_secs()).map_err(|_| Error::InvalidOperation)?; if duration.subsec_nanos() == 0 { Ok((-seconds, 0)) } else { Ok(( seconds .checked_neg() .and_then(|value| value.checked_sub(1)) .ok_or(Error::InvalidOperation)?, 1_000_000_000 - duration.subsec_nanos(), )) } } } } fn system_time_from_parts(seconds: i64, nanos: u32) -> Result { if nanos >= 1_000_000_000 { return Err(Error::Argument); } if seconds >= 0 { Ok(UNIX_EPOCH + Duration::new(u64::try_from(seconds).map_err(|_| Error::Argument)?, nanos)) } else { UNIX_EPOCH .checked_sub(Duration::from_secs(seconds.unsigned_abs())) .and_then(|value| value.checked_add(Duration::from_nanos(u64::from(nanos)))) .ok_or(Error::Argument) } } fn osd_i32(map: &HashMap, key: &str) -> Result { map.get(key).map_or(Ok(0), OSD::as_integer) } fn osd_u32(map: &HashMap, key: &str) -> Result { map.get(key).map_or(Ok(0), OSD::as_u_integer) } fn osd_uuid(map: &HashMap, key: &str) -> Result { map.get(key).ok_or(Error::Argument)?.as_uuid() } fn optional_uuid(map: &HashMap, key: &str) -> Result, Error> { map.get(key).map(OSD::as_uuid).transpose() } fn parse_asset_type(value: Option<&OSD>) -> Result { match value { None => Ok(AssetType::Unknown), Some(OSD::String(value)) => Ok(match value.to_ascii_lowercase().as_str() { "texture" => AssetType::Texture, "sound" => AssetType::Sound, "callcard" | "callingcard" => AssetType::CallingCard, "landmark" => AssetType::Landmark, "script" => AssetType::Script, "clothing" => AssetType::Clothing, "object" => AssetType::Object, "notecard" => AssetType::Notecard, "category" | "folder" => AssetType::Folder, "lsltext" => AssetType::LSLText, "lslbyte" | "lslbytecode" => AssetType::LSLBytecode, "txtr_tga" => AssetType::TextureTGA, "bodypart" => AssetType::Bodypart, "snd_wav" => AssetType::SoundWAV, "img_tga" => AssetType::ImageTGA, "jpeg" | "img_jpeg" => AssetType::ImageJPEG, "animatn" | "animation" => AssetType::Animation, "gesture" => AssetType::Gesture, "simstate" => AssetType::Simstate, "link" => AssetType::Link, "link_f" | "linkfolder" => AssetType::LinkFolder, "mesh" => AssetType::Mesh, "settings" => AssetType::Settings, "material" => AssetType::Material, _ => AssetType::Unknown, }), Some(value) => asset_type_from_i32(value.as_integer()?), } } fn parse_inventory_type(value: Option<&OSD>) -> Result { match value { None => Ok(InventoryType::UNKNOWN), Some(OSD::String(value)) => Ok(match value.to_ascii_lowercase().as_str() { "texture" => InventoryType::TEXTURE, "sound" => InventoryType::SOUND, "callcard" | "callingcard" => InventoryType::CALLING_CARD, "landmark" => InventoryType::LANDMARK, "object" => InventoryType::OBJECT, "notecard" => InventoryType::NOTECARD, "category" | "folder" => InventoryType::CATEGORY, "root" | "rootcategory" => InventoryType::ROOT_CATEGORY, "lsl" | "script" => InventoryType::LSL, "snapshot" => InventoryType::SNAPSHOT, "attach" | "attachment" => InventoryType::ATTACHMENT, "wearable" => InventoryType::WEARABLE, "animation" => InventoryType::ANIMATION, "gesture" => InventoryType::GESTURE, "mesh" => InventoryType::MESH, "settings" => InventoryType::SETTINGS, "material" => InventoryType::MATERIAL, _ => InventoryType::UNKNOWN, }), Some(value) => Ok(InventoryType( i8::try_from(value.as_integer()?).map_err(|_| Error::Argument)?, )), } } fn inventory_type_name(value: InventoryType) -> String { match value { InventoryType::UNKNOWN => "Unknown".into(), InventoryType::TEXTURE => "Texture".into(), InventoryType::SOUND => "Sound".into(), InventoryType::CALLING_CARD => "CallingCard".into(), InventoryType::LANDMARK => "Landmark".into(), InventoryType::OBJECT => "Object".into(), InventoryType::NOTECARD => "Notecard".into(), InventoryType::CATEGORY => "Category".into(), InventoryType::ROOT_CATEGORY => "RootCategory".into(), InventoryType::LSL => "LSL".into(), InventoryType::SNAPSHOT => "Snapshot".into(), InventoryType::ATTACHMENT => "Attachment".into(), InventoryType::WEARABLE => "Wearable".into(), InventoryType::ANIMATION => "Animation".into(), InventoryType::GESTURE => "Gesture".into(), InventoryType::MESH => "Mesh".into(), InventoryType::SETTINGS => "Settings".into(), InventoryType::MATERIAL => "Material".into(), InventoryType(other) => other.to_string(), } } fn decrypt_shadow_id(shadow_id: UUID) -> Result { let magic = UUID::new_with_string("3c115e51-04f4-523c-9fa6-98aff1034730".into())?; Ok(UUID::bitxor(shadow_id, magic)) } fn asset_type_from_i32(value: i32) -> Result { Ok(match value { -1 => AssetType::Unknown, 0 => AssetType::Texture, 1 => AssetType::Sound, 2 => AssetType::CallingCard, 3 => AssetType::Landmark, 4 => AssetType::Script, 5 => AssetType::Clothing, 6 => AssetType::Object, 7 => AssetType::Notecard, 8 => AssetType::Folder, 10 => AssetType::LSLText, 11 => AssetType::LSLBytecode, 12 => AssetType::TextureTGA, 13 => AssetType::Bodypart, 17 => AssetType::SoundWAV, 18 => AssetType::ImageTGA, 19 => AssetType::ImageJPEG, 20 => AssetType::Animation, 21 => AssetType::Gesture, 22 => AssetType::Simstate, 24 => AssetType::Link, 25 => AssetType::LinkFolder, 40 => AssetType::Widget, 45 => AssetType::Person, 49 => AssetType::Mesh, 56 => AssetType::Settings, 57 => AssetType::Material, _ => return Err(Error::Argument), }) } fn folder_type_from_i32(value: i32) -> Result { Ok(match value { -1 => FolderType::None, 0 => FolderType::Texture, 1 => FolderType::Sound, 2 => FolderType::CallingCard, 3 => FolderType::Landmark, 5 => FolderType::Clothing, 6 => FolderType::Object, 7 => FolderType::Notecard, 8 => FolderType::Root, 9 => FolderType::OldRoot, 10 => FolderType::LSLText, 13 => FolderType::BodyPart, 14 => FolderType::Trash, 15 => FolderType::Snapshot, 16 => FolderType::LostAndFound, 20 => FolderType::Animation, 21 => FolderType::Gesture, 23 => FolderType::Favorites, 26 => FolderType::EnsembleStart, 45 => FolderType::EnsembleEnd, 46 => FolderType::CurrentOutfit, 47 => FolderType::Outfit, 48 => FolderType::MyOutfits, 49 => FolderType::Mesh, 50 => FolderType::Inbox, 51 => FolderType::Outbox, 52 => FolderType::BasicRoot, 53 => FolderType::MarketplaceListings, 54 => FolderType::MarketplaceStock, 55 => FolderType::MarketplaceVersion, 56 => FolderType::Settings, 57 => FolderType::Material, 100 => FolderType::Suitcase, _ => return Err(Error::Argument), }) } fn sale_type_from_i32(value: i32) -> Result { Ok(match value { 0 => SaleType::Not, 1 => SaleType::Original, 2 => SaleType::Copy, 3 => SaleType::Contents, _ => return Err(Error::Argument), }) } const fn attachment_point_from_u8(value: u8) -> AttachmentPoint { match value { 1 => AttachmentPoint::Chest, 2 => AttachmentPoint::Skull, 3 => AttachmentPoint::LeftShoulder, 4 => AttachmentPoint::RightShoulder, 5 => AttachmentPoint::LeftHand, 6 => AttachmentPoint::RightHand, 7 => AttachmentPoint::LeftFoot, 8 => AttachmentPoint::RightFoot, 9 => AttachmentPoint::Spine, 10 => AttachmentPoint::Pelvis, 11 => AttachmentPoint::Mouth, 12 => AttachmentPoint::Chin, 13 => AttachmentPoint::LeftEar, 14 => AttachmentPoint::RightEar, 15 => AttachmentPoint::LeftEyeball, 16 => AttachmentPoint::RightEyeball, 17 => AttachmentPoint::Nose, 18 => AttachmentPoint::RightUpperArm, 19 => AttachmentPoint::RightForearm, 20 => AttachmentPoint::LeftUpperArm, 21 => AttachmentPoint::LeftForearm, 22 => AttachmentPoint::RightHip, 23 => AttachmentPoint::RightUpperLeg, 24 => AttachmentPoint::RightLowerLeg, 25 => AttachmentPoint::LeftHip, 26 => AttachmentPoint::LeftUpperLeg, 27 => AttachmentPoint::LeftLowerLeg, 28 => AttachmentPoint::Stomach, 29 => AttachmentPoint::LeftPec, 30 => AttachmentPoint::RightPec, 31 => AttachmentPoint::HUDCenter2, 32 => AttachmentPoint::HUDTopRight, 33 => AttachmentPoint::HUDTop, 34 => AttachmentPoint::HUDTopLeft, 35 => AttachmentPoint::HUDCenter, 36 => AttachmentPoint::HUDBottomLeft, 37 => AttachmentPoint::HUDBottom, 38 => AttachmentPoint::HUDBottomRight, 39 => AttachmentPoint::Neck, 40 => AttachmentPoint::Root, 41 => AttachmentPoint::LeftHandRing, 42 => AttachmentPoint::RightHandRing, 43 => AttachmentPoint::TailBase, 44 => AttachmentPoint::TailTip, 45 => AttachmentPoint::LeftWing, 46 => AttachmentPoint::RightWing, 47 => AttachmentPoint::Jaw, 48 => AttachmentPoint::AltLeftEar, 49 => AttachmentPoint::AltRightEar, 50 => AttachmentPoint::AltLeftEye, 51 => AttachmentPoint::AltRightEye, 52 => AttachmentPoint::Tongue, 53 => AttachmentPoint::Groin, 54 => AttachmentPoint::LeftHindFoot, 55 => AttachmentPoint::RightHindFoot, _ => AttachmentPoint::Default, } } const fn wearable_type_from_u8(value: u8) -> WearableType { match value { 0 => WearableType::Shape, 1 => WearableType::Skin, 2 => WearableType::Hair, 3 => WearableType::Eyes, 4 => WearableType::Shirt, 5 => WearableType::Pants, 6 => WearableType::Shoes, 7 => WearableType::Socks, 8 => WearableType::Jacket, 9 => WearableType::Gloves, 10 => WearableType::Undershirt, 11 => WearableType::Underpants, 12 => WearableType::Skirt, 13 => WearableType::Alpha, 14 => WearableType::Tattoo, 15 => WearableType::Physics, 16 => WearableType::Universal, _ => WearableType::Invalid, } } #[cfg(test)] mod tests { use super::*; use std::sync::atomic::{AtomicUsize, Ordering}; fn fixture() -> (Arc, Inventory) { let client = Arc::new(GridClient::new().expect("client")); let inventory = Inventory::new_with_grid_client_uuid(Arc::clone(&client), UUID::random().unwrap()) .unwrap(); (client, inventory) } fn temporary_file(name: &str) -> PathBuf { std::env::temp_dir().join(format!( "metacrate-{name}-{}", UUID::random().unwrap().to_string() )) } #[test] fn cache_round_trip_preserves_roots_concrete_type_and_permissions() { let (_client, mut inventory) = fixture(); let mut root = InventoryFolder::new(UUID::random().unwrap()).unwrap(); root.base.set_name("My Inventory".into()); root.set_preferred_type(FolderType::Root); let root_id = root.base.uuid(); inventory.set_root_folder(Some(root)); let mut library = InventoryFolder::new(UUID::random().unwrap()).unwrap(); library.base.set_name("Library".into()); library.set_preferred_type(FolderType::BasicRoot); let library_id = library.base.uuid(); inventory.set_library_folder(Some(library)); let mut wearable = InventoryWearable::new(UUID::random().unwrap()).unwrap(); wearable.base.base.set_parent_uuid(root_id); wearable.base.base.set_name("Shirt".into()); wearable.base.set_asset_type(AssetType::Clothing); wearable.base.set_asset_uuid(UUID::random().unwrap()); wearable .base .set_permissions(Permissions::new(1, 2, 3, 4, 5).unwrap()); wearable .base .set_creation_date(UNIX_EPOCH - Duration::from_millis(500)); wearable.set_wearable_type(WearableType::Shirt); let wearable_id = wearable.base.base.uuid(); inventory.update_node_for(&wearable).unwrap(); let path = temporary_file("inventory-roundtrip"); inventory .save_to_disk_with_string(path.to_string_lossy().into_owned()) .unwrap(); inventory.clear().unwrap(); assert_eq!(inventory.count(), 0); assert_eq!(inventory.root_folder().unwrap().base.uuid(), root_id); assert_eq!(inventory.library_folder().unwrap().base.uuid(), library_id); assert_eq!( inventory .restore_from_disk_with_string(path.to_string_lossy().into_owned()) .unwrap(), 1 ); let restored = inventory .get_value_or_default_with_uuid_a7c63fbe::(wearable_id) .unwrap() .unwrap(); assert_eq!(restored.wearable_type(), WearableType::Shirt); assert_eq!( restored.base.creation_date(), UNIX_EPOCH - Duration::from_millis(500) ); assert_eq!( restored.base.permissions(), Permissions::new(1, 2, 3, 4, 5).unwrap() ); assert_eq!(inventory.root_folder().unwrap().base.uuid(), root_id); assert_eq!(inventory.library_folder().unwrap().base.uuid(), library_id); let _ = fs::remove_file(path); } #[test] fn models_preserve_csharp_equality_shadow_and_indexer_rules() { let item_id = UUID::random().unwrap(); let mut left = InventoryItem::new_with_uuid(item_id).unwrap(); let mut right = left.clone(); left.set_creator_id(UUID::random().unwrap()); right.set_creator_id(UUID::random().unwrap()); left.set_transaction_id(UUID::random().unwrap()); right.set_transaction_id(UUID::random().unwrap()); assert_eq!(left, right, "C# equality excludes transient IDs"); let asset_id = UUID::random().unwrap(); let shadow_id = UUID::bitxor( asset_id, UUID::new_with_string("3c115e51-04f4-523c-9fa6-98aff1034730".into()).unwrap(), ); left.update( OSDMap::new_with_dictionary(HashMap::from([( "shadow_id".into(), OSD::UUID(shadow_id), )])) .unwrap(), ) .unwrap(); assert_eq!(left.asset_uuid(), asset_id); let (_client, mut inventory) = fixture(); let folder = InventoryFolder::new(UUID::random().unwrap()).unwrap(); let folder_id = folder.base.uuid(); let mismatched_key = UUID::random().unwrap(); inventory.set_item(mismatched_key, folder.base); assert!(inventory.contains_with_uuid(folder_id).unwrap()); assert!(!inventory.contains_with_uuid(mismatched_key).unwrap()); } #[tokio::test] async fn cache_rejects_corruption_and_unknown_versions() { let (_client, inventory) = fixture(); let path = temporary_file("inventory-corrupt"); fs::write(&path, b"not-an-inventory-cache").unwrap(); assert_eq!( inventory .restore_from_disk_with_string(path.to_string_lossy().into_owned()) .unwrap(), -1 ); assert!( inventory .restore_from_disk_with_string_cancellation_token( path.to_string_lossy().into_owned(), None ) .await .is_err() ); let snapshot = inventory.cache_snapshot().unwrap(); let mut bytes = encode_cache(&snapshot).unwrap(); bytes[8..12].copy_from_slice(&99_u32.to_le_bytes()); fs::write(&path, bytes).unwrap(); assert_eq!( inventory .restore_from_disk_with_string(path.to_string_lossy().into_owned()) .unwrap(), -1 ); assert!( inventory .restore_from_disk_with_string_cancellation_token( path.to_string_lossy().into_owned(), None ) .await .is_err() ); let _ = fs::remove_file(path); } #[test] fn callbacks_run_after_store_lock_is_released() { let (_client, inventory) = fixture(); let reentrant = inventory.clone(); let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); let _subscription = inventory.subscribe_inventory_object_added(Arc::new(move |event| { assert!(reentrant.contains_with_uuid(event.obj().uuid()).unwrap()); assert!(reentrant.count() > 0); observed.fetch_add(1, Ordering::SeqCst); })); let folder = InventoryFolder::new(UUID::random().unwrap()).unwrap(); inventory.update_node_for(&folder).unwrap(); assert_eq!(calls.load(Ordering::SeqCst), 1); } #[test] fn root_item_fast_path_keeps_link_index_and_events_consistent() { let (_client, inventory) = fixture(); let item_id = UUID::random().unwrap(); let first_target = UUID::random().unwrap(); let second_target = UUID::random().unwrap(); let mut item = InventoryItem::new_with_uuid(item_id).unwrap(); item.set_asset_type(AssetType::Link); item.set_asset_uuid(first_target); inventory.update_node_for(&item).unwrap(); assert_eq!(inventory.find_all_links(first_target).unwrap().len(), 1); item.set_asset_uuid(second_target); inventory.update_node_for(&item).unwrap(); assert!(inventory.find_all_links(first_target).unwrap().is_empty()); assert_eq!(inventory.find_all_links(second_target).unwrap().len(), 1); item.set_asset_type(AssetType::Texture); inventory.update_node_for(&item).unwrap(); assert!(inventory.find_all_links(second_target).unwrap().is_empty()); assert_eq!(inventory.count(), 1); } #[test] fn system_folders_sort_first_and_are_discoverable() { let (_client, mut inventory) = fixture(); let root = InventoryFolder::new(UUID::random().unwrap()).unwrap(); let root_id = root.base.uuid(); inventory.set_root_folder(Some(root)); let mut ordinary = InventoryFolder::new(UUID::random().unwrap()).unwrap(); ordinary.base.set_parent_uuid(root_id); ordinary.base.set_name("A ordinary".into()); inventory.update_node_for(&ordinary).unwrap(); let mut trash = InventoryFolder::new(UUID::random().unwrap()).unwrap(); trash.base.set_parent_uuid(root_id); trash.base.set_name("Z trash".into()); trash.set_preferred_type(FolderType::Trash); let trash_id = trash.base.uuid(); inventory.update_node_for(&trash).unwrap(); let sorted = inventory .get_contents_sorted_with_uuid( root_id, crate::InventorySortOrder(crate::InventorySortOrder::SYSTEM_FOLDERS_TO_TOP.0), ) .unwrap(); assert_eq!(sorted[0].inventory_base().uuid(), trash_id); assert_eq!( inventory.find_folder_for_type(FolderType::Trash), Some(trash_id) ); } }