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
3059 lines
105 KiB
Rust
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(¤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<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)
|
|
);
|
|
}
|
|
}
|