Files
MetaCrate/crates/libremetaverse/src/inventory.rs
Chili Palmer 738fe3933e
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m8s
Native code generation / deterministic (push) Failing after 2m6s
Concurrency and resource soak audit / soak (push) Failing after 12m13s
Documentation / documentation (push) Failing after 1m36s
Imaging and meshing gate / native (push) Failing after 3m2s
JPEG 2000 feature / linux (push) Successful in 2m48s
performance evidence / audit (push) Failing after 13m49s
Release platform and feature matrix / audit (push) Successful in 44s
Native Rust workspace compile / compile (push) Failing after 55s
Skia feature / linux (push) Successful in 31m13s
Dependency and supply-chain audit / audit (push) Failing after 9m13s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Failing after 9m49s
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
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Benchmark Rust against pinned C# reference (#105)
2026-08-12 03:26:56 +00:00

3059 lines
105 KiB
Rust

//! 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<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
value
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write<T>(value: &RwLock<T>) -> 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<Self, Error> {
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<Object>) -> bool {
obj.as_ref()
.and_then(Object::downcast_ref::<Self>)
.is_some_and(|other| self == other)
}
pub fn from_osd(llsd: OSD) -> Result<Self, Error> {
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<Self, Error> {
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<OSD, Error> {
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<bool, Error> {
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<Object>) -> bool {
obj.as_ref()
.and_then(Object::downcast_ref::<Self>)
.is_some_and(|other| self == other)
}
pub fn get_hash_code(&self) -> i32 {
self.uuid.get_hash_code()
}
pub fn get_osd(&self) -> Result<OSD, Error> {
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<H: Hasher>(&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<Self, Error> {
Ok(Self::with_type(type_, item_id))
}
pub fn new_with_uuid(uuid: UUID) -> Result<Self, Error> {
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<Object>) -> bool {
obj.as_ref()
.and_then(Object::downcast_ref::<Self>)
.is_some_and(|other| self == other)
}
pub fn from_osd(data: OSD) -> Result<Self, Error> {
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<OSD, Error> {
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<bool, Error> {
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<String, OSD>) -> 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<Self, Error> {
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<Object>) -> bool {
obj.as_ref()
.and_then(Object::downcast_ref::<Self>)
.is_some_and(|other| self == other)
}
pub fn from_osd(data: OSD) -> Result<Self, Error> {
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<OSD, Error> {
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<String, OSD>) -> 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<Self, Error> {
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<Self, Error> {
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<Self, Error> {
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<Self, Error> {
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<Self, Error> {
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<Self> {
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<dyn InventoryObjectClass> {
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<T: Clone + 'static>(&self) -> Option<T> {
self.boxed().as_any().downcast_ref::<T>().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<RwLock<InventoryNodeInner>>,
}
#[derive(Debug)]
struct InventoryNodeInner {
data: Option<InventoryValue>,
parent: Option<Weak<RwLock<InventoryNodeInner>>>,
children: HashMap<UUID, InventoryNode>,
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<Self, Error> {
Ok(Self::empty())
}
pub fn new_with_inventory_base(data: &dyn InventoryObjectClass) -> Result<Self, Error> {
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<Self, Error> {
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<UUID> {
read(&self.inner)
.data
.as_ref()
.map(|value| value.base().uuid)
}
fn value(&self) -> Option<InventoryValue> {
read(&self.inner).data.clone()
}
fn set_value(&self, value: InventoryValue) {
write(&self.inner).data = Some(value);
}
pub fn data(&self) -> Option<Box<dyn InventoryObjectClass>> {
self.value().map(|value| value.boxed())
}
pub fn set_data(&mut self, value: Option<Box<dyn InventoryObjectClass>>) {
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<Self> {
read(&self.inner)
.parent
.as_ref()
.and_then(Weak::upgrade)
.map(|inner| Self { inner })
}
pub fn set_parent(&mut self, value: Option<Self>) {
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<Self, Error> {
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<bool, Error> {
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<UUID> {
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<InventoryNode> {
read(&self.parent.inner)
.children
.values()
.cloned()
.collect()
}
}
#[derive(Clone, Debug)]
pub struct InventoryObjectAddedEventArgs {
value: InventoryValue,
}
impl InventoryObjectAddedEventArgs {
pub fn new(obj: InventoryBase) -> Result<Self, Error> {
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<dyn InventoryObjectClass> {
self.value.boxed()
}
}
#[derive(Clone, Debug)]
pub struct InventoryObjectRemovedEventArgs {
value: InventoryValue,
}
impl InventoryObjectRemovedEventArgs {
pub fn new(obj: InventoryBase) -> Result<Self, Error> {
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<dyn InventoryObjectClass> {
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<Self, Error> {
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<dyn InventoryObjectClass> {
self.old.boxed()
}
pub fn new_value(&self) -> Box<dyn InventoryObjectClass> {
self.new.boxed()
}
}
#[derive(Default)]
struct InventoryState {
items: HashMap<UUID, InventoryNode>,
links: HashMap<UUID, HashSet<UUID>>,
root: Option<UUID>,
library_root: Option<UUID>,
root_node: Option<InventoryNode>,
library_root_node: Option<InventoryNode>,
}
struct InventoryInner {
owner: UUID,
_client: Weak<GridClient>,
state: RwLock<InventoryState>,
added: EventRegistry<InventoryObjectAddedEventArgs>,
removed: EventRegistry<InventoryObjectRemovedEventArgs>,
updated: EventRegistry<InventoryObjectUpdatedEventArgs>,
}
#[derive(Clone)]
pub struct Inventory {
inner: Arc<InventoryInner>,
}
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<InventoryObjectAddedEventArgs>,
) -> Subscription {
self.inner.added.subscribe(handler)
}
pub fn subscribe_inventory_object_removed(
&self,
handler: EventHandler<InventoryObjectRemovedEventArgs>,
) -> Subscription {
self.inner.removed.subscribe(handler)
}
pub fn subscribe_inventory_object_updated(
&self,
handler: EventHandler<InventoryObjectUpdatedEventArgs>,
) -> Subscription {
self.inner.updated.subscribe(handler)
}
pub fn new_with_grid_client(client: Arc<GridClient>) -> Result<Self, Error> {
let owner = client.network().native_agent_id();
Self::new_with_grid_client_uuid(client, owner)
}
pub fn new_with_grid_client_uuid(client: Arc<GridClient>, owner: UUID) -> Result<Self, Error> {
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<bool, Error> {
self.contains_with_uuid(obj.inventory_base().uuid)
}
pub fn contains_with_uuid(&self, uuid: UUID) -> Result<bool, Error> {
Ok(read(&self.inner.state).items.contains_key(&uuid))
}
pub fn find_all_links(&self, asset_id: UUID) -> Result<Vec<InventoryNode>, 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<Vec<Box<dyn InventoryObjectClass>>, Error> {
self.get_contents_with_uuid(folder.base.uuid)
}
pub fn get_contents_with_uuid(
&self,
folder: UUID,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, 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<Vec<Box<dyn InventoryObjectClass>>, 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<UUID> {
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<InventoryNode, Error> {
read(&self.inner.state)
.items
.get(&uuid)
.cloned()
.ok_or(Error::InvalidOperation)
}
pub fn get_node_or_default(&self, uuid: UUID) -> Result<Option<InventoryNode>, Error> {
Ok(read(&self.inner.state).items.get(&uuid).cloned())
}
pub fn try_get_node_for(&self, uuid: UUID, node: &mut Option<InventoryNode>) -> 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<Option<InventoryBase>, 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<T: Clone + 'static>(
&self,
uuid: UUID,
) -> Result<Option<T>, Error> {
Ok(read(&self.inner.state)
.items
.get(&uuid)
.and_then(InventoryNode::value)
.and_then(|value| value.clone_as::<T>()))
}
/// 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<InventoryItem> {
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<InventoryFolder>,
items: Vec<InventoryItem>,
removed_items: &[UUID],
removed_categories: &[UUID],
broken_links: &[UUID],
versions: &HashMap<UUID, i32>,
) -> 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<Box<dyn InventoryObjectClass>>,
) -> Result<bool, Error> {
*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<T: Clone + 'static>(
&self,
uuid: UUID,
item: &mut Option<T>,
) -> bool {
*item = read(&self.inner.state)
.items
.get(&uuid)
.and_then(InventoryNode::value)
.and_then(|value| value.clone_as::<T>());
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(&current) {
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<InventoryFolder> {
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<InventoryFolder>) {
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<InventoryFolder> {
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<InventoryFolder>) {
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, HashSet<UUID>>,
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::<InventoryFolder>();
let right_folder = right.as_any().downcast_ref::<InventoryFolder>();
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<CancellationToken>,
) -> 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<i32, Error> {
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<CancellationToken>,
) -> Result<i32, Error> {
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<CacheSnapshot, Error> {
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<i32, Error> {
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<UUID>,
library_root: Option<UUID>,
values: Vec<InventoryValue>,
}
async fn run_blocking<T, F>(operation: F) -> Result<T, Error>
where
T: Send + 'static,
F: FnOnce() -> Result<T, Error> + 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<PathBuf, Error> {
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<CacheSnapshot, Error> {
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<Vec<u8>, 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<u8>, 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<CacheSnapshot, Error> {
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<InventoryValue, Error> {
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<T>(&self, context: &'static str) -> Result<T, Error> {
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<u8, Error> {
Ok(self.take(1)?[0])
}
fn i8(&mut self) -> Result<i8, Error> {
Ok(self.u8()?.cast_signed())
}
fn u32(&mut self) -> Result<u32, Error> {
Ok(u32::from_le_bytes(
self.take(4)?
.try_into()
.map_err(|_| Error::InvalidOperation)?,
))
}
fn i32(&mut self) -> Result<i32, Error> {
Ok(i32::from_le_bytes(
self.take(4)?
.try_into()
.map_err(|_| Error::InvalidOperation)?,
))
}
fn i64(&mut self) -> Result<i64, Error> {
Ok(i64::from_le_bytes(
self.take(8)?
.try_into()
.map_err(|_| Error::InvalidOperation)?,
))
}
fn uuid(&mut self) -> Result<UUID, Error> {
UUID::new_with_bytes_int32(self.take(16)?.to_vec(), 0)
}
fn optional_uuid(&mut self) -> Result<Option<UUID>, Error> {
Ok(if self.u8()? == 0 {
None
} else {
Some(self.uuid()?)
})
}
fn string(&mut self) -> Result<String, Error> {
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<u8>, value: UUID) -> Result<(), Error> {
cache_extend(output, &value.get_bytes()?)
}
fn write_optional_uuid(output: &mut Vec<u8>, value: Option<UUID>) -> 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<u8>, 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<u8>, value: u8) -> Result<(), Error> {
cache_extend(output, &[value])
}
fn cache_extend(output: &mut Vec<u8>, 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<SystemTime, Error> {
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<String, OSD>, key: &str) -> Result<i32, Error> {
map.get(key).map_or(Ok(0), OSD::as_integer)
}
fn osd_u32(map: &HashMap<String, OSD>, key: &str) -> Result<u32, Error> {
map.get(key).map_or(Ok(0), OSD::as_u_integer)
}
fn osd_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<UUID, Error> {
map.get(key).ok_or(Error::Argument)?.as_uuid()
}
fn optional_uuid(map: &HashMap<String, OSD>, key: &str) -> Result<Option<UUID>, Error> {
map.get(key).map(OSD::as_uuid).transpose()
}
fn parse_asset_type(value: Option<&OSD>) -> Result<AssetType, Error> {
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<InventoryType, Error> {
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<UUID, Error> {
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<AssetType, Error> {
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<FolderType, Error> {
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<SaleType, Error> {
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<GridClient>, 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::<InventoryWearable>(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)
);
}
}