//! Native inventory protocol manager. //! //! This module owns request correlation and wire behavior. The hierarchy in //! [`crate::inventory`] remains a transport-free store, which lets packet and //! capability replies be validated before they become observable local state. //! Store snapshots and subscriber lists are cloned before asynchronous work or //! event delivery, so callbacks are not invoked while an inventory lock is held. #![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API. #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together. #![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs. use crate::agent_manager::{EventRegistry, InstantMessage, InstantMessageEventArgs}; use crate::client_core::ClientWeakHandle; use crate::inventory::{ Inventory, InventoryAnimation, InventoryAttachment, InventoryBase, InventoryCallingCard, InventoryCategory, InventoryFolder, InventoryGesture, InventoryItem, InventoryLSL, InventoryLandmark, InventoryMaterial, InventoryNotecard, InventoryObject, InventorySettings, InventorySnapshot, InventorySound, InventoryTexture, InventoryWearable, Permissions, }; use crate::messages::linden::{CopyInventoryFromNotecardMessage, ScriptRunningReplyMessage}; use crate::network_manager::{CapsEventQueueCallback, RawPacketReceivedEventArgs, Simulator}; use crate::packet_catalog::GeneratedPacket; use crate::packets::{ AbortXferPacket, BulkUpdateInventoryPacket, ConfirmXferPacketPacket, CopyInventoryFromNotecardPacket, CopyInventoryFromNotecardPacketInventoryDataBlock, CopyInventoryItemPacket, CopyInventoryItemPacketInventoryDataBlock, CreateInventoryFolderPacket, CreateInventoryItemPacket, DeRezObjectPacket, DeRezObjectPacketObjectDataBlock, FetchInventoryPacket, FetchInventoryPacketInventoryDataBlock, FetchInventoryReplyPacket, GetScriptRunningPacket, InventoryDescendentsPacket, LinkInventoryItemPacket, MoveInventoryFolderPacket, MoveInventoryFolderPacketInventoryDataBlock, MoveInventoryItemPacket, MoveInventoryItemPacketInventoryDataBlock, MoveTaskInventoryPacket, PacketType, PurgeInventoryDescendentsPacket, RemoveInventoryObjectsPacket, RemoveInventoryObjectsPacketFolderDataBlock, RemoveInventoryObjectsPacketItemDataBlock, RemoveTaskInventoryPacket, ReplyTaskInventoryPacket, RequestTaskInventoryPacket, RequestXferPacket, RezObjectPacket, RezRestoreToWorldPacket, RezScriptPacket, SaveAssetIntoInventoryPacket, SendXferPacketPacket, SetScriptRunningPacket, UpdateCreateInventoryItemPacket, UpdateInventoryFolderPacket, UpdateInventoryFolderPacketFolderDataBlock, UpdateInventoryItemPacket, UpdateInventoryItemPacketInventoryDataBlock, UpdateTaskInventoryPacket, }; use crate::{ DeRezDestination, GridClient, InstantMessageDialog, InstantMessageOnline, InventoryItemClass, InventoryObjectClass, InventorySortOrder, PermissionMask, }; use crate::{LoginResponseData, NetworkManagerLoginResponseCallback}; use futures_channel::oneshot; use futures_util::future::{Either, select}; use libremetaverse_structured_data::{OSD, OSDParser}; use libremetaverse_types::compat::{ CancellationToken, EventHandler, ExternalError, Subscription, Task, Uri, }; use libremetaverse_types::{ AssetType, EnumValueMetadata, Error, FolderType, InventoryType, Quaternion, SaleType, UUID, Utils, Vector3, Vector3d, WearableType, }; use std::collections::{HashMap, HashSet}; use std::fmt; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::{Arc, Condvar, Mutex, RwLock}; use std::thread::JoinHandle; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; const MAGIC_ID_TEXT: &str = "3c115e51-04f4-523c-9fa6-98aff1034730"; const MAX_TASK_INVENTORY_BYTES: usize = 16 * 1024 * 1024; const MAX_TASK_INVENTORY_ENTRIES: usize = 100_000; const MAX_PENDING_REQUESTS: usize = 4_096; const CALLBACK_TIMEOUT: Duration = Duration::from_mins(1); const CALLBACK_SWEEP_INTERVAL: Duration = Duration::from_millis(100); const MAX_INVENTORY_RECURSION_DEPTH: usize = 512; const MAX_INVENTORY_TRAVERSAL: usize = 100_000; macro_rules! fill_inventory_wire_block { ($block:expr, $item:expr, $transaction_id:expr, $type_value:expr) => {{ let item = $item; let permissions = item.permissions(); $block.item_id = item.base.uuid(); $block.folder_id = item.base.parent_uuid(); $block.creator_id = item.creator_id(); $block.owner_id = item.base.owner_id(); $block.group_id = item.group_id(); $block.base_mask = permissions.base_mask.0; $block.owner_mask = permissions.owner_mask.0; $block.group_mask = permissions.group_mask.0; $block.everyone_mask = permissions.everyone_mask.0; $block.next_owner_mask = permissions.next_owner_mask.0; $block.group_owned = item.group_owned(); $block.transaction_id = $transaction_id; $block.type_ = $type_value; $block.inv_type = item.inventory_type().0; $block.flags = item.flags(); $block.sale_type = item.sale_type() as u8; $block.sale_price = item.sale_price(); $block.name = Utils::string_to_bytes(item.base.name())?; $block.description = Utils::string_to_bytes(item.description())?; $block.creation_date = unix_seconds_i32(item.creation_date()); }}; } fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { value .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn read(value: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { value .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn write(value: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { value .write() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn expire_waiters( requests: &Mutex>>, now: Instant, deadline: impl Fn(&T) -> Instant, ) -> Vec { let mut requests = mutex(requests); let keys: Vec<_> = requests.keys().copied().collect(); let mut expired = Vec::new(); for key in keys { if let Some(waiters) = requests.get_mut(&key) { let mut index = 0; while index < waiters.len() { if deadline(&waiters[index]) <= now { expired.push(waiters.swap_remove(index)); } else { index += 1; } } if waiters.is_empty() { requests.remove(&key); } } } expired } fn wire_string(mut value: Vec) -> String { while value.last() == Some(&0) { value.pop(); } String::from_utf8_lossy(&value).into_owned() } fn decode_packet(bytes: &[u8]) -> Result { let mut packet = T::new_generated(); let mut position = 0_i32; let mut packet_end = i32::try_from(bytes.len()).map_err(|_| Error::Argument)? - 1; let mut zero_buffer = vec![0_u8; crate::udp_transport::UdpTransportConfig::default().max_decoded_packet_size]; packet.decode_from_bytes( bytes, &mut position, &mut packet_end, Some(zero_buffer.as_mut_slice()), )?; Ok(packet) } fn unix_seconds_i64(value: SystemTime) -> i64 { match value.duration_since(UNIX_EPOCH) { Ok(duration) => i64::try_from(duration.as_secs()).unwrap_or(i64::MAX), Err(error) => -i64::try_from(error.duration().as_secs()).unwrap_or(i64::MAX), } } fn unix_seconds_u32_wrapping(value: SystemTime) -> u32 { let bytes = unix_seconds_i64(value).to_le_bytes(); u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) } fn uuid_crc(value: UUID) -> u32 { let mut value = value; value.crc().unwrap_or_default() } fn system_time_from_unix_i32(seconds: i32) -> SystemTime { if seconds >= 0 { UNIX_EPOCH + Duration::from_secs(seconds.unsigned_abs().into()) } else { UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs().into()) } } fn unix_seconds_i32(value: SystemTime) -> i32 { i32::try_from(unix_seconds_i64(value)).unwrap_or_else(|_| { if value >= UNIX_EPOCH { i32::MAX } else { i32::MIN } }) } fn asset_type_from_i8(value: i8) -> AssetType { 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, _ => AssetType::Unknown, } } fn folder_type_from_i8(value: i8) -> FolderType { folder_type_from_i8_checked(value).unwrap_or(FolderType::None) } fn folder_type_from_i8_checked(value: i8) -> Option { match value { -1 => Some(FolderType::None), 0 => Some(FolderType::Texture), 1 => Some(FolderType::Sound), 2 => Some(FolderType::CallingCard), 3 => Some(FolderType::Landmark), 5 => Some(FolderType::Clothing), 6 => Some(FolderType::Object), 7 => Some(FolderType::Notecard), 8 => Some(FolderType::Root), 9 => Some(FolderType::OldRoot), 10 => Some(FolderType::LSLText), 13 => Some(FolderType::BodyPart), 14 => Some(FolderType::Trash), 15 => Some(FolderType::Snapshot), 16 => Some(FolderType::LostAndFound), 20 => Some(FolderType::Animation), 21 => Some(FolderType::Gesture), 23 => Some(FolderType::Favorites), 26 => Some(FolderType::EnsembleStart), 45 => Some(FolderType::EnsembleEnd), 46 => Some(FolderType::CurrentOutfit), 47 => Some(FolderType::Outfit), 48 => Some(FolderType::MyOutfits), 49 => Some(FolderType::Mesh), 50 => Some(FolderType::Inbox), 51 => Some(FolderType::Outbox), 52 => Some(FolderType::BasicRoot), 53 => Some(FolderType::MarketplaceListings), 54 => Some(FolderType::MarketplaceStock), 55 => Some(FolderType::MarketplaceVersion), 56 => Some(FolderType::Settings), 57 => Some(FolderType::Material), 100 => Some(FolderType::Suitcase), _ => None, } } fn sale_type_from_u8(value: u8) -> SaleType { match value { 1 => SaleType::Original, 2 => SaleType::Copy, 3 => SaleType::Contents, _ => SaleType::Not, } } #[derive(Clone)] struct InventoryWireData { asset_id: UUID, base_mask: u32, creation_date: i32, creator_id: UUID, description: Vec, everyone_mask: u32, flags: u32, folder_id: UUID, group_id: UUID, group_mask: u32, group_owned: bool, inv_type: i8, item_id: UUID, name: Vec, next_owner_mask: u32, owner_id: UUID, owner_mask: u32, sale_price: i32, sale_type: u8, type_: i8, } fn inventory_item_from_wire(data: InventoryWireData) -> Result { let mut inventory_type = InventoryType(data.inv_type); let asset_type = asset_type_from_i8(data.type_); if inventory_type == InventoryType::TEXTURE && matches!(asset_type, AssetType::Object | AssetType::Mesh) { inventory_type = InventoryType::ATTACHMENT; } let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, data.item_id)?; item.set_asset_uuid(data.asset_id); item.set_asset_type(asset_type); item.set_creation_date(system_time_from_unix_i32(data.creation_date)); item.set_creator_id(data.creator_id); item.set_description(wire_string(data.description)); item.set_flags(data.flags); item.set_group_id(data.group_id); item.set_group_owned(data.group_owned); item.set_inventory_type(inventory_type); item.base.set_name(wire_string(data.name)); item.base.set_owner_id(data.owner_id); item.base.set_parent_uuid(data.folder_id); item.set_permissions(Permissions::new( data.base_mask, data.everyone_mask, data.group_mask, data.next_owner_mask, data.owner_mask, )?); item.set_sale_price(data.sale_price); item.set_sale_type(sale_type_from_u8(data.sale_type)); Ok(item) } fn typed_inventory_item(item: InventoryItem) -> Box { macro_rules! boxed { ($kind:ty) => {{ let mut value = <$kind>::new(item.base.uuid()).expect("known inventory constructor"); value.base = item; Box::new(value) as Box }}; } match item.inventory_type() { InventoryType::TEXTURE => boxed!(InventoryTexture), InventoryType::SOUND => boxed!(InventorySound), InventoryType::CALLING_CARD => boxed!(InventoryCallingCard), InventoryType::LANDMARK => boxed!(InventoryLandmark), InventoryType::OBJECT => boxed!(InventoryObject), InventoryType::NOTECARD => boxed!(InventoryNotecard), InventoryType::CATEGORY => boxed!(InventoryCategory), InventoryType::LSL => boxed!(InventoryLSL), InventoryType::SNAPSHOT => boxed!(InventorySnapshot), InventoryType::ATTACHMENT => boxed!(InventoryAttachment), InventoryType::WEARABLE => boxed!(InventoryWearable), InventoryType::ANIMATION => boxed!(InventoryAnimation), InventoryType::GESTURE => boxed!(InventoryGesture), InventoryType::SETTINGS => boxed!(InventorySettings), InventoryType::MATERIAL => boxed!(InventoryMaterial), _ => Box::new(item), } } /// C# `FindObjectByPathReplyEventArgs`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FindObjectByPathReplyEventArgs { path: String, inventory_object_id: UUID, } impl FindObjectByPathReplyEventArgs { pub fn new(path: String, inventory_object_id: UUID) -> Result { Ok(Self { path, inventory_object_id, }) } pub fn inventory_object_id(&self) -> UUID { self.inventory_object_id } pub fn path(&self) -> String { self.path.clone() } } /// C# `FolderUpdatedEventArgs`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct FolderUpdatedEventArgs { folder_id: UUID, success: bool, } impl FolderUpdatedEventArgs { pub fn new(folder_id: UUID, success: bool) -> Result { Ok(Self { folder_id, success }) } pub fn folder_id(&self) -> UUID { self.folder_id } pub fn success(&self) -> bool { self.success } } /// C# `ItemReceivedEventArgs`. #[derive(Clone, Debug)] pub struct ItemReceivedEventArgs { item: InventoryItem, } impl ItemReceivedEventArgs { pub fn new(item: InventoryItem) -> Result { Ok(Self { item }) } pub fn item(&self) -> InventoryItem { self.item.clone() } } /// C# `SaveAssetToInventoryEventArgs`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SaveAssetToInventoryEventArgs { item_id: UUID, new_asset_id: UUID, } impl SaveAssetToInventoryEventArgs { pub fn new(item_id: UUID, new_asset_id: UUID) -> Result { Ok(Self { item_id, new_asset_id, }) } pub fn item_id(&self) -> UUID { self.item_id } pub fn new_asset_id(&self) -> UUID { self.new_asset_id } } /// C# `ScriptRunningReplyEventArgs`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ScriptRunningReplyEventArgs { object_id: UUID, script_id: UUID, is_mono: bool, is_running: bool, } impl ScriptRunningReplyEventArgs { pub fn new( object_id: UUID, sctript_id: UUID, is_mono: bool, is_running: bool, ) -> Result { Ok(Self { object_id, script_id: sctript_id, is_mono, is_running, }) } pub fn is_mono(&self) -> bool { self.is_mono } pub fn is_running(&self) -> bool { self.is_running } pub fn object_id(&self) -> UUID { self.object_id } pub fn script_id(&self) -> UUID { self.script_id } } /// C# `TaskInventoryReplyEventArgs`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TaskInventoryReplyEventArgs { item_id: UUID, serial: i16, asset_filename: String, } impl TaskInventoryReplyEventArgs { pub fn new(item_id: UUID, serial: i16, asset_filename: String) -> Result { Ok(Self { item_id, serial, asset_filename, }) } pub fn asset_filename(&self) -> String { self.asset_filename.clone() } pub fn item_id(&self) -> UUID { self.item_id } pub fn serial(&self) -> i16 { self.serial } } /// C# `TaskItemReceivedEventArgs`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct TaskItemReceivedEventArgs { item_id: UUID, folder_id: UUID, creator_id: UUID, asset_id: UUID, type_: InventoryType, } impl TaskItemReceivedEventArgs { pub fn new( item_id: UUID, folder_id: UUID, creator_id: UUID, asset_id: UUID, type_: InventoryType, ) -> Result { Ok(Self { item_id, folder_id, creator_id, asset_id, type_, }) } pub fn asset_id(&self) -> UUID { self.asset_id } pub fn creator_id(&self) -> UUID { self.creator_id } pub fn folder_id(&self) -> UUID { self.folder_id } pub fn item_id(&self) -> UUID { self.item_id } pub fn type_(&self) -> InventoryType { self.type_ } } /// C# `InventoryObjectOfferedEventArgs`. #[derive(Clone, Debug)] pub struct InventoryObjectOfferedEventArgs { offer: InstantMessage, asset_type: AssetType, object_id: UUID, from_task: bool, decision: Arc>, } #[derive(Clone, Copy, Debug)] struct InventoryOfferDecision { folder_id: UUID, accept: bool, } impl InventoryObjectOfferedEventArgs { pub fn new( offer_details: InstantMessage, type_: AssetType, object_id: UUID, from_task: bool, folder_id: UUID, ) -> Result { Ok(Self { offer: offer_details, asset_type: type_, object_id, from_task, decision: Arc::new(RwLock::new(InventoryOfferDecision { folder_id, accept: false, })), }) } pub fn accept(&self) -> bool { read(&self.decision).accept } pub fn set_accept(&mut self, value: bool) { write(&self.decision).accept = value; } pub fn asset_type(&self) -> AssetType { self.asset_type } pub fn folder_id(&self) -> UUID { read(&self.decision).folder_id } pub fn set_folder_id(&mut self, value: UUID) { write(&self.decision).folder_id = value; } pub fn from_task(&self) -> bool { self.from_task } pub fn object_id(&self) -> UUID { self.object_id } pub fn offer(&self) -> InstantMessage { self.offer.clone() } } /// Result returned by the C#-compatible multi-item copy APIs. #[derive(Clone, Debug, Default)] pub struct InventoryManagerCopyItemsResult { copied_items: Option>, error: Option, success: bool, } impl InventoryManagerCopyItemsResult { pub fn new() -> Result { Ok(Self::default()) } pub fn copied_items(&self) -> Option> { self.copied_items.clone() } pub fn set_copied_items(&mut self, value: Option>) { self.copied_items = value; } pub fn error(&self) -> Option { self.error.clone() } pub fn set_error(&mut self, value: Option) { self.error = value; } pub fn success(&self) -> bool { self.success } pub fn set_success(&mut self, value: bool) { self.success = value; } } /// Result returned by the C#-compatible folder refresh API. #[derive(Clone, Debug, Default)] pub struct InventoryManagerFolderUpdateResult { contents: Option>, folder_id: UUID, success: bool, } impl InventoryManagerFolderUpdateResult { pub fn new() -> Result { Ok(Self::default()) } pub fn contents(&self) -> Option> { self.contents.clone() } pub fn set_contents(&mut self, value: Option>) { self.contents = value; } pub fn folder_id(&self) -> UUID { self.folder_id } pub fn set_folder_id(&mut self, value: UUID) { self.folder_id = value; } pub fn success(&self) -> bool { self.success } pub fn set_success(&mut self, value: bool) { self.success = value; } } struct PendingCreated { deadline: Instant, sender: oneshot::Sender>, } struct PendingCopy { deadline: Instant, expected: usize, copied: Vec, sender: oneshot::Sender, } struct PendingFetch { request_id: u64, deadline: Instant, sender: oneshot::Sender>, } struct PendingTaskReply { request_id: u64, deadline: Instant, sender: oneshot::Sender, } struct PendingOffer { request_id: u64, deadline: Instant, sender: oneshot::Sender, } struct TaskXfer { deadline: Instant, next_packet: u32, announced_size: Option, bytes: Vec, sender: oneshot::Sender>>, } struct CleanupWorker { stop: Arc<(Mutex, Condvar)>, handle: JoinHandle<()>, } #[derive(Default)] struct InventoryManagerEvents { find_object_by_path_reply: EventRegistry, folder_updated: EventRegistry, inventory_object_offered: EventRegistry, item_received: EventRegistry, save_asset_to_inventory: EventRegistry, script_running_reply: EventRegistry, task_inventory_reply: EventRegistry, task_item_received: EventRegistry, } /// Shared state for a client-owned inventory manager. pub(crate) struct InventoryManagerInner { client: ClientWeakHandle, store: RwLock>, disposed: AtomicBool, callback_position: AtomicU32, request_position: AtomicU64, pending_created: Mutex>, pending_copied: Mutex>, pending_fetches: Mutex>>, pending_task_replies: Mutex>>, pending_offers: Mutex>, task_xfers: Mutex>, cleanup_worker: Mutex>, raw_packet_subscription: Mutex>, caps_subscription: Mutex>, im_subscription: Mutex>, login_callback: Mutex>, events: InventoryManagerEvents, } /// Stateful native replacement for the generated `InventoryManager` shell. #[derive(Clone)] pub struct InventoryManager { inner: Arc, } impl fmt::Debug for InventoryManager { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("InventoryManager") .field("has_store", &read(&self.inner.store).is_some()) .field("disposed", &self.inner.disposed.load(Ordering::Acquire)) .finish() } } impl InventoryManager { pub(crate) fn native_from_inner(inner: Arc) -> Self { Self { inner } } pub(crate) fn native_inner(&self) -> Arc { Arc::clone(&self.inner) } pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; let inner = Arc::new(InventoryManagerInner { client: client.native_weak_handle(), store: RwLock::new(None), disposed: AtomicBool::new(false), callback_position: AtomicU32::new(0), request_position: AtomicU64::new(0), pending_created: Mutex::new(HashMap::new()), pending_copied: Mutex::new(HashMap::new()), pending_fetches: Mutex::new(HashMap::new()), pending_task_replies: Mutex::new(HashMap::new()), pending_offers: Mutex::new(Vec::new()), task_xfers: Mutex::new(HashMap::new()), cleanup_worker: Mutex::new(None), raw_packet_subscription: Mutex::new(None), caps_subscription: Mutex::new(None), im_subscription: Mutex::new(None), login_callback: Mutex::new(None), events: InventoryManagerEvents::default(), }); let manager = Self { inner }; manager.install_packet_handler()?; manager.install_message_handlers()?; manager.install_login_handler()?; manager.install_cleanup_worker()?; Ok(manager) } fn install_cleanup_worker(&self) -> Result<(), Error> { let stop = Arc::new((Mutex::new(false), Condvar::new())); let worker_stop = Arc::clone(&stop); let weak = Arc::downgrade(&self.inner); let handle = std::thread::Builder::new() .name("inventory-callback-cleanup".to_owned()) .spawn(move || { loop { let (lock, wake) = &*worker_stop; let stopped = mutex(lock); let (stopped, _) = wake .wait_timeout_while(stopped, CALLBACK_SWEEP_INTERVAL, |value| !*value) .unwrap_or_else(std::sync::PoisonError::into_inner); if *stopped { break; } drop(stopped); let Some(inner) = weak.upgrade() else { break; }; InventoryManager::native_from_inner(inner).expire_pending(Instant::now()); } }) .map_err(|_| Error::InvalidOperation)?; *mutex(&self.inner.cleanup_worker) = Some(CleanupWorker { stop, handle }); Ok(()) } fn expire_pending(&self, now: Instant) { let created = { let mut pending = mutex(&self.inner.pending_created); let expired: Vec<_> = pending .iter() .filter_map(|(id, request)| (request.deadline <= now).then_some(*id)) .collect(); expired .into_iter() .filter_map(|id| pending.remove(&id)) .collect::>() }; for request in created { let _ = request.sender.send(None); } let copied = { let mut pending = mutex(&self.inner.pending_copied); let expired: Vec<_> = pending .iter() .filter_map(|(id, request)| (request.deadline <= now).then_some(*id)) .collect(); expired .into_iter() .filter_map(|id| pending.remove(&id)) .collect::>() }; for request in copied { let success = !request.copied.is_empty(); let _ = request.sender.send(InventoryManagerCopyItemsResult { copied_items: success.then_some(request.copied), error: None, success, }); } let fetches = expire_waiters(&self.inner.pending_fetches, now, |request| request.deadline); for request in fetches { let _ = request.sender.send(None); } let task_replies = expire_waiters(&self.inner.pending_task_replies, now, |request| { request.deadline }); for request in task_replies { let _ = request.sender.send(String::new()); } mutex(&self.inner.pending_offers).retain(|request| request.deadline > now); let xfers = { let mut pending = mutex(&self.inner.task_xfers); let expired: Vec<_> = pending .iter() .filter_map(|(id, request)| (request.deadline <= now).then_some(*id)) .collect(); expired .into_iter() .filter_map(|id| pending.remove(&id)) .collect::>() }; for request in xfers { let _ = request.sender.send(None); } } fn client(&self) -> Result { if self.inner.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } self.inner.client.upgrade().ok_or(Error::InvalidOperation) } fn install_packet_handler(&self) -> Result<(), Error> { let network = self.client()?.native_network()?; let weak = Arc::downgrade(&self.inner); let subscription = network.subscribe_raw_packet(Arc::new(move |event| { if let Some(inner) = weak.upgrade() { let _ = InventoryManager::native_from_inner(inner).handle_raw_packet(event); } })); *mutex(&self.inner.raw_packet_subscription) = Some(subscription); Ok(()) } fn install_message_handlers(&self) -> Result<(), Error> { let mut client = self.client()?; let weak = Arc::downgrade(&self.inner); let im_subscription = client.native_self().native_subscribe_im(Arc::new( move |event: InstantMessageEventArgs| { if let Some(inner) = weak.upgrade() { let _ = InventoryManager::native_from_inner(inner).handle_inventory_offer(event); } }, )); *mutex(&self.inner.im_subscription) = Some(im_subscription); let weak = Arc::downgrade(&self.inner); let caps_subscription = client.native_network()?.subscribe_caps( "ScriptRunningReply".to_owned(), CapsEventQueueCallback::from_handler(move |_name, message, _simulator| { let Some(inner) = weak.upgrade() else { return; }; let Ok(mut reply) = ScriptRunningReplyMessage::new() else { return; }; if message .serialize() .and_then(|map| reply.deserialize(map)) .is_ok() { inner .events .script_running_reply .emit(ScriptRunningReplyEventArgs { object_id: reply.object_id, script_id: reply.item_id, is_mono: reply.mono, is_running: reply.running, }); } }), ); *mutex(&self.inner.caps_subscription) = Some(caps_subscription); Ok(()) } fn install_login_handler(&self) -> Result<(), Error> { let weak = Arc::downgrade(&self.inner); let callback = NetworkManagerLoginResponseCallback::from_handler( move |success, redirect, _message, _reason, response| { if success && !redirect && let (Some(inner), Some(response)) = (weak.upgrade(), response) { let _ = InventoryManager::native_from_inner(inner) .install_login_inventory(response); } }, ); self.client()? .native_network()? .native_register_login_response_callback( callback.clone(), Some(vec![ "inventory-root".to_owned(), "inventory-skeleton".to_owned(), "inventory-lib-root".to_owned(), "inventory-lib-owner".to_owned(), "inventory-skel-lib".to_owned(), ]), )?; *mutex(&self.inner.login_callback) = Some(callback); Ok(()) } fn install_login_inventory(&self, response: LoginResponseData) -> Result<(), Error> { let client = Arc::new(self.client()?); let mut store = Inventory::new_with_grid_client_uuid(client, response.agent_id())?; let mut root = InventoryFolder::new(response.inventory_root())?; root.base.set_parent_uuid(UUID::zero()); store.set_root_folder(Some(root)); for folder in response.inventory_skeleton() { store.update_node_for(&folder)?; } let mut library = InventoryFolder::new(response.library_root())?; library.base.set_parent_uuid(UUID::zero()); store.set_library_folder(Some(library)); for folder in response.library_skeleton() { store.update_node_for(&folder)?; } *write(&self.inner.store) = Some(store); Ok(()) } fn handle_raw_packet(&self, event: RawPacketReceivedEventArgs) -> Result<(), Error> { match event.packet_type { PacketType::UpdateCreateInventoryItem => { self.handle_update_create(decode_packet::( &event.data, )?)?; } PacketType::BulkUpdateInventory => { self.handle_bulk_update(decode_packet::(&event.data)?)?; } PacketType::FetchInventoryReply => { self.handle_fetch_reply(decode_packet::(&event.data)?)?; } PacketType::InventoryDescendents => { self.handle_descendents(decode_packet::(&event.data)?)?; } PacketType::MoveInventoryItem => { self.handle_move_item(decode_packet::(&event.data)?)?; } PacketType::SaveAssetIntoInventory => { let packet = decode_packet::(&event.data)?; self.inner .events .save_asset_to_inventory .emit(SaveAssetToInventoryEventArgs { item_id: packet.inventory_data.item_id, new_asset_id: packet.inventory_data.new_asset_id, }); } PacketType::ReplyTaskInventory => { let packet = decode_packet::(&event.data)?; let filename = wire_string(packet.inventory_data.filename); let waiters = mutex(&self.inner.pending_task_replies) .remove(&packet.inventory_data.task_id) .unwrap_or_default(); for waiter in waiters { let _ = waiter.sender.send(filename.clone()); } self.inner .events .task_inventory_reply .emit(TaskInventoryReplyEventArgs { item_id: packet.inventory_data.task_id, serial: packet.inventory_data.serial, asset_filename: filename, }); } PacketType::SendXferPacket => self.handle_task_xfer_packet( decode_packet::(&event.data)?, event.simulator, )?, _ => {} } Ok(()) } fn handle_inventory_offer(&self, event: InstantMessageEventArgs) -> Result<(), Error> { let offer = event.im(); let (asset_type, object_id, from_task) = match offer.dialog { InstantMessageDialog::InventoryOffered if offer.binary_bucket.len() == 17 => ( asset_type_from_i8(offer.binary_bucket[0].cast_signed()), UUID::from_bytes(offer.binary_bucket.clone(), 1)?, false, ), InstantMessageDialog::TaskInventoryOffered if offer.binary_bucket.len() == 1 => ( asset_type_from_i8(offer.binary_bucket[0].cast_signed()), UUID::zero(), true, ), InstantMessageDialog::InventoryOffered | InstantMessageDialog::TaskInventoryOffered => { return Ok(()); } _ => return Ok(()), }; let args = InventoryObjectOfferedEventArgs::new( offer.clone(), asset_type, object_id, from_task, self.native_find_folder_for_asset_type(asset_type), )?; self.inner .events .inventory_object_offered .emit(args.clone()); let waiters = std::mem::take(&mut *mutex(&self.inner.pending_offers)); for waiter in waiters { let _ = waiter.sender.send(args.clone()); } let response_dialog = match (offer.dialog, args.accept()) { (InstantMessageDialog::InventoryOffered, true) => { InstantMessageDialog::InventoryAccepted } (InstantMessageDialog::TaskInventoryOffered, true) => { InstantMessageDialog::TaskInventoryAccepted } (InstantMessageDialog::InventoryOffered, false) => { InstantMessageDialog::InventoryDeclined } (InstantMessageDialog::TaskInventoryOffered, false) => { InstantMessageDialog::TaskInventoryDeclined } _ => return Ok(()), }; let bucket = if args.accept() { args.folder_id().get_bytes()? } else { Vec::new() }; let mut client = self.client()?; let agent = client.native_self(); let _ = agent.native_instant_message_full( agent.native_name(), offer.from_agent_id, String::new(), offer.im_session_id, response_dialog, InstantMessageOnline::Online, Vector3::zero(), UUID::zero(), bucket, ); if args.accept() && object_id != UUID::zero() { let _ = self.native_request_fetch_inventory( HashMap::from([(object_id, offer.to_agent_id)]), None, ); } Ok(()) } fn handle_task_xfer_packet( &self, packet: SendXferPacketPacket, simulator: Simulator, ) -> Result<(), Error> { let xfer_id = packet.xfer_id.id; let packet_number = packet.xfer_id.packet & 0x0fff_ffff; let is_last = packet.xfer_id.packet & 0x8000_0000 != 0; let mut complete = None; let confirmation; { let mut transfers = mutex(&self.inner.task_xfers); let Some(transfer) = transfers.get_mut(&xfer_id) else { return Ok(()); }; if packet_number == transfer.next_packet { confirmation = packet_number; let mut incoming = packet.data_packet.data; if packet_number == 0 { let size_bytes: [u8; 4] = incoming .get(..4) .ok_or(Error::InvalidOperation)? .try_into() .map_err(|_| Error::InvalidOperation)?; let size = usize::try_from(u32::from_le_bytes(size_bytes)) .map_err(|_| Error::Argument)?; if size > MAX_TASK_INVENTORY_BYTES { let transfer = transfers.remove(&xfer_id).expect("known xfer"); drop(transfers); let _ = transfer.sender.send(None); return Err(Error::Argument); } transfer.bytes = Vec::with_capacity(size); transfer.announced_size = Some(size); incoming.drain(..4); } let size = transfer.announced_size.ok_or(Error::InvalidOperation)?; if transfer.bytes.len().saturating_add(incoming.len()) > size { let transfer = transfers.remove(&xfer_id).expect("known xfer"); drop(transfers); let _ = transfer.sender.send(None); return Err(Error::InvalidOperation); } transfer.bytes.extend_from_slice(&incoming); transfer.next_packet = transfer.next_packet.saturating_add(1); if is_last { let transfer = transfers.remove(&xfer_id).expect("known xfer"); if transfer.bytes.len() == size { complete = Some((transfer.sender, Some(transfer.bytes))); } else { complete = Some((transfer.sender, None)); } } } else { confirmation = transfer.next_packet.saturating_sub(1); } } let mut confirm = ConfirmXferPacketPacket::new_with_constructor()?; confirm.xfer_id.id = xfer_id; confirm.xfer_id.packet = confirmation; let confirmation_result = self.send_generated(&simulator, PacketType::ConfirmXferPacket, &confirm); if let Some((sender, data)) = complete { let _ = sender.send(data); } confirmation_result } fn store_clone(&self) -> Option { read(&self.inner.store).clone() } pub(crate) fn native_reconcile_ais( &self, folders: Vec, items: Vec, meta: &crate::inventory_ais::AISResponseMeta, ) -> Result<(), Error> { let Some(store) = self.store_clone() else { return Ok(()); }; store.native_apply_ais_batch( folders, items, &meta.items_removed(), &meta.categories_removed(), &meta.broken_links_removed(), &meta.category_version_updates(), ) } #[cfg(test)] pub(crate) fn native_set_store_for_tests(&self, store: Inventory) { *write(&self.inner.store) = Some(store); } fn cache_server_item(&self, item: &InventoryItem) -> Result<(), Error> { if item.base.uuid() == UUID::zero() || item.inventory_type() == InventoryType::FOLDER { return Err(Error::Argument); } if let Some(store) = self.store_clone() { let typed = typed_inventory_item(item.clone()); store.update_node_for(typed.as_ref())?; } Ok(()) } fn complete_created_item(&self, callback_id: u32, item: InventoryItem) { let created = mutex(&self.inner.pending_created).remove(&callback_id); if let Some(created) = created { let _ = created.sender.send(Some(item.clone())); } let mut copied = mutex(&self.inner.pending_copied); let Some(copy) = copied.get_mut(&callback_id) else { return; }; copy.copied.push(item.base.clone()); if copy.copied.len() < copy.expected { return; } let copy = copied .remove(&callback_id) .expect("copy request still present"); drop(copied); let _ = copy.sender.send(InventoryManagerCopyItemsResult { copied_items: Some(copy.copied), error: None, success: true, }); } fn handle_update_create(&self, packet: UpdateCreateInventoryItemPacket) -> Result<(), Error> { for block in packet.inventory_data { if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 { continue; } let mut item = inventory_item_from_wire(InventoryWireData { asset_id: block.asset_id, base_mask: block.base_mask, creation_date: block.creation_date, creator_id: block.creator_id, description: block.description, everyone_mask: block.everyone_mask, flags: block.flags, folder_id: block.folder_id, group_id: block.group_id, group_mask: block.group_mask, group_owned: block.group_owned, inv_type: block.inv_type, item_id: block.item_id, name: block.name, next_owner_mask: block.next_owner_mask, owner_id: block.owner_id, owner_mask: block.owner_mask, sale_price: block.sale_price, sale_type: block.sale_type, type_: block.type_, })?; if item.base.parent_uuid() == UUID::zero() { item.base .set_parent_uuid(self.native_find_folder_for_asset_type(item.asset_type())); self.native_request_update_items(vec![item.clone()], UUID::zero())?; } self.cache_server_item(&item)?; self.complete_created_item(block.callback_id, item.clone()); self.inner .events .task_item_received .emit(TaskItemReceivedEventArgs { item_id: item.base.uuid(), folder_id: item.base.parent_uuid(), creator_id: item.creator_id(), asset_id: item.asset_uuid(), type_: item.inventory_type(), }); } Ok(()) } fn handle_bulk_update(&self, packet: BulkUpdateInventoryPacket) -> Result<(), Error> { let store = self.store_clone(); if let Some(store) = &store { for block in packet.folder_data { if block.folder_id == UUID::zero() { continue; } let mut folder = store .get_value_or_default_with_uuid_a7c63fbe::(block.folder_id)? .unwrap_or(InventoryFolder::new(block.folder_id)?); folder.base.set_name(wire_string(block.name)); folder.base.set_owner_id(packet.agent_data.agent_id); folder.base.set_parent_uuid(block.parent_id); folder.set_preferred_type(folder_type_from_i8(block.type_)); store.update_node_for(&folder)?; } } for block in packet.item_data { if block.item_id == UUID::zero() { continue; } let item = inventory_item_from_wire(InventoryWireData { asset_id: block.asset_id, base_mask: block.base_mask, creation_date: block.creation_date, creator_id: block.creator_id, description: block.description, everyone_mask: block.everyone_mask, flags: block.flags, folder_id: block.folder_id, group_id: block.group_id, group_mask: block.group_mask, group_owned: block.group_owned, inv_type: block.inv_type, item_id: block.item_id, name: block.name, next_owner_mask: block.next_owner_mask, owner_id: block.owner_id, owner_mask: block.owner_mask, sale_price: block.sale_price, sale_type: block.sale_type, type_: block.type_, })?; self.cache_server_item(&item)?; self.complete_created_item(block.callback_id, item); } Ok(()) } fn handle_fetch_reply(&self, packet: FetchInventoryReplyPacket) -> Result<(), Error> { for block in packet.inventory_data { if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 { continue; } let item = inventory_item_from_wire(InventoryWireData { asset_id: block.asset_id, base_mask: block.base_mask, creation_date: block.creation_date, creator_id: block.creator_id, description: block.description, everyone_mask: block.everyone_mask, flags: block.flags, folder_id: block.folder_id, group_id: block.group_id, group_mask: block.group_mask, group_owned: block.group_owned, inv_type: block.inv_type, item_id: block.item_id, name: block.name, next_owner_mask: block.next_owner_mask, owner_id: block.owner_id, owner_mask: block.owner_mask, sale_price: block.sale_price, sale_type: block.sale_type, type_: block.type_, })?; self.cache_server_item(&item)?; let waiters = mutex(&self.inner.pending_fetches) .remove(&item.base.uuid()) .unwrap_or_default(); for waiter in waiters { let _ = waiter.sender.send(Some(item.clone())); } self.inner .events .item_received .emit(ItemReceivedEventArgs { item }); } Ok(()) } fn handle_descendents(&self, packet: InventoryDescendentsPacket) -> Result<(), Error> { let folder_id = packet.agent_data.folder_id; let owner_id = packet.agent_data.owner_id; let Some(store) = self.store_clone() else { return Ok(()); }; let mut parent = store .get_value_or_default_with_uuid_a7c63fbe::(folder_id)? .unwrap_or(InventoryFolder::new(folder_id)?); if packet.agent_data.version < parent.version() { return Ok(()); } for block in packet.folder_data { if block.folder_id == UUID::zero() { continue; } let mut folder = InventoryFolder::new(block.folder_id)?; folder.base.set_parent_uuid(block.parent_id); folder.base.set_owner_id(owner_id); folder.base.set_name(wire_string(block.name)); folder.set_preferred_type(folder_type_from_i8(block.type_)); store.update_node_for(&folder)?; } for block in packet.item_data { if block.item_id == UUID::zero() { continue; } let item = inventory_item_from_wire(InventoryWireData { asset_id: block.asset_id, base_mask: block.base_mask, creation_date: block.creation_date, creator_id: block.creator_id, description: block.description, everyone_mask: block.everyone_mask, flags: block.flags, folder_id: block.folder_id, group_id: block.group_id, group_mask: block.group_mask, group_owned: block.group_owned, inv_type: block.inv_type, item_id: block.item_id, name: block.name, next_owner_mask: block.next_owner_mask, owner_id, owner_mask: block.owner_mask, sale_price: block.sale_price, sale_type: block.sale_type, type_: block.type_, })?; self.cache_server_item(&item)?; } parent.set_version(packet.agent_data.version); parent.set_descendent_count(packet.agent_data.descendents); parent.base.set_owner_id(owner_id); store.update_node_for(&parent)?; self.inner .events .folder_updated .emit(FolderUpdatedEventArgs { folder_id, success: true, }); Ok(()) } fn handle_move_item(&self, packet: MoveInventoryItemPacket) -> Result<(), Error> { let Some(store) = self.store_clone() else { return Ok(()); }; for block in packet.inventory_data { let Some(mut item) = store.native_item_value(block.item_id) else { continue; }; item.base.set_parent_uuid(block.folder_id); let name = wire_string(block.new_name); if !name.is_empty() { item.base.set_name(name); } store.update_node_for(&item)?; } Ok(()) } pub(crate) fn native_subscribe_find_object_by_path_reply( &self, handler: EventHandler, ) -> Subscription { self.inner .events .find_object_by_path_reply .subscribe(handler) } pub(crate) fn native_subscribe_folder_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.events.folder_updated.subscribe(handler) } pub(crate) fn native_subscribe_inventory_object_offered( &self, handler: EventHandler, ) -> Subscription { self.inner .events .inventory_object_offered .subscribe(handler) } pub(crate) fn native_subscribe_item_received( &self, handler: EventHandler, ) -> Subscription { self.inner.events.item_received.subscribe(handler) } pub(crate) fn native_subscribe_save_asset_to_inventory( &self, handler: EventHandler, ) -> Subscription { self.inner.events.save_asset_to_inventory.subscribe(handler) } pub(crate) fn native_subscribe_script_running_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.events.script_running_reply.subscribe(handler) } pub(crate) fn native_subscribe_task_inventory_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.events.task_inventory_reply.subscribe(handler) } pub(crate) fn native_subscribe_task_item_received( &self, handler: EventHandler, ) -> Subscription { self.inner.events.task_item_received.subscribe(handler) } pub(crate) fn native_magic_id() -> UUID { UUID::new_with_string(MAGIC_ID_TEXT.to_owned()).expect("static inventory magic UUID") } pub(crate) fn native_decrypt_shadow_id(shadow_id: UUID) -> Result { Ok(UUID::bitxor(shadow_id, Self::native_magic_id())) } pub(crate) fn native_encrypt_asset_id(asset_id: UUID) -> Result { Self::native_decrypt_shadow_id(asset_id) } pub(crate) fn native_item_crc(item: &InventoryItem) -> Result { let mut crc = 0_u32; for value in [ item.asset_uuid(), item.base.parent_uuid(), item.base.uuid(), item.creator_id(), item.base.owner_id(), item.group_id(), ] { crc = crc.wrapping_add(uuid_crc(value)); } let permissions = item.permissions(); for value in [ permissions.owner_mask.0, permissions.next_owner_mask.0, permissions.everyone_mask.0, permissions.group_mask.0, item.flags(), i32::from(item.inventory_type().0).cast_unsigned(), i32::from(item.asset_type() as i8).cast_unsigned(), unix_seconds_u32_wrapping(item.creation_date()), item.sale_price().cast_unsigned(), u32::from(item.sale_type() as u8).wrapping_mul(0x0707_3096), ] { crc = crc.wrapping_add(value); } Ok(crc) } pub(crate) fn native_create_inventory_item( type_: InventoryType, id: UUID, ) -> Result, Error> { macro_rules! boxed { ($kind:ty) => { Box::new(<$kind>::new(id)?) as Box }; } Ok(match type_ { InventoryType::TEXTURE => boxed!(InventoryTexture), InventoryType::SOUND => boxed!(InventorySound), InventoryType::CALLING_CARD => boxed!(InventoryCallingCard), InventoryType::LANDMARK => boxed!(InventoryLandmark), InventoryType::OBJECT => boxed!(InventoryObject), InventoryType::NOTECARD => boxed!(InventoryNotecard), InventoryType::CATEGORY => boxed!(InventoryCategory), InventoryType::LSL => boxed!(InventoryLSL), InventoryType::SNAPSHOT => boxed!(InventorySnapshot), InventoryType::ATTACHMENT => boxed!(InventoryAttachment), InventoryType::WEARABLE => boxed!(InventoryWearable), InventoryType::ANIMATION => boxed!(InventoryAnimation), InventoryType::GESTURE => boxed!(InventoryGesture), InventoryType::SETTINGS => boxed!(InventorySettings), InventoryType::MATERIAL => boxed!(InventoryMaterial), _ => Box::new(InventoryItem::new_with_inventory_type_uuid(type_, id)?), }) } pub(crate) fn native_store(&self) -> Option { read(&self.inner.store).clone() } pub(crate) fn native_no_results() -> Task> { Task(std::marker::PhantomData) } pub(crate) fn native_create_or_retrieve_inventory_item( &self, inventory_type: InventoryType, item_id: UUID, ) -> Result { if let Some(item) = self .store_clone() .and_then(|store| store.native_item_value(item_id)) { return Ok(item); } InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id) } pub(crate) fn native_create_folder( &self, parent_id: UUID, mut name: String, preferred_type: FolderType, ) -> Result { let store = self.store_clone(); if preferred_type != FolderType::None { let owner_id = self.ids()?.0; if let Some(existing) = store .as_ref() .and_then(|store| store.find_folder_for_type(preferred_type)) .and_then(|folder_id| { store.as_ref().and_then(|store| { store .get_value_or_default_with_uuid_a7c63fbe::(folder_id) .ok() .flatten() }) }) .filter(|folder| folder.base.owner_id() == owner_id) { return Ok(existing.base.uuid()); } } if name.is_empty() { preferred_type .enum_text() .unwrap_or("New Folder") .clone_into(&mut name); } let folder_id = UUID::random()?; let owner_id = self.ids()?.0; let mut folder = InventoryFolder::new(folder_id)?; folder.base.set_parent_uuid(parent_id); folder.base.set_owner_id(owner_id); folder.base.set_name(name.clone()); folder.set_version(1); folder.set_descendent_count(0); folder.set_preferred_type(preferred_type); if let Some(store) = store { store.update_node_for(&folder)?; } let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = CreateInventoryFolderPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.folder_data.folder_id = folder_id; packet.folder_data.parent_id = parent_id; packet.folder_data.type_ = preferred_type as i8; packet.folder_data.name = Utils::string_to_bytes(name)?; self.send_generated(&simulator, PacketType::CreateInventoryFolder, &packet)?; Ok(folder_id) } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_create_item( &self, parent_folder: UUID, name: String, description: String, asset_type: AssetType, asset_transaction_id: UUID, inventory_type: InventoryType, wearable_type: WearableType, next_owner_mask: PermissionMask, cancellation_token: Option, ) -> Result, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; self.ensure_pending_capacity()?; let callback_id = self.next_callback_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_created).insert( callback_id, PendingCreated { deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }, ); let send_result = (|| { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = CreateInventoryItemPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_block.callback_id = callback_id; packet.inventory_block.folder_id = parent_folder; packet.inventory_block.transaction_id = asset_transaction_id; packet.inventory_block.next_owner_mask = next_owner_mask.0; packet.inventory_block.type_ = asset_type as i8; packet.inventory_block.inv_type = inventory_type.0; packet.inventory_block.wearable_type = wearable_type as u8; packet.inventory_block.name = Utils::string_to_bytes(name)?; packet.inventory_block.description = Utils::string_to_bytes(description)?; self.send_generated(&simulator, PacketType::CreateInventoryItem, &packet) })(); if let Err(error) = send_result { mutex(&self.inner.pending_created).remove(&callback_id); return Err(error); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(item), _)) => Ok(item), Either::Left((Err(_), _)) => Ok(None), Either::Right(_) => { mutex(&self.inner.pending_created).remove(&callback_id); Err(Error::Cancelled) } } } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_create_link( &self, folder_id: UUID, item_id: UUID, name: String, description: String, inventory_type: InventoryType, transaction_id: UUID, cancellation_token: Option, ) -> Result, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; if let Ok(simulator) = self.current_simulator(None) && self .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token) .await? .is_some() { let mut created = self .create_links_via_cap( folder_id, vec![(item_id, name, description, inventory_type)], cancellation_token, ) .await?; return Ok(created.pop()); } self.ensure_pending_capacity()?; let callback_id = self.next_callback_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_created).insert( callback_id, PendingCreated { deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }, ); let send_result = (|| { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = LinkInventoryItemPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_block.callback_id = callback_id; packet.inventory_block.folder_id = folder_id; packet.inventory_block.transaction_id = transaction_id; packet.inventory_block.old_item_id = item_id; packet.inventory_block.type_ = if inventory_type == InventoryType::FOLDER { AssetType::LinkFolder as i8 } else { AssetType::Link as i8 }; packet.inventory_block.inv_type = inventory_type.0; packet.inventory_block.name = Utils::string_to_bytes(name)?; packet.inventory_block.description = Utils::string_to_bytes(description)?; self.send_generated(&simulator, PacketType::LinkInventoryItem, &packet) })(); if let Err(error) = send_result { mutex(&self.inner.pending_created).remove(&callback_id); return Err(error); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(item), _)) => Ok(item), Either::Left((Err(_), _)) => Ok(None), Either::Right(_) => { mutex(&self.inner.pending_created).remove(&callback_id); Err(Error::Cancelled) } } } pub(crate) async fn native_create_links( &self, folder_id: UUID, links: Box>, callback: Option>, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let links: Vec<_> = links.collect(); if links.is_empty() { if let Some(callback) = callback { callback(true); } return Ok(()); } let cap = if let Ok(simulator) = self.current_simulator(None) { self.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token) .await? } else { None }; if cap.is_some() { let values = links .into_iter() .map(|(item, description)| { ( item.base.uuid(), item.base.name(), description, item.inventory_type(), ) }) .collect(); let success = self .create_links_via_cap(folder_id, values, cancellation_token) .await .is_ok(); if let Some(callback) = callback { callback(success); } return Ok(()); } let mut success = true; for (item, description) in links { cancellation_token.throw_if_cancellation_requested()?; if self .native_create_link( folder_id, item.base.uuid(), item.base.name(), description, item.inventory_type(), UUID::random()?, Some(cancellation_token.clone()), ) .await? .is_none() { success = false; } } if let Some(callback) = callback { callback(success); } Ok(()) } async fn create_links_via_cap( &self, folder_id: UUID, links: Vec<(UUID, String, String, InventoryType)>, cancellation_token: CancellationToken, ) -> Result, Error> { let simulator = self.current_simulator(None)?; let cap = self .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token) .await? .ok_or(Error::InvalidOperation)?; let uri = Uri(format!( "{}/category/{}?tid={}", cap.0.trim_end_matches('/'), folder_id, UUID::random()? )); let payload = OSD::Map(HashMap::from([( "links".to_owned(), OSD::Array( links .into_iter() .map(|(linked_id, name, description, inventory_type)| { OSD::Map(HashMap::from([ ("linked_id".to_owned(), OSD::UUID(linked_id)), ("name".to_owned(), OSD::String(name)), ("desc".to_owned(), OSD::String(description)), ( "type".to_owned(), OSD::Integer(if inventory_type == InventoryType::FOLDER { AssetType::LinkFolder as i32 } else { AssetType::Link as i32 }), ), ( "inv_type".to_owned(), OSD::Integer(i32::from(inventory_type.0)), ), ])) }) .collect(), ), )])); let bytes = OSDParser::serialize_llsd_xml_bytes(payload)?; let (response, body) = self .client()? .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".to_owned(), bytes, cancellation_token, None, ) .await?; if !response.is_success_status_code() { return Err(Error::HttpRequest); } let mut created = Vec::new(); if let Ok(OSD::Map(root)) = OSDParser::deserialize_with_bytes(body) && let Some(OSD::Map(embedded)) = root.get("_embedded") && let Some(OSD::Map(values)) = embedded.get("links") { for value in values.values() { if let Ok(item) = InventoryItem::from_osd(value.clone()) { self.cache_server_item(&item)?; self.inner .events .item_received .emit(ItemReceivedEventArgs { item: item.clone() }); created.push(item); } } } Ok(created) } pub(crate) fn native_request_fetch_inventory( &self, items: HashMap, cancellation_token: Option, ) -> Result<(), Error> { cancellation_token .unwrap_or_default() .throw_if_cancellation_requested()?; let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = FetchInventoryPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_data = items .into_iter() .map(|(item_id, owner_id)| { let mut block = FetchInventoryPacketInventoryDataBlock::new_with_constructor()?; block.item_id = item_id; block.owner_id = owner_id; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::FetchInventory, &packet) } pub(crate) async fn native_request_fetch_inventory_async( &self, items: HashMap, cancellation_token: Option, callback: Option) + Send + Sync>>, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let capability = if let Ok(simulator) = self.current_simulator(None) { self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token) .await? } else { None }; if let Some(capability) = capability { let fetched = self .fetch_inventory_via_cap(capability, items, cancellation_token) .await?; if let Some(callback) = callback { callback(fetched); } return Ok(()); } self.native_request_fetch_inventory(items, Some(cancellation_token)) } async fn fetch_inventory_via_cap( &self, capability: Uri, items: HashMap, cancellation_token: CancellationToken, ) -> Result, Error> { let agent_id = self.ids()?.0; let item_values = items .into_iter() .map(|(item_id, owner_id)| { OSD::Map(HashMap::from([ ("item_id".to_owned(), OSD::UUID(item_id)), ("owner_id".to_owned(), OSD::UUID(owner_id)), ])) }) .collect(); let payload = OSD::Map(HashMap::from([ ("agent_id".to_owned(), OSD::UUID(agent_id)), ("items".to_owned(), OSD::Array(item_values)), ])); let body = OSDParser::serialize_llsd_xml_bytes(payload)?; let (response, response_body) = self .client()? .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( capability, "application/llsd+xml".to_owned(), body, cancellation_token, None, ) .await?; if !response.is_success_status_code() { return Err(Error::HttpRequest); } let OSD::Map(map) = OSDParser::deserialize_with_bytes(response_body)? else { return Err(Error::Argument); }; let Some(OSD::Array(values)) = map.get("items") else { return Ok(Vec::new()); }; let mut fetched = Vec::with_capacity(values.len()); for value in values { let item = InventoryItem::from_osd(value.clone())?; if item.base.uuid() == UUID::zero() { continue; } self.cache_server_item(&item)?; let waiters = mutex(&self.inner.pending_fetches) .remove(&item.base.uuid()) .unwrap_or_default(); for waiter in waiters { let _ = waiter.sender.send(Some(item.clone())); } self.inner .events .item_received .emit(ItemReceivedEventArgs { item: item.clone() }); fetched.push(item); } Ok(fetched) } pub(crate) async fn native_fetch_item( &self, item_id: UUID, owner_id: UUID, cancellation_token: Option, force_http: bool, ) -> Result, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); if cancellation_token.is_cancellation_requested() { return Ok(None); } self.ensure_pending_capacity()?; let request_id = self.next_request_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_fetches) .entry(item_id) .or_default() .push(PendingFetch { request_id, deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }); let request = HashMap::from([(item_id, owner_id)]); let send_result = if force_http { let capability = if let Ok(simulator) = self.current_simulator(None) { self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token) .await? } else { None }; if let Some(capability) = capability { self.fetch_inventory_via_cap(capability, request, cancellation_token.clone()) .await .map(|_| ()) } else { self.remove_pending_fetch(item_id, request_id); return Ok(None); } } else { self.native_request_fetch_inventory_async( request, Some(cancellation_token.clone()), None, ) .await }; if let Err(error) = send_result { self.remove_pending_fetch(item_id, request_id); return Err(error); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(item), _)) => Ok(item), Either::Left((Err(_), _)) | Either::Right(_) => { self.remove_pending_fetch(item_id, request_id); Ok(None) } } } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_request_folder_contents( &self, folder_id: UUID, owner_id: UUID, fetch_folders: bool, fetch_items: bool, order: InventorySortOrder, cancellation_token: Option, ) -> Result, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let cap_name = if owner_id == self.ids()?.0 { "FetchInventoryDescendents2" } else { "FetchLibDescendents2" }; let capability = if let Ok(simulator) = self.current_simulator(None) { self.wait_for_capability(&simulator, cap_name, &cancellation_token) .await? } else { None }; let Some(capability) = capability else { self.inner .events .folder_updated .emit(FolderUpdatedEventArgs { folder_id, success: false, }); return Ok(Vec::new()); }; let mut folder = InventoryFolder::new(folder_id)?; folder.base.set_owner_id(owner_id); self.native_request_folder_contents_batch( vec![folder], capability, fetch_folders, fetch_items, order, Some(cancellation_token), ) .await } pub(crate) async fn native_request_folder_contents_batch( &self, batch: Vec, capability: Uri, fetch_folders: bool, fetch_items: bool, order: InventorySortOrder, cancellation_token: Option, ) -> Result, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let requested = batch .iter() .map(|folder| { OSD::Map(HashMap::from([ ("folder_id".to_owned(), OSD::UUID(folder.base.uuid())), ("owner_id".to_owned(), OSD::UUID(folder.base.owner_id())), ("fetch_folders".to_owned(), OSD::Boolean(fetch_folders)), ("fetch_items".to_owned(), OSD::Boolean(fetch_items)), ("sort_order".to_owned(), OSD::Integer(order.0)), ])) }) .collect(); let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([( "folders".to_owned(), OSD::Array(requested), )])))?; let response = self .client()? .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( capability, "application/llsd+xml".to_owned(), body, cancellation_token, None, ) .await; let (response, response_body) = match response { Ok(response) if response.0.is_success_status_code() => response, Ok(_) | Err(_) => { for folder in batch { self.inner .events .folder_updated .emit(FolderUpdatedEventArgs { folder_id: folder.base.uuid(), success: false, }); } return Ok(Vec::new()); } }; let _ = response; let OSD::Map(root) = OSDParser::deserialize_with_bytes(response_body)? else { return Err(Error::Argument); }; let Some(OSD::Array(folders)) = root.get("folders") else { return Ok(Vec::new()); }; let store = self.store_clone(); let mut result = Vec::new(); for value in folders { let OSD::Map(map) = value else { continue; }; let folder_id = map.get("folder_id").ok_or(Error::Argument)?.as_uuid()?; let owner_id = map.get("owner_id").map_or(Ok(UUID::zero()), OSD::as_uuid)?; let mut parent = store .as_ref() .and_then(|store| { store .get_value_or_default_with_uuid_a7c63fbe::(folder_id) .ok() .flatten() }) .unwrap_or(InventoryFolder::new(folder_id)?); parent.base.set_owner_id(owner_id); if let Some(value) = map.get("version") { parent.set_version(value.as_integer()?); } if let Some(value) = map.get("descendents") { parent.set_descendent_count(value.as_integer()?); } if let Some(store) = &store { store.update_node_for(&parent)?; } if let Some(OSD::Array(categories)) = map.get("categories") { for value in categories { let folder = InventoryFolder::from_osd(value.clone())?; if let Some(store) = &store { store.update_node_for(&folder)?; } result.push(folder.base.clone()); } } if let Some(OSD::Array(items)) = map.get("items") { for value in items { let item = InventoryItem::from_osd(value.clone())?; self.cache_server_item(&item)?; result.push(item.base.clone()); } } self.inner .events .folder_updated .emit(FolderUpdatedEventArgs { folder_id, success: true, }); } Ok(result) } pub(crate) async fn native_request_folder_update( &self, folder_id: UUID, owner_id: UUID, fetch_folders: bool, fetch_items: bool, order: InventorySortOrder, cancellation_token: Option, ) -> Result { let cancellation_token = cancellation_token.unwrap_or_default(); let cap_name = if owner_id == self.ids()?.0 { "FetchInventoryDescendents2" } else { "FetchLibDescendents2" }; let has_capability = if let Ok(simulator) = self.current_simulator(None) { self.wait_for_capability(&simulator, cap_name, &cancellation_token) .await? .is_some() } else { false }; let contents = self .native_request_folder_contents( folder_id, owner_id, fetch_folders, fetch_items, order, Some(cancellation_token), ) .await?; Ok(InventoryManagerFolderUpdateResult { contents: Some(contents), folder_id, success: has_capability, }) } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_folder_contents( &self, folder_id: UUID, owner_id: UUID, fetch_folders: bool, fetch_items: bool, order: InventorySortOrder, cancellation_token: Option, follow_links: bool, ) -> Result>, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); let remote = self .native_request_folder_contents( folder_id, owner_id, fetch_folders, fetch_items, order, Some(cancellation_token.clone()), ) .await; if matches!(remote, Err(Error::Cancelled)) { return remote.map(|_| Vec::new()); } let store = self.store_clone(); let mut contents = store .as_ref() .and_then(|store| store.get_contents_sorted_with_uuid(folder_id, order).ok()) .unwrap_or_default(); if contents.is_empty() && let Ok(values) = remote { contents = values .into_iter() .map(|value| Box::new(value) as Box) .collect(); } if follow_links { for entry in &mut contents { let Some(item) = store .as_ref() .and_then(|store| store.native_item_value(entry.inventory_base().uuid())) else { continue; }; if !item.is_link()? { continue; } if let Some(real) = store .as_ref() .and_then(|store| store.native_item_value(item.asset_uuid())) { *entry = typed_inventory_item(real); } else if let Some(real) = self .native_fetch_item( item.asset_uuid(), owner_id, Some(cancellation_token.clone()), false, ) .await? { *entry = typed_inventory_item(real); } } } Ok(contents) } pub(crate) fn native_find_folder_for_type(&self, folder_type: FolderType) -> UUID { let Some(store) = self.store_clone() else { return UUID::zero(); }; store.find_folder_for_type(folder_type).unwrap_or_else(|| { store .root_folder() .map_or_else(UUID::zero, |folder| folder.base.uuid()) }) } pub(crate) fn native_find_folder_for_asset_type(&self, asset_type: AssetType) -> UUID { let Some(folder_type) = folder_type_from_i8_checked(asset_type as i8) else { return self .store_clone() .and_then(|store| store.root_folder()) .map_or_else(UUID::zero, |folder| folder.base.uuid()); }; self.native_find_folder_for_type(folder_type) } pub(crate) fn native_local_find( &self, base_folder: UUID, path: Vec, level: i32, first_only: bool, ) -> Result, Error> { if level < 0 { return Ok(Vec::new()); } let Some(store) = self.store_clone() else { return Ok(Vec::new()); }; let mut visited = HashSet::new(); local_find_in_store( &store, base_folder, &path, usize::try_from(level).map_err(|_| Error::Argument)?, first_only, &mut visited, 0, ) } pub(crate) async fn native_find_object_by_path( &self, base_folder: UUID, owner_id: UUID, path: String, cancellation_token: Option, ) -> Result { if path.is_empty() { return Err(Error::Argument); } let cancellation_token = cancellation_token.unwrap_or_default(); let segments: Vec<_> = path.split('/').map(ToOwned::to_owned).collect(); let mut folder = base_folder; for (index, segment) in segments.iter().enumerate() { cancellation_token.throw_if_cancellation_requested()?; let contents = self .native_folder_contents( folder, owner_id, true, true, InventorySortOrder::BY_NAME, Some(cancellation_token.clone()), false, ) .await?; let Some(found) = contents .into_iter() .find(|entry| entry.inventory_base().name() == *segment) else { self.inner .events .find_object_by_path_reply .emit(FindObjectByPathReplyEventArgs { path, inventory_object_id: UUID::zero(), }); return Ok(UUID::zero()); }; folder = found.inventory_base().uuid(); if index + 1 == segments.len() { self.inner .events .find_object_by_path_reply .emit(FindObjectByPathReplyEventArgs { path, inventory_object_id: folder, }); return Ok(folder); } } self.inner .events .find_object_by_path_reply .emit(FindObjectByPathReplyEventArgs { path, inventory_object_id: UUID::zero(), }); Ok(UUID::zero()) } pub(crate) fn native_update_folder_properties( &self, folder_id: UUID, parent_id: UUID, name: String, folder_type: FolderType, ) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = UpdateInventoryFolderPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; let mut block = UpdateInventoryFolderPacketFolderDataBlock::new_with_constructor()?; block.folder_id = folder_id; block.parent_id = parent_id; block.name = Utils::string_to_bytes(name.clone())?; block.type_ = folder_type as i8; packet.folder_data = vec![block]; self.send_generated(&simulator, PacketType::UpdateInventoryFolder, &packet)?; if let Some(store) = self.store_clone() && let Some(mut folder) = store.get_value_or_default_with_uuid_a7c63fbe::(folder_id)? { folder.base.set_parent_uuid(parent_id); folder.base.set_name(name); folder.set_preferred_type(folder_type); store.update_node_for(&folder)?; } Ok(()) } pub(crate) fn native_move_folders( &self, moves: HashMap, cancellation_token: Option, ) -> Result<(), Error> { cancellation_token .unwrap_or_default() .throw_if_cancellation_requested()?; self.apply_local_folder_moves(&moves)?; self.send_move_folders_packet(&moves) } fn apply_local_folder_moves(&self, moves: &HashMap) -> Result<(), Error> { if let Some(store) = self.store_clone() { for (folder_id, parent_id) in moves { if let Some(mut folder) = store.get_value_or_default_with_uuid_a7c63fbe::(*folder_id)? { folder.base.set_parent_uuid(*parent_id); store.update_node_for(&folder)?; } } } Ok(()) } fn send_move_folders_packet(&self, moves: &HashMap) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = MoveInventoryFolderPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_data.stamp = false; packet.inventory_data = moves .iter() .map(|(folder_id, parent_id)| { let mut block = MoveInventoryFolderPacketInventoryDataBlock::new_with_constructor()?; block.folder_id = *folder_id; block.parent_id = *parent_id; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::MoveInventoryFolder, &packet) } pub(crate) fn native_move_items( &self, moves: HashMap, cancellation_token: Option, ) -> Result<(), Error> { cancellation_token .unwrap_or_default() .throw_if_cancellation_requested()?; self.apply_local_item_moves(&moves)?; self.send_move_items_packet(&moves) } fn apply_local_item_moves(&self, moves: &HashMap) -> Result<(), Error> { if let Some(store) = self.store_clone() { for (item_id, (folder_id, new_name)) in moves { if let Some(mut item) = store.native_item_value(*item_id) { item.base.set_parent_uuid(*folder_id); if !new_name.is_empty() { item.base.set_name(new_name.clone()); } store.update_node_for(&item)?; } } } Ok(()) } fn send_move_items_packet(&self, moves: &HashMap) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = MoveInventoryItemPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_data.stamp = false; packet.inventory_data = moves .iter() .map(|(item_id, (folder_id, name))| { let mut block = MoveInventoryItemPacketInventoryDataBlock::new_with_constructor()?; block.item_id = *item_id; block.folder_id = *folder_id; block.new_name = Utils::string_to_bytes(name.clone())?; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::MoveInventoryItem, &packet) } pub(crate) async fn native_move_folders_async( &self, moves: HashMap, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; self.apply_local_folder_moves(&moves)?; if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? { for (folder_id, parent_id) in moves { cancellation_token.throw_if_cancellation_requested()?; let _ = self .ais_patch_parent( &capability, "category", folder_id, parent_id, cancellation_token.clone(), ) .await; } return Ok(()); } self.send_move_folders_packet(&moves) } pub(crate) async fn native_move_items_async( &self, moves: HashMap, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; self.apply_local_item_moves(&moves)?; let can_use_ais = moves.values().all(|(_, name)| name.is_empty()); if can_use_ais && let Some(capability) = self.inventory_api_capability(&cancellation_token).await? { for (item_id, (parent_id, _)) in moves { cancellation_token.throw_if_cancellation_requested()?; let _ = self .ais_patch_parent( &capability, "item", item_id, parent_id, cancellation_token.clone(), ) .await; } return Ok(()); } self.send_move_items_packet(&moves) } async fn inventory_api_capability( &self, cancellation_token: &CancellationToken, ) -> Result, Error> { let Ok(simulator) = self.current_simulator(None) else { return Ok(None); }; self.wait_for_capability(&simulator, "InventoryAPIv3", cancellation_token) .await } async fn ais_patch_parent( &self, capability: &Uri, kind: &str, object_id: UUID, parent_id: UUID, cancellation_token: CancellationToken, ) -> Result { let uri = Uri(format!( "{}/{kind}/{object_id}", capability.0.trim_end_matches('/') )); let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([( "parent_id".to_owned(), OSD::UUID(parent_id), )])))?; let response = self .client()? .native_http_caps_client() .patch_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".to_owned(), body, cancellation_token, None, ) .await?; Ok(response.0.is_success_status_code()) } pub(crate) fn native_move_object( &self, item: InventoryBase, new_parent: InventoryFolder, ) -> Result<(), Error> { if self .store_clone() .and_then(|store| { store .get_value_or_default_with_uuid_a7c63fbe::(item.uuid()) .ok() .flatten() }) .is_some() { self.native_move_folders(HashMap::from([(item.uuid(), new_parent.base.uuid())]), None) } else { self.native_move_items( HashMap::from([(item.uuid(), (new_parent.base.uuid(), String::new()))]), None, ) } } pub(crate) fn native_request_update_items( &self, items: Vec, transaction_id: UUID, ) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = UpdateInventoryItemPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_data.transaction_id = transaction_id; packet.inventory_data = items .iter() .map(|item| { let mut block = UpdateInventoryItemPacketInventoryDataBlock::new_with_constructor()?; fill_inventory_wire_block!( block, item, item.transaction_id(), item.asset_type() as i8 ); block.callback_id = 0; block.crc = Self::native_item_crc(item)?; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::UpdateInventoryItem, &packet) } pub(crate) async fn native_remove_descendants( &self, folder_id: UUID, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? { if self .ais_delete_object( &capability, &format!("category/{folder_id}/children"), cancellation_token, ) .await .unwrap_or(false) { self.remove_local_descendants(folder_id, true)?; } return Ok(()); } let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = PurgeInventoryDescendentsPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_data.folder_id = folder_id; self.send_generated(&simulator, PacketType::PurgeInventoryDescendents, &packet)?; self.remove_local_descendants(folder_id, true) } pub(crate) async fn native_remove_inventory_objects( &self, items: Vec, folders: Vec, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? { for item in items { cancellation_token.throw_if_cancellation_requested()?; if self .ais_delete_object( &capability, &format!("item/{item}"), cancellation_token.clone(), ) .await .unwrap_or(false) { self.remove_local_descendants(item, true)?; } } for folder in folders { cancellation_token.throw_if_cancellation_requested()?; if self .ais_delete_object( &capability, &format!("category/{folder}"), cancellation_token.clone(), ) .await .unwrap_or(false) { self.remove_local_descendants(folder, true)?; } } return Ok(()); } let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.item_data = items .iter() .map(|item_id| { let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?; block.item_id = *item_id; Ok(block) }) .collect::, Error>>()?; packet.folder_data = folders .iter() .map(|folder_id| { let mut block = RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?; block.folder_id = *folder_id; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet)?; for item in items { self.remove_local_descendants(item, true)?; } for folder in folders { self.remove_local_descendants(folder, true)?; } Ok(()) } async fn ais_delete_object( &self, capability: &Uri, path: &str, cancellation_token: CancellationToken, ) -> Result { let uri = Uri(format!("{}/{path}", capability.0.trim_end_matches('/'))); let response = self .client()? .native_http_caps_client() .delete_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".to_owned(), Vec::new(), cancellation_token, None, ) .await?; Ok(response.0.is_success_status_code() || response.0.status_code == 410) } fn remove_local_descendants(&self, root: UUID, include_root: bool) -> Result<(), Error> { let Some(store) = self.store_clone() else { return Ok(()); }; if include_root { if let Some(value) = store.get_value_or_default_with_uuid(root)? { store.remove_node_for(&value)?; } return Ok(()); } let contents = store.get_contents_with_uuid(root).unwrap_or_default(); for value in contents { store.remove_node_for(value.as_ref())?; } Ok(()) } pub(crate) fn native_empty_folder(&self, folder_id: UUID) -> Result<(), Error> { let Some(store) = self.store_clone() else { return Ok(()); }; let contents = store.get_contents_with_uuid(folder_id).unwrap_or_default(); let mut items = Vec::new(); let mut folders = Vec::new(); for value in contents { if value.as_any().is::() { folders.push(value.inventory_base().uuid()); } else { items.push(value.inventory_base().uuid()); } } let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.item_data = items .iter() .map(|id| { let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?; block.item_id = *id; Ok(block) }) .collect::, Error>>()?; packet.folder_data = folders .iter() .map(|id| { let mut block = RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?; block.folder_id = *id; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet) } pub(crate) async fn native_empty_system_folder( &self, folder_type: FolderType, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let folder_id = self.native_find_folder_for_type(folder_type); if folder_id != UUID::zero() { self.native_remove_descendants(folder_id, Some(cancellation_token)) .await?; } Ok(()) } fn copy_from_notecard_packet( &self, object_id: UUID, notecard_id: UUID, folder_id: UUID, item_id: UUID, ) -> Result { let (agent_id, session_id) = self.ids()?; let mut packet = CopyInventoryFromNotecardPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.notecard_data.object_id = object_id; packet.notecard_data.notecard_item_id = notecard_id; let mut block = CopyInventoryFromNotecardPacketInventoryDataBlock::new_with_constructor()?; block.folder_id = folder_id; block.item_id = item_id; packet.inventory_data = vec![block]; Ok(packet) } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_copy_item_from_notecard( &self, object_id: UUID, notecard_id: UUID, folder_id: UUID, item_id: UUID, cancellation_token: Option, progress: Option< Box>, >, ) -> Result, Error> { const NOTECARD_CALLBACK_ID: u32 = 0; let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; self.ensure_pending_capacity()?; let (sender, receiver) = oneshot::channel(); { let mut copied = mutex(&self.inner.pending_copied); // This protocol fixes the callback ID at zero. Reject a concurrent // request rather than overwriting the only correlatable waiter. if copied.contains_key(&NOTECARD_CALLBACK_ID) { return Err(Error::InvalidOperation); } copied.insert( NOTECARD_CALLBACK_ID, PendingCopy { deadline: Instant::now() + CALLBACK_TIMEOUT, expected: 1, copied: Vec::with_capacity(1), sender, }, ); } let send_result = async { let simulator = self.current_simulator(None)?; if let Some(capability) = self .wait_for_capability(&simulator, "CopyInventoryFromNotecard", &cancellation_token) .await? { let mut message = CopyInventoryFromNotecardMessage::new()?; message.callback_id = 0; message.folder_id = folder_id; message.item_id = item_id; message.notecard_id = notecard_id; message.object_id = object_id; let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(message.serialize()?.snapshot()))?; match self .client()? .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( capability, "application/llsd+xml".to_owned(), body, cancellation_token.clone(), progress, ) .await { Ok((response, _)) if response.is_success_status_code() => return Ok(()), Err(Error::Cancelled) => return Err(Error::Cancelled), Ok(_) | Err(_) => {} } } cancellation_token.throw_if_cancellation_requested()?; let packet = self.copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id)?; self.send_generated(&simulator, PacketType::CopyInventoryFromNotecard, &packet) } .await; if let Err(error) = send_result { mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID); return Err(error); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(result), _)) => Ok(result .copied_items .and_then(|mut items| (!items.is_empty()).then(|| items.remove(0)))), Either::Left((Err(_), _)) => Ok(None), Either::Right(_) => { mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID); Err(Error::Cancelled) } } } pub(crate) async fn native_copy_items( &self, items: Vec, target_folders: Vec, new_names: Vec, old_owner_id: UUID, cancellation_token: Option, ) -> Result { if items.len() != target_folders.len() || (!new_names.is_empty() && items.len() != new_names.len()) { return Err(Error::Argument); } let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; if items.is_empty() { return Ok(InventoryManagerCopyItemsResult { copied_items: Some(Vec::new()), error: None, success: true, }); } if let Ok(simulator) = self.current_simulator(None) && let Some(capability) = self .wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token) .await? { let operations = items .iter() .zip(&target_folders) .enumerate() .map(|(index, (item_id, folder_id))| { let mut operation = HashMap::from([ ("item_id".to_owned(), OSD::UUID(*item_id)), ("folder_id".to_owned(), OSD::UUID(*folder_id)), ]); if let Some(name) = new_names.get(index) && !name.is_empty() { operation.insert("new_name".to_owned(), OSD::String(name.clone())); } OSD::Map(operation) }) .collect(); let payload = OSD::Map(HashMap::from([ ("items".to_owned(), OSD::Array(operations)), ("agent_id".to_owned(), OSD::UUID(self.ids()?.0)), ])); let body = OSDParser::serialize_llsd_xml_bytes(payload)?; let response = self .client()? .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( capability, "application/llsd+xml".to_owned(), body, cancellation_token.clone(), None, ) .await; if response.is_ok_and(|response| response.0.is_success_status_code()) { return Ok(InventoryManagerCopyItemsResult { copied_items: Some(Vec::new()), error: None, success: true, }); } } self.ensure_pending_capacity()?; let callback_id = self.next_callback_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_copied).insert( callback_id, PendingCopy { deadline: Instant::now() + CALLBACK_TIMEOUT, expected: items.len(), copied: Vec::with_capacity(items.len()), sender, }, ); let send_result = (|| { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = CopyInventoryItemPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_data = items .iter() .zip(&target_folders) .enumerate() .map(|(index, (item_id, folder_id))| { let mut block = CopyInventoryItemPacketInventoryDataBlock::new_with_constructor()?; block.callback_id = callback_id; block.old_item_id = *item_id; block.old_agent_id = old_owner_id; block.new_folder_id = *folder_id; block.new_name = Utils::string_to_bytes(new_names.get(index).cloned().unwrap_or_default())?; Ok(block) }) .collect::, Error>>()?; self.send_generated(&simulator, PacketType::CopyInventoryItem, &packet) })(); if let Err(error) = send_result { mutex(&self.inner.pending_copied).remove(&callback_id); return Ok(InventoryManagerCopyItemsResult { copied_items: None, error: Some(ExternalError(error.to_string())), success: false, }); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(result), _)) => Ok(result), Either::Left((Err(_), _)) => Ok(InventoryManagerCopyItemsResult::default()), Either::Right(_) => { mutex(&self.inner.pending_copied).remove(&callback_id); Err(Error::Cancelled) } } } pub(crate) async fn native_copy_item( &self, item_id: UUID, new_parent: UUID, new_name: String, old_owner_id: UUID, cancellation_token: Option, ) -> Result, Error> { Ok(self .native_copy_items( vec![item_id], vec![new_parent], vec![new_name], old_owner_id, cancellation_token, ) .await? .copied_items .and_then(|mut items| (!items.is_empty()).then(|| items.remove(0)))) } pub(crate) async fn native_copy_item_default_owner( &self, item_id: UUID, new_parent: UUID, new_name: String, cancellation_token: Option, ) -> Result, Error> { self.native_copy_item( item_id, new_parent, new_name, self.ids()?.0, cancellation_token, ) .await } pub(crate) fn native_dispose(&self) -> Result<(), Error> { if self.inner.disposed.swap(true, Ordering::AcqRel) { return Ok(()); } if let Some(worker) = mutex(&self.inner.cleanup_worker).take() { let (stopped, wake) = &*worker.stop; *mutex(stopped) = true; wake.notify_all(); if worker.handle.thread().id() != std::thread::current().id() { let _ = worker.handle.join(); } } mutex(&self.inner.raw_packet_subscription).take(); mutex(&self.inner.caps_subscription).take(); mutex(&self.inner.im_subscription).take(); if let Some(callback) = mutex(&self.inner.login_callback).take() && let Ok(client) = self.inner.client.upgrade().ok_or(Error::InvalidOperation) && let Ok(network) = client.native_network() { let _ = network.native_unregister_login_response_callback(&callback); } let created = mutex(&self.inner.pending_created) .drain() .map(|(_, v)| v) .collect::>(); let copied = mutex(&self.inner.pending_copied) .drain() .map(|(_, v)| v) .collect::>(); let fetches = mutex(&self.inner.pending_fetches) .drain() .flat_map(|(_, v)| v) .collect::>(); let task_replies = mutex(&self.inner.pending_task_replies) .drain() .flat_map(|(_, v)| v) .collect::>(); let offers = std::mem::take(&mut *mutex(&self.inner.pending_offers)); let xfers = mutex(&self.inner.task_xfers) .drain() .map(|(_, v)| v) .collect::>(); for request in created { let _ = request.sender.send(None); } for request in copied { let _ = request .sender .send(InventoryManagerCopyItemsResult::default()); } for request in fetches { let _ = request.sender.send(None); } for request in task_replies { let _ = request.sender.send(String::new()); } drop(offers); for request in xfers { let _ = request.sender.send(None); } write(&self.inner.store).take(); Ok(()) } fn current_simulator(&self, simulator: Option) -> Result { simulator .or_else(|| { self.client() .ok()? .native_network() .ok()? .native_current_sim() }) .ok_or(Error::InvalidOperation) } fn ids(&self) -> Result<(UUID, UUID), Error> { let network = self.client()?.native_network()?; Ok((network.native_agent_id(), network.native_session_id())) } fn send_encoded( simulator: &Simulator, packet_type: PacketType, bytes: Vec, ) -> Result<(), Error> { simulator.native_send_packet_data( bytes.clone(), i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, packet_type, false, ) } #[allow(clippy::unused_self)] // Kept as a manager method to centralize all outbound packet sends. fn send_generated( &self, simulator: &Simulator, packet_type: PacketType, packet: &P, ) -> Result<(), Error> { for bytes in packet.encode_multiple()? { Self::send_encoded(simulator, packet_type, bytes)?; } Ok(()) } fn next_callback_id(&self) -> u32 { loop { let value = self .inner .callback_position .fetch_add(1, Ordering::Relaxed) .wrapping_add(1); if value != 0 { return value; } } } fn next_request_id(&self) -> u64 { loop { let value = self .inner .request_position .fetch_add(1, Ordering::Relaxed) .wrapping_add(1); if value != 0 { return value; } } } fn ensure_pending_capacity(&self) -> Result<(), Error> { let total = mutex(&self.inner.pending_created).len() + mutex(&self.inner.pending_copied).len() + mutex(&self.inner.pending_fetches) .values() .map(Vec::len) .sum::() + mutex(&self.inner.pending_task_replies) .values() .map(Vec::len) .sum::() + mutex(&self.inner.pending_offers).len() + mutex(&self.inner.task_xfers).len(); if total >= MAX_PENDING_REQUESTS { Err(Error::InvalidOperation) } else { Ok(()) } } fn remove_pending_fetch(&self, item_id: UUID, request_id: u64) { let mut pending = mutex(&self.inner.pending_fetches); if let Some(waiters) = pending.get_mut(&item_id) { waiters.retain(|waiter| waiter.request_id != request_id); if waiters.is_empty() { pending.remove(&item_id); } } } fn remove_pending_task_reply(&self, object_id: UUID, request_id: u64) { let mut pending = mutex(&self.inner.pending_task_replies); if let Some(waiters) = pending.get_mut(&object_id) { waiters.retain(|waiter| waiter.request_id != request_id); if waiters.is_empty() { pending.remove(&object_id); } } } pub(crate) fn native_request_task_inventory( &self, object_local_id: u32, simulator: Option, ) -> Result<(), Error> { let simulator = self.current_simulator(simulator)?; let (agent_id, session_id) = self.ids()?; let mut packet = RequestTaskInventoryPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_data.local_id = object_local_id; Self::send_encoded( &simulator, PacketType::RequestTaskInventory, packet.to_bytes_with_method()?, ) } pub(crate) fn native_update_task_inventory( &self, object_local_id: u32, item: InventoryItem, simulator: Option, only_modify_metadata: bool, ) -> Result { let simulator = self.current_simulator(simulator)?; let transaction_id = if only_modify_metadata { UUID::zero() } else { UUID::random()? }; let (agent_id, session_id) = self.ids()?; let mut packet = UpdateTaskInventoryPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.update_data.key = 0; packet.update_data.local_id = object_local_id; fill_inventory_wire_block!( packet.inventory_data, &item, transaction_id, item.asset_type() as i8 ); packet.inventory_data.crc = Self::native_item_crc(&item)?; self.send_generated(&simulator, PacketType::UpdateTaskInventory, &packet)?; Ok(transaction_id) } pub(crate) fn native_move_task_inventory( &self, object_local_id: u32, task_item_id: UUID, inventory_folder_id: UUID, simulator: Simulator, ) -> Result<(), Error> { let (agent_id, session_id) = self.ids()?; let mut packet = MoveTaskInventoryPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_data.folder_id = inventory_folder_id; packet.inventory_data.item_id = task_item_id; packet.inventory_data.local_id = object_local_id; self.send_generated(&simulator, PacketType::MoveTaskInventory, &packet) } pub(crate) fn native_remove_task_inventory( &self, object_local_id: u32, task_item_id: UUID, simulator: Simulator, ) -> Result<(), Error> { let (agent_id, session_id) = self.ids()?; let mut packet = RemoveTaskInventoryPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.inventory_data.item_id = task_item_id; packet.inventory_data.local_id = object_local_id; self.send_generated(&simulator, PacketType::RemoveTaskInventory, &packet) } pub(crate) fn native_copy_script_to_task( &self, object_local_id: u32, item: InventoryItem, enable_script: bool, simulator: Option, ) -> Result { let simulator = self.current_simulator(simulator)?; let transaction_id = UUID::random()?; let (agent_id, session_id) = self.ids()?; let mut packet = RezScriptPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.update_block.object_local_id = object_local_id; packet.update_block.enabled = enable_script; fill_inventory_wire_block!( packet.inventory_block, &item, transaction_id, item.asset_type() as i8 ); packet.inventory_block.crc = Self::native_item_crc(&item)?; self.send_generated(&simulator, PacketType::RezScript, &packet)?; Ok(transaction_id) } pub(crate) fn native_request_get_script_running( &self, object_id: UUID, script_id: UUID, ) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let mut packet = GetScriptRunningPacket::new_with_constructor()?; packet.script.object_id = object_id; packet.script.item_id = script_id; self.send_generated(&simulator, PacketType::GetScriptRunning, &packet) } pub(crate) fn native_request_set_script_running( &self, object_id: UUID, script_id: UUID, running: bool, ) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = SetScriptRunningPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.script.object_id = object_id; packet.script.item_id = script_id; packet.script.running = running; self.send_generated(&simulator, PacketType::SetScriptRunning, &packet) } #[allow(clippy::too_many_arguments)] pub(crate) fn native_rez_from_inventory( &self, simulator: Simulator, task_id: UUID, _rotation: Quaternion, position: Vector3, item: InventoryItem, group_owner: UUID, query_id: UUID, rez_selected: bool, ) -> Result { let (agent_id, session_id) = self.ids()?; let permissions = item.permissions(); let mut packet = RezObjectPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_data.group_id = group_owner; packet.rez_data.from_task_id = task_id; packet.rez_data.bypass_raycast = 1; packet.rez_data.ray_start = position; packet.rez_data.ray_end = position; packet.rez_data.ray_target_id = UUID::zero(); packet.rez_data.ray_end_is_intersection = false; packet.rez_data.rez_selected = rez_selected; packet.rez_data.remove_item = false; packet.rez_data.item_flags = item.flags(); packet.rez_data.group_mask = permissions.group_mask.0; packet.rez_data.everyone_mask = permissions.everyone_mask.0; packet.rez_data.next_owner_mask = permissions.next_owner_mask.0; fill_inventory_wire_block!( packet.inventory_data, &item, query_id, item.inventory_type().0 ); self.send_generated(&simulator, PacketType::RezObject, &packet)?; self.remove_if_no_copy(item.base.uuid())?; Ok(query_id) } pub(crate) fn native_rez_from_inventory_default( &self, simulator: Simulator, rotation: Quaternion, position: Vector3, item: InventoryItem, group_owner: Option, ) -> Result { let group_owner = if let Some(group_owner) = group_owner { group_owner } else { let mut client = self.client()?; client.native_self().native_active_group() }; self.native_rez_from_inventory( simulator, UUID::zero(), rotation, position, item, group_owner, UUID::random()?, true, ) } pub(crate) fn native_restore_rez_from_inventory( &self, simulator: Simulator, item: InventoryItem, query_id: UUID, ) -> Result { let (agent_id, session_id) = self.ids()?; let mut packet = RezRestoreToWorldPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; fill_inventory_wire_block!( packet.inventory_data, &item, query_id, item.inventory_type().0 ); self.send_generated(&simulator, PacketType::RezRestoreToWorld, &packet)?; Ok(query_id) } pub(crate) fn native_de_rez_to_inventory( &self, object_local_id: u32, destination: DeRezDestination, destination_id: UUID, transaction_id: UUID, ) -> Result<(), Error> { let simulator = self.current_simulator(None)?; let (agent_id, session_id) = self.ids()?; let mut packet = DeRezObjectPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.agent_block.group_id = UUID::zero(); packet.agent_block.destination = destination as u8; packet.agent_block.destination_id = destination_id; packet.agent_block.packet_count = 1; packet.agent_block.packet_number = 1; packet.agent_block.transaction_id = transaction_id; let mut block = DeRezObjectPacketObjectDataBlock::new_with_constructor()?; block.object_local_id = object_local_id; packet.object_data = vec![block]; self.send_generated(&simulator, PacketType::DeRezObject, &packet) } pub(crate) fn native_de_rez_to_objects_folder( &self, object_local_id: u32, ) -> Result<(), Error> { self.native_de_rez_to_inventory( object_local_id, DeRezDestination::AgentInventoryTake, self.native_find_folder_for_type(FolderType::Object), UUID::random()?, ) } pub(crate) fn native_give_item( &self, item_id: UUID, item_name: String, asset_type: AssetType, recipient: UUID, do_effect: bool, ) -> Result<(), Error> { let mut bucket = Vec::with_capacity(17); bucket.push(asset_type as u8); bucket.extend_from_slice(&item_id.get_bytes()?); self.send_inventory_offer(recipient, item_name, bucket, do_effect)?; self.remove_if_no_copy(item_id) } pub(crate) async fn native_give_folder( &self, folder_id: UUID, folder_name: String, recipient: UUID, do_effect: bool, cancellation_token: Option, ) -> Result<(), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); let (folders, items) = self .native_collect_inventory_recursive( folder_id, self.ids()?.0, Some(cancellation_token.clone()), ) .await?; if folders.len() + items.len() > Self::MAX_GIVE_ITEMS as usize || items.is_empty() { return Ok(()); } let mut bucket = Vec::with_capacity(17 * (folders.len() + items.len() + 1)); bucket.push(AssetType::Folder as u8); bucket.extend_from_slice(&folder_id.get_bytes()?); for folder in &folders { bucket.push(AssetType::Folder as u8); bucket.extend_from_slice(&folder.base.uuid().get_bytes()?); } for item in &items { bucket.push(item.asset_type() as u8); bucket.extend_from_slice(&item.base.uuid().get_bytes()?); } self.send_inventory_offer(recipient, folder_name, bucket, do_effect)?; for item in items { self.remove_if_no_copy(item.base.uuid())?; } Ok(()) } pub(crate) async fn native_wait_for_next_inventory_offer( &self, cancellation_token: Option, ) -> Result { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; self.ensure_pending_capacity()?; let request_id = self.next_request_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_offers).push(PendingOffer { request_id, deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }); let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(offer), _)) => Ok(offer), Either::Left((Err(_), _)) => Err(Error::InvalidOperation), Either::Right(_) => { mutex(&self.inner.pending_offers) .retain(|request| request.request_id != request_id); Err(Error::Cancelled) } } } fn send_inventory_offer( &self, recipient: UUID, message: String, binary_bucket: Vec, do_effect: bool, ) -> Result<(), Error> { let mut client = self.client()?; let region_id = client .native_network()? .native_current_sim() .map_or_else(UUID::zero, |simulator| simulator.native_id()); let color = client.settings.default_effect_color; let agent = client.native_self(); let agent_id = agent.native_agent_id(); agent.native_instant_message_full( agent.native_name(), recipient, message, UUID::random()?, InstantMessageDialog::InventoryOffered, InstantMessageOnline::Online, Vector3::zero(), region_id, binary_bucket, )?; if do_effect { agent.native_viewer_effect( agent_id, recipient, Vector3d::zero(), color, 1.0, UUID::random()?, crate::EffectType::Beam, None, )?; } Ok(()) } fn remove_if_no_copy(&self, item_id: UUID) -> Result<(), Error> { let Some(store) = self.store_clone() else { return Ok(()); }; let Some(item) = store.native_item_value(item_id) else { return Ok(()); }; if item.permissions().owner_mask.0 & PermissionMask::COPY.0 == 0 { store.remove_node_for(&item)?; } Ok(()) } async fn native_collect_inventory_recursive( &self, root_folder: UUID, owner_id: UUID, cancellation_token: Option, ) -> Result<(Vec, Vec), Error> { let cancellation_token = cancellation_token.unwrap_or_default(); let mut folders = Vec::new(); let mut items = Vec::new(); let mut pending = vec![(root_folder, 0_usize)]; let mut visited = HashSet::new(); let mut traversed = 0_usize; while let Some((folder_id, depth)) = pending.pop() { cancellation_token.throw_if_cancellation_requested()?; if depth > MAX_INVENTORY_RECURSION_DEPTH || traversed >= MAX_INVENTORY_TRAVERSAL || !visited.insert(folder_id) { continue; } let contents = self .native_folder_contents( folder_id, owner_id, true, true, InventorySortOrder::BY_DATE, Some(cancellation_token.clone()), false, ) .await?; for value in contents { traversed = traversed.saturating_add(1); if let Some(folder) = value.as_any().downcast_ref::() { folders.push(folder.clone()); pending.push((folder.base.uuid(), depth + 1)); } else if let Some(store_item) = self .store_clone() .and_then(|store| store.native_item_value(value.inventory_base().uuid())) { items.push(store_item); } else if let Some(item) = value.as_any().downcast_ref::() { items.push(item.clone()); } if traversed >= MAX_INVENTORY_TRAVERSAL { break; } } } Ok((folders, items)) } pub(crate) async fn native_get_inventory_recursive( &self, folder_id: UUID, owner_id: UUID, _folders: Vec, _items: Vec, cancellation_token: Option, ) -> Result<(), Error> { self.native_collect_inventory_recursive(folder_id, owner_id, cancellation_token) .await .map(|_| ()) } async fn wait_for_capability( &self, simulator: &Simulator, name: &str, cancellation_token: &CancellationToken, ) -> Result, Error> { let Some(caps) = simulator.native_caps() else { return Ok(None); }; if let Some(uri) = caps.capability_uri(name.to_owned())? { return Ok(Some(uri)); } if caps.seed_request_finished() { return Ok(None); } let (sender, receiver) = oneshot::channel(); let sender = Arc::new(Mutex::new(Some(sender))); let wake = Arc::clone(&sender); let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| { if let Some(sender) = mutex(&wake).take() { let _ = sender.send(()); } }))); if let Some(uri) = caps.capability_uri(name.to_owned())? { return Ok(Some(uri)); } let cancellation = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancellation); match select(receiver, cancellation).await { Either::Left(_) => caps.capability_uri(name.to_owned()), Either::Right(_) => Err(Error::Cancelled), } } pub(crate) async fn native_get_task_inventory( &self, object_id: UUID, object_local_id: u32, simulator: Option, cancellation_token: Option, ) -> Result>, Error> { let cancellation_token = cancellation_token.unwrap_or_default(); cancellation_token.throw_if_cancellation_requested()?; let simulator = self.current_simulator(simulator)?; if let Some(capability) = self .wait_for_capability(&simulator, "RequestTaskInventory", &cancellation_token) .await? { return self .get_task_inventory_via_cap(object_id, capability, cancellation_token) .await; } // Legacy task inventory is a two-stage correlated operation: first the // simulator supplies a temporary filename, then an Xfer downloads it. self.ensure_pending_capacity()?; let request_id = self.next_request_id(); let (sender, receiver) = oneshot::channel(); mutex(&self.inner.pending_task_replies) .entry(object_id) .or_default() .push(PendingTaskReply { request_id, deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }); // A disconnected fake simulator can reject the enqueue; keep waiting so // cancellation still demonstrates callback teardown deterministically. let _ = self.native_request_task_inventory(object_local_id, Some(simulator.clone())); let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); let filename = match select(receiver, cancelled).await { Either::Left((Ok(filename), _)) if !filename.is_empty() => filename, Either::Left(_) => return Ok(Vec::new()), Either::Right(_) => { self.remove_pending_task_reply(object_id, request_id); return Ok(Vec::new()); } }; let Some(bytes) = self .download_task_inventory_xfer(simulator, filename, cancellation_token.clone()) .await? else { return Ok(Vec::new()); }; if bytes.len() > MAX_TASK_INVENTORY_BYTES { return Ok(Vec::new()); } let task_data = wire_string(bytes); parse_task_inventory_values(&task_data) } async fn download_task_inventory_xfer( &self, simulator: Simulator, filename: String, cancellation_token: CancellationToken, ) -> Result>, Error> { cancellation_token.throw_if_cancellation_requested()?; self.ensure_pending_capacity()?; let mut xfer_id = UUID::random()?.get_u_long()?; { let transfers = mutex(&self.inner.task_xfers); while transfers.contains_key(&xfer_id) { xfer_id = UUID::random()?.get_u_long()?; } } let (sender, receiver) = oneshot::channel(); mutex(&self.inner.task_xfers).insert( xfer_id, TaskXfer { deadline: Instant::now() + CALLBACK_TIMEOUT, next_packet: 0, announced_size: None, bytes: Vec::new(), sender, }, ); let mut request = RequestXferPacket::new_with_constructor()?; request.xfer_id.id = xfer_id; request.xfer_id.filename = Utils::string_to_bytes(filename)?; request.xfer_id.file_path = 4; request.xfer_id.delete_on_completion = true; request.xfer_id.use_big_packets = false; request.xfer_id.v_file_id = UUID::zero(); request.xfer_id.v_file_type = AssetType::Unknown as i16; if let Err(error) = self.send_generated(&simulator, PacketType::RequestXfer, &request) { mutex(&self.inner.task_xfers).remove(&xfer_id); return Err(error); } let cancelled = cancellation_token.cancelled(); futures_util::pin_mut!(receiver, cancelled); match select(receiver, cancelled).await { Either::Left((Ok(data), _)) => Ok(data), Either::Left((Err(_), _)) => Ok(None), Either::Right(_) => { mutex(&self.inner.task_xfers).remove(&xfer_id); let mut abort = AbortXferPacket::new_with_constructor()?; abort.xfer_id.id = xfer_id; let _ = self.send_generated(&simulator, PacketType::AbortXfer, &abort); Ok(None) } } } async fn get_task_inventory_via_cap( &self, object_id: UUID, capability: Uri, cancellation_token: CancellationToken, ) -> Result>, Error> { let mut folder = InventoryFolder::new(object_id)?; folder.base.set_name("Contents".to_owned()); folder.base.set_parent_uuid(UUID::zero()); let mut result: Vec> = vec![Box::new(folder)]; let separator = if capability.0.contains('?') { '&' } else { '?' }; let uri = Uri(format!("{}{separator}task_id={}", capability.0, object_id)); let response = self .client()? .native_http_caps_client() .get(uri, cancellation_token, None) .await; let Ok((response, bytes)) = response else { return Ok(result); }; if !response.is_success_status_code() || bytes.len() > MAX_TASK_INVENTORY_BYTES { return Ok(result); } let Ok(OSD::Map(map)) = OSDParser::deserialize_with_bytes(bytes) else { return Ok(result); }; let Some(OSD::Array(contents)) = map.get("contents") else { return Ok(result); }; for value in contents.iter().take(MAX_TASK_INVENTORY_ENTRIES) { let OSD::Map(item_map) = value else { continue; }; if !item_map.contains_key("item_id") { continue; } if let Ok(item) = InventoryItem::from_osd(OSD::Map(item_map.clone())) { result.push(Box::new(item)); } } Ok(result) } pub(crate) fn native_parse_task_inventory( task_data: String, ) -> Result, Error> { if task_data.len() > MAX_TASK_INVENTORY_BYTES { return Err(Error::Argument); } let parsed = parse_task_inventory_values(&task_data)?; Ok(parsed .into_iter() .map(|value| value.inventory_base().clone()) .collect()) } } fn local_find_in_store( store: &Inventory, base_folder: UUID, path: &[String], level: usize, first_only: bool, visited: &mut HashSet, depth: usize, ) -> Result, Error> { if path.is_empty() || level >= path.len() || depth > MAX_INVENTORY_RECURSION_DEPTH || visited.len() >= MAX_INVENTORY_TRAVERSAL || !visited.insert(base_folder) { return Ok(Vec::new()); } let mut result = Vec::new(); for value in store .get_contents_with_uuid(base_folder) .unwrap_or_default() { if value.inventory_base().name() != path[level] { continue; } if level + 1 == path.len() { result.push(value.inventory_base().clone()); if first_only { break; } } else if value.as_any().is::() { result.extend(local_find_in_store( store, value.inventory_base().uuid(), path, level + 1, first_only, visited, depth + 1, )?); if first_only && !result.is_empty() { break; } } } Ok(result) } fn parse_line(line: &str) -> Option<(&str, Option<&str>)> { let line = line.trim(); if line.is_empty() { return None; } let mut parts = line.splitn(2, char::is_whitespace); let key = parts.next()?; let value = parts .next() .map(str::trim) .filter(|value| !value.is_empty()); Some((key, value)) } fn parse_uuid(value: Option<&str>) -> UUID { value .and_then(|value| UUID::new_with_string(value.to_owned()).ok()) .unwrap_or_else(UUID::zero) } fn pipe_string(value: Option<&str>) -> String { value .unwrap_or_default() .split('|') .next() .unwrap_or_default() .to_owned() } fn parse_hex(value: Option<&str>) -> u32 { value .and_then(|value| value.strip_prefix("0x").or(Some(value))) .and_then(|value| u32::from_str_radix(value, 16).ok()) .unwrap_or_default() } fn parse_task_inventory_values( task_data: &str, ) -> Result>, Error> { let normalized = task_data.replace("\r\n", "\n"); let lines: Vec<_> = normalized.lines().collect(); let mut line = 0_usize; let mut result: Vec> = Vec::new(); while line < lines.len() && result.len() < MAX_TASK_INVENTORY_ENTRIES { let Some((kind, _)) = parse_line(lines[line]) else { line += 1; continue; }; line += 1; if kind == "inv_object" { let mut item_id = UUID::zero(); let mut parent_id = UUID::zero(); let mut name = String::new(); let mut asset_type = AssetType::Unknown; while line < lines.len() { let current = parse_line(lines[line]); line += 1; let Some((key, value)) = current else { continue; }; if key == "{" { continue; } if key == "}" { break; } match key { "obj_id" => item_id = parse_uuid(value), "parent_id" => parent_id = parse_uuid(value), "type" => { asset_type = Utils::string_to_asset_type(value.unwrap_or_default().to_owned()) .unwrap_or(AssetType::Unknown); } "name" => name = pipe_string(value), _ => {} } } if asset_type == AssetType::Folder { let mut folder = InventoryFolder::new(item_id)?; folder.base.set_parent_uuid(parent_id); folder.base.set_name(name); result.push(Box::new(folder)); } else { let mut item = InventoryItem::new_with_uuid(item_id)?; item.base.set_parent_uuid(parent_id); item.base.set_name(name); item.set_asset_type(asset_type); result.push(Box::new(item)); } } else if kind == "inv_item" { let mut item_id = UUID::zero(); let mut asset_id = UUID::zero(); let mut parent_id = UUID::zero(); let mut creator_id = UUID::zero(); let mut owner_id = UUID::zero(); let mut last_owner_id = UUID::zero(); let mut group_id = UUID::zero(); let mut group_owned = false; let mut name = String::new(); let mut description = String::new(); let mut asset_type = AssetType::Unknown; let mut inventory_type = InventoryType::UNKNOWN; let mut creation_date = UNIX_EPOCH; let mut flags = 0_u32; let mut permissions = Permissions::no_permissions(); let mut sale_type = SaleType::Not; let mut sale_price = 0_i32; while line < lines.len() { let current = parse_line(lines[line]); line += 1; let Some((key, value)) = current else { continue; }; if key == "{" { continue; } if key == "}" { break; } if key == "permissions" { while line < lines.len() { let current = parse_line(lines[line]); line += 1; let Some((key, value)) = current else { continue; }; if key == "{" { continue; } if key == "}" { break; } match key { "creator_mask" | "base_mask" => { permissions.base_mask.0 = parse_hex(value); } "owner_mask" => permissions.owner_mask.0 = parse_hex(value), "group_mask" => permissions.group_mask.0 = parse_hex(value), "everyone_mask" => permissions.everyone_mask.0 = parse_hex(value), "next_owner_mask" => permissions.next_owner_mask.0 = parse_hex(value), "creator_id" => creator_id = parse_uuid(value), "owner_id" => owner_id = parse_uuid(value), "last_owner_id" => last_owner_id = parse_uuid(value), "group_id" => group_id = parse_uuid(value), "group_owned" => { group_owned = value .and_then(|value| value.parse::().ok()) .is_some_and(|value| value != 0); } _ => {} } } } else if key == "sale_info" { while line < lines.len() { let current = parse_line(lines[line]); line += 1; let Some((key, value)) = current else { continue; }; if key == "{" { continue; } if key == "}" { break; } match key { "sale_type" => { sale_type = Utils::string_to_sale_type( value.unwrap_or_default().to_owned(), ) .unwrap_or(SaleType::Not); } "sale_price" => { sale_price = value .and_then(|value| value.parse().ok()) .unwrap_or_default(); } _ => {} } } } else { match key { "item_id" => item_id = parse_uuid(value), "parent_id" => parent_id = parse_uuid(value), "shadow_id" => { asset_id = InventoryManager::native_decrypt_shadow_id(parse_uuid(value))?; } "asset_id" => asset_id = parse_uuid(value), "type" => { asset_type = Utils::string_to_asset_type(value.unwrap_or_default().to_owned()) .unwrap_or(AssetType::Unknown); } "inv_type" => { inventory_type = Utils::string_to_inventory_type( value.unwrap_or_default().to_owned(), ) .unwrap_or(InventoryType::UNKNOWN); } "flags" => { flags = value .and_then(|value| value.parse().ok()) .unwrap_or_default(); } "name" => name = pipe_string(value), "desc" => description = pipe_string(value), "creation_date" => { if let Some(seconds) = value.and_then(|value| value.parse::().ok()) { creation_date = UNIX_EPOCH + Duration::from_secs(seconds); } } _ => {} } } } let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id)?; item.set_asset_uuid(asset_id); item.set_asset_type(asset_type); item.set_creation_date(creation_date); item.set_creator_id(creator_id); item.set_description(description); item.set_flags(flags); item.set_group_id(group_id); item.set_group_owned(group_owned); item.base.set_name(name); item.base.set_owner_id(owner_id); item.set_last_owner_id(last_owner_id); item.base.set_parent_uuid(parent_id); item.set_permissions(permissions); item.set_sale_price(sale_price); item.set_sale_type(sale_type); result.push(Box::new(item)); } } Ok(result) } impl InventoryManager { pub(crate) async fn native_update_material_inventory( &self, material: crate::assets::AssetMaterial, material_item_id: UUID, task_id: Option, cancellation_token: Option, progress: Option< Box>, >, ) -> Result<(bool, String, UUID, UUID), Error> { let client = self.client()?; let simulator = client .native_network()? .current_sim() .ok_or(Error::InvalidOperation)?; let caps = simulator.native_caps().ok_or(Error::InvalidOperation)?; let name = if task_id.is_some() { "UpdateMaterialTaskInventory" } else { "UpdateMaterialAgentInventory" }; let deadline = Instant::now() + Duration::from_secs(2); let uri = loop { if let Some(uri) = caps.capability_uri(name.to_owned())? { break uri; } if caps.seed_request_finished() || Instant::now() >= deadline { return Err(Error::InvalidOperation); } std::thread::sleep(Duration::from_millis(1)); }; let mut query = HashMap::from([("item_id".to_owned(), OSD::UUID(material_item_id))]); if let Some(task_id) = task_id { query.insert("task_id".to_owned(), OSD::UUID(task_id)); } let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(query))?; let token = cancellation_token.unwrap_or_default(); let (response, metadata) = client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), payload, token.clone(), None, ) .await?; if !(200..300).contains(&response.status_code) { return Ok(( false, format!("HTTP {}", response.status_code), UUID::zero(), UUID::zero(), )); } let meta: serde_json::Value = serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?; let status = meta .get("state") .and_then(serde_json::Value::as_str) .unwrap_or_default() .to_owned(); let uploader = meta .get("uploader") .and_then(serde_json::Value::as_str) .ok_or(Error::Argument)? .to_owned(); let data = material.to_json()?.into_bytes(); let (upload_response, response_body) = client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( Uri(uploader), "application/octet-stream".into(), data, token, progress, ) .await?; if !(200..300).contains(&upload_response.status_code) { return Ok(( false, format!("HTTP {}", upload_response.status_code), material_item_id, UUID::zero(), )); } let result: serde_json::Value = serde_json::from_slice(&response_body).map_err(|_| Error::Argument)?; let final_status = result .get("state") .and_then(serde_json::Value::as_str) .unwrap_or(&status) .to_owned(); let asset_id = result .get("new_asset") .or_else(|| result.get("new_asset_id")) .and_then(serde_json::Value::as_str) .map(|id| UUID::new_with_string(id.to_owned())) .transpose()? .unwrap_or_else(UUID::zero); let item_id = result .get("item_id") .and_then(serde_json::Value::as_str) .map(|id| UUID::new_with_string(id.to_owned())) .transpose()? .unwrap_or(material_item_id); Ok((final_status == "complete", final_status, asset_id, item_id)) } } #[cfg(test)] mod tests { use super::*; use crate::packets::{ BulkUpdateInventoryPacketItemDataBlock, InventoryDescendentsPacketFolderDataBlock, UpdateCreateInventoryItemPacketInventoryDataBlock, }; use futures_util::FutureExt; fn fixture() -> (Arc, InventoryManager, Inventory) { let mut client = GridClient::new().expect("client"); let network = client.native_network().expect("network"); client.native_set_network(network); let _ = client.native_self(); let client = Arc::new(client); let manager = InventoryManager::native_new(Some(Arc::clone(&client))).expect("manager"); let store = Inventory::new_with_grid_client_uuid( Arc::clone(&client), UUID::random().expect("owner"), ) .expect("store"); *write(&manager.inner.store) = Some(store.clone()); (client, manager, store) } fn created_block( item_id: UUID, folder_id: UUID, owner_id: UUID, callback_id: u32, ) -> UpdateCreateInventoryItemPacketInventoryDataBlock { let mut block = UpdateCreateInventoryItemPacketInventoryDataBlock::new_with_constructor() .expect("block"); block.item_id = item_id; block.folder_id = folder_id; block.owner_id = owner_id; block.asset_id = UUID::random().expect("asset"); block.inv_type = InventoryType::NOTECARD.0; block.type_ = AssetType::Notecard as i8; block.callback_id = callback_id; block.name = b"Created\0".to_vec(); block } #[test] fn fake_create_reply_correlates_callback_and_caches_validated_item() { let (_client, manager, store) = fixture(); let item_id = UUID::random().expect("item"); let folder_id = UUID::random().expect("folder"); let owner_id = UUID::random().expect("owner"); let (matched_sender, matched_receiver) = oneshot::channel(); let (other_sender, mut other_receiver) = oneshot::channel(); mutex(&manager.inner.pending_created).insert( 41, PendingCreated { deadline: Instant::now() + CALLBACK_TIMEOUT, sender: matched_sender, }, ); mutex(&manager.inner.pending_created).insert( 42, PendingCreated { deadline: Instant::now() + CALLBACK_TIMEOUT, sender: other_sender, }, ); let mut packet = UpdateCreateInventoryItemPacket::new_with_constructor().expect("packet"); packet.inventory_data = vec![created_block(item_id, folder_id, owner_id, 41)]; manager.handle_update_create(packet).expect("reply"); let item = matched_receiver .now_or_never() .expect("matched callback completed") .expect("matched sender") .expect("created item"); assert_eq!(item.base.uuid(), item_id); assert_eq!(item.base.parent_uuid(), folder_id); assert!(other_receiver.try_recv().expect("other receiver").is_none()); assert!(store.native_item_value(item_id).is_some()); assert!(mutex(&manager.inner.pending_created).contains_key(&42)); manager.native_dispose().expect("dispose"); } #[test] fn fake_bulk_copy_reply_waits_for_every_block() { let (_client, manager, _store) = fixture(); let callback_id = 73; let (sender, receiver) = oneshot::channel(); mutex(&manager.inner.pending_copied).insert( callback_id, PendingCopy { deadline: Instant::now() + CALLBACK_TIMEOUT, expected: 2, copied: Vec::new(), sender, }, ); let mut packet = BulkUpdateInventoryPacket::new_with_constructor().expect("packet"); packet.item_data = (0..2) .map(|index| { let source = created_block( UUID::random().expect("item"), UUID::random().expect("folder"), UUID::random().expect("owner"), callback_id, ); let mut block = BulkUpdateInventoryPacketItemDataBlock::new_with_constructor().expect("block"); block.item_id = source.item_id; block.folder_id = source.folder_id; block.owner_id = source.owner_id; block.asset_id = source.asset_id; block.inv_type = source.inv_type; block.type_ = source.type_; block.callback_id = source.callback_id; block.name = format!("copy-{index}\0").into_bytes(); block }) .collect(); manager.handle_bulk_update(packet).expect("bulk reply"); let result = receiver .now_or_never() .expect("copy completed") .expect("copy sender"); assert!(result.success()); assert_eq!(result.copied_items().expect("copies").len(), 2); manager.native_dispose().expect("dispose"); } #[test] fn copy_from_notecard_udp_packet_preserves_protocol_identifiers() { let (_client, manager, _store) = fixture(); let object_id = UUID::random().expect("object"); let notecard_id = UUID::random().expect("notecard"); let folder_id = UUID::random().expect("folder"); let item_id = UUID::random().expect("item"); let packet = manager .copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id) .expect("packet"); let (agent_id, session_id) = manager.ids().expect("identity"); assert_eq!(packet.agent_data.agent_id, agent_id); assert_eq!(packet.agent_data.session_id, session_id); assert_eq!(packet.notecard_data.object_id, object_id); assert_eq!(packet.notecard_data.notecard_item_id, notecard_id); assert_eq!(packet.inventory_data.len(), 1); assert_eq!(packet.inventory_data[0].folder_id, folder_id); assert_eq!(packet.inventory_data[0].item_id, item_id); manager.native_dispose().expect("dispose"); } #[test] fn cancelling_one_fetch_waiter_preserves_concurrent_correlation() { let (_client, manager, _store) = fixture(); let item_id = UUID::random().expect("item"); let (cancelled_sender, cancelled_receiver) = oneshot::channel(); let (active_sender, active_receiver) = oneshot::channel(); mutex(&manager.inner.pending_fetches).insert( item_id, vec![ PendingFetch { request_id: 1, deadline: Instant::now() + CALLBACK_TIMEOUT, sender: cancelled_sender, }, PendingFetch { request_id: 2, deadline: Instant::now() + CALLBACK_TIMEOUT, sender: active_sender, }, ], ); manager.remove_pending_fetch(item_id, 1); assert!( cancelled_receiver .now_or_never() .expect("cancelled") .is_err() ); let mut reply = FetchInventoryReplyPacket::new_with_constructor().expect("packet"); let source = created_block( item_id, UUID::random().expect("folder"), UUID::random().expect("owner"), 0, ); let mut block = crate::packets::FetchInventoryReplyPacketInventoryDataBlock::new_with_constructor() .expect("block"); block.item_id = source.item_id; block.folder_id = source.folder_id; block.owner_id = source.owner_id; block.asset_id = source.asset_id; block.inv_type = source.inv_type; block.type_ = source.type_; reply.inventory_data = vec![block]; manager.handle_fetch_reply(reply).expect("fetch reply"); assert_eq!( active_receiver .now_or_never() .expect("active callback completed") .expect("active sender") .expect("item") .base .uuid(), item_id ); manager.native_dispose().expect("dispose"); } #[test] fn stale_descendents_packet_cannot_mutate_store() { let (_client, manager, store) = fixture(); let parent_id = UUID::random().expect("parent"); let child_id = UUID::random().expect("child"); let mut parent = InventoryFolder::new(parent_id).expect("parent folder"); parent.set_version(10); store.update_node_for(&parent).expect("cache parent"); let mut packet = InventoryDescendentsPacket::new_with_constructor().expect("packet"); packet.agent_data.folder_id = parent_id; packet.agent_data.version = 9; let mut child = InventoryDescendentsPacketFolderDataBlock::new_with_constructor().expect("child block"); child.folder_id = child_id; child.parent_id = parent_id; child.name = b"stale\0".to_vec(); packet.folder_data = vec![child]; manager.handle_descendents(packet).expect("stale reply"); assert!(!store.contains_with_uuid(child_id).expect("contains")); assert_eq!( store .get_value_or_default_with_uuid_a7c63fbe::(parent_id) .expect("lookup") .expect("parent") .version(), 10 ); manager.native_dispose().expect("dispose"); } #[test] fn asset_type_without_folder_type_falls_back_to_inventory_root() { let (_client, manager, mut store) = fixture(); let root_id = UUID::random().expect("root"); let ordinary_id = UUID::random().expect("ordinary folder"); let root = InventoryFolder::new(root_id).expect("root folder"); store.set_root_folder(Some(root)); let mut ordinary = InventoryFolder::new(ordinary_id).expect("ordinary folder"); ordinary.base.set_parent_uuid(root_id); ordinary.set_preferred_type(FolderType::None); store .update_node_for(&ordinary) .expect("cache ordinary folder"); assert_eq!( manager.native_find_folder_for_asset_type(AssetType::Script), root_id ); manager.native_dispose().expect("dispose"); } #[test] fn task_xfer_rejects_announced_data_above_bound() { let (client, manager, _store) = fixture(); let simulator = Simulator::new( client.as_ref().clone(), "127.0.0.1:9".parse().expect("endpoint"), 0, None, None, ) .expect("simulator"); let xfer_id = 991_u64; let (sender, receiver) = oneshot::channel(); mutex(&manager.inner.task_xfers).insert( xfer_id, TaskXfer { deadline: Instant::now() + CALLBACK_TIMEOUT, next_packet: 0, announced_size: None, bytes: Vec::new(), sender, }, ); let mut packet = SendXferPacketPacket::new_with_constructor().expect("packet"); packet.xfer_id.id = xfer_id; packet.xfer_id.packet = 0x8000_0000; packet.data_packet.data = u32::try_from(MAX_TASK_INVENTORY_BYTES + 1) .expect("bounded size") .to_le_bytes() .to_vec(); assert_eq!( manager.handle_task_xfer_packet(packet, simulator), Err(Error::Argument) ); assert!( receiver .now_or_never() .expect("xfer completed") .expect("xfer sender") .is_none() ); assert!(!mutex(&manager.inner.task_xfers).contains_key(&xfer_id)); manager.native_dispose().expect("dispose"); } #[test] fn fake_inventory_offer_is_correlated_and_shares_acceptance_decision() { let (_client, manager, _store) = fixture(); let item_id = UUID::random().expect("item"); let destination = UUID::random().expect("destination"); let observed = Arc::new(Mutex::new(None)); let observed_by_handler = Arc::clone(&observed); let _subscription = manager.native_subscribe_inventory_object_offered(Arc::new(move |mut args| { args.set_folder_id(destination); args.set_accept(true); *mutex(&observed_by_handler) = Some(args); })); let (sender, receiver) = oneshot::channel(); mutex(&manager.inner.pending_offers).push(PendingOffer { request_id: 17, deadline: Instant::now() + CALLBACK_TIMEOUT, sender, }); let mut bucket = vec![AssetType::Notecard as u8]; bucket.extend_from_slice(&item_id.get_bytes().expect("UUID bytes")); let offer = InstantMessage { binary_bucket: bucket, dialog: InstantMessageDialog::InventoryOffered, from_agent_id: UUID::random().expect("sender"), from_agent_name: "Sender".to_owned(), group_im: false, im_session_id: UUID::random().expect("session"), message: "Notecard".to_owned(), offline: InstantMessageOnline::Online, parent_estate_id: 0, position: Vector3::zero(), region_id: UUID::zero(), timestamp: UNIX_EPOCH, to_agent_id: UUID::random().expect("recipient"), }; manager .handle_inventory_offer(InstantMessageEventArgs::new(offer, None).expect("event")) .expect("offer"); let awaited = receiver .now_or_never() .expect("waiter completed") .expect("waiter sender"); assert_eq!(awaited.object_id(), item_id); assert_eq!(awaited.asset_type(), AssetType::Notecard); assert!(awaited.accept()); assert_eq!(awaited.folder_id(), destination); assert!(observed.lock().expect("observed").is_some()); manager.native_dispose().expect("dispose"); } #[test] fn fake_caps_script_running_reply_emits_correlated_event() { let (client, manager, _store) = fixture(); let simulator = Simulator::new( client.as_ref().clone(), "127.0.0.1:9".parse().expect("endpoint"), 0, None, None, ) .expect("simulator"); let object_id = UUID::random().expect("object"); let script_id = UUID::random().expect("script"); let observed = Arc::new(Mutex::new(None)); let observed_by_handler = Arc::clone(&observed); let _subscription = manager.native_subscribe_script_running_reply(Arc::new(move |event| { *mutex(&observed_by_handler) = Some(event); })); let mut reply = ScriptRunningReplyMessage::new().expect("message"); reply.object_id = object_id; reply.item_id = script_id; reply.mono = true; reply.running = false; client .native_network() .expect("network") .dispatch_caps_event("ScriptRunningReply", &reply, simulator); let event = observed.lock().expect("observed").clone().expect("event"); assert_eq!(event.object_id(), object_id); assert_eq!(event.script_id(), script_id); assert!(event.is_mono()); assert!(!event.is_running()); manager.native_dispose().expect("dispose"); } }