#![allow(clippy::float_cmp)] // Protocol fixture values are exactly representable. use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; use std::thread; use libremetaverse_rlv::{ CameraSettings, InventoryMap, RlvAttachmentPoint, RlvBlacklist, RlvInventoryItem, RlvPermissionsService, RlvPermissionsServiceHoverTextLocation, RlvPermissionsServiceObjectLocation, RlvPermissionsServiceTouchLocation, RlvRestriction, RlvRestrictionManager, RlvRestrictionType, RlvSharedFolder, RlvValue, RlvWearableType, }; use libremetaverse_types::compat::Guid; fn guid(byte: u8) -> Guid { Guid([byte; 16]) } fn restriction(behavior: RlvRestrictionType, sender: u8, values: Vec) -> RlvRestriction { RlvRestriction::from_values(behavior, guid(sender), format!("sender-{sender}"), values) } fn poll_ready(future: F) -> F::Output { let waker = Waker::noop(); let mut context = Context::from_waker(waker); let mut future = std::pin::pin!(future); match future.as_mut().poll(&mut context) { Poll::Ready(output) => output, Poll::Pending => panic!("pure state future unexpectedly yielded"), } } #[test] fn manager_deduplicates_updates_and_dispatches_after_unlocking() { let manager = RlvRestrictionManager::new(); let events = Arc::new(Mutex::new(Vec::new())); let captured = Arc::clone(&events); let callback_manager = manager.clone(); let _subscription = manager.subscribe_restriction_updated(Some(Arc::new(move |event| { // A re-entrant read would deadlock if callbacks ran under the state lock. let count = callback_manager .find_restrictions(None, None) .unwrap() .len(); captured .lock() .unwrap() .push((event.is_new(), event.is_deleted(), count)); }))); let fly = restriction(RlvRestrictionType::Fly, 1, vec![]); assert!(manager.add_restriction(fly.clone()).unwrap()); assert!(!manager.add_restriction(fly.clone()).unwrap()); assert_eq!( manager.get_tracked_prim_ids().unwrap().collect::>(), [guid(1)] ); assert!(manager.remove_restriction(&fly)); assert!(!manager.remove_restriction(&fly)); assert_eq!( *events.lock().unwrap(), [(true, false, 1), (false, true, 0)] ); } #[test] fn manager_handles_concurrent_independent_sources_deterministically() { let manager = RlvRestrictionManager::new(); let mut workers = Vec::new(); for sender in 1..=24 { let worker_manager = manager.clone(); workers.push(thread::spawn(move || { worker_manager .add_restriction(restriction(RlvRestrictionType::Jump, sender, vec![])) .unwrap() })); } for worker in workers { assert!(worker.join().unwrap()); } let restrictions = manager .get_restrictions_by_type(RlvRestrictionType::Jump) .unwrap(); assert_eq!(restrictions.len(), 24); let mut tracked = manager.get_tracked_prim_ids().unwrap().collect::>(); tracked.sort_by_key(|value| value.0); assert_eq!(tracked, (1..=24).map(guid).collect::>()); } #[test] fn concurrent_lock_rebuilds_cannot_publish_a_stale_revision() { let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap(); let first = root.add_child(guid(2), "first".into()).unwrap(); let second = root.add_child(guid(3), "second".into()).unwrap(); first .add_item( guid(4), "first attachment".into(), false, None, Some(Some(guid(40))), None, None, ) .unwrap(); second .add_item( guid(5), "second attachment".into(), false, None, Some(Some(guid(50))), None, None, ) .unwrap(); let manager = RlvRestrictionManager::new(); manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap())); let barrier = Arc::new(std::sync::Barrier::new(3)); let mut workers = Vec::new(); for sender in [40, 50] { let worker_manager = manager.clone(); let worker_barrier = Arc::clone(&barrier); workers.push(thread::spawn(move || { worker_barrier.wait(); worker_manager .add_restriction(restriction(RlvRestrictionType::DetachThis, sender, vec![])) .unwrap(); })); } barrier.wait(); for worker in workers { worker.join().unwrap(); } let locked = manager.get_locked_folders().unwrap(); assert_eq!(locked.len(), 2); assert!(!locked[&first.id()].can_detach()); assert!(!locked[&second.id()].can_detach()); } #[test] fn source_removal_counts_repeated_inputs_before_deduplication() { let manager = RlvRestrictionManager::new(); manager .add_restriction(restriction(RlvRestrictionType::Fly, 1, vec![])) .unwrap(); let repeated = std::iter::repeat_n(guid(1), 100_001); assert!(poll_ready(manager.remove_restrictions_for_objects(Box::new(repeated), None)).is_err()); assert_eq!(manager.find_restrictions(None, None).unwrap().len(), 1); } #[test] fn blacklist_is_case_insensitive_sorted_and_thread_safe() { let blacklist = RlvBlacklist::new(); let mut workers = Vec::new(); for behavior in ["Fly", "JUMP", "sit", "CamZoomMax"] { let worker_blacklist = blacklist.clone(); workers.push(thread::spawn(move || { worker_blacklist .blacklist_behavior(behavior.to_owned()) .unwrap(); })); } for worker in workers { worker.join().unwrap(); } assert_eq!( blacklist.get_blacklist().unwrap(), ["camzoommax", "fly", "jump", "sit"] ); assert!(blacklist.is_blacklisted("fLy".into()).unwrap()); blacklist.un_blacklist_behavior("FLY".into()).unwrap(); assert!(!blacklist.is_blacklisted("fly".into()).unwrap()); } #[test] fn camera_restrictions_apply_minimum_maximum_alias_and_last_value_rules() { let manager = RlvRestrictionManager::new(); for (sender, value) in [(4, 0.4), (7, 0.7)] { manager .add_restriction(restriction( RlvRestrictionType::CamZoomMin, sender, vec![RlvValue::Real(value)], )) .unwrap(); } for (sender, value) in [(20, 2.0), (12, 1.2)] { manager .add_restriction(restriction( RlvRestrictionType::CamZoomMax, sender, vec![RlvValue::Real(value)], )) .unwrap(); } manager .add_restriction(restriction( RlvRestrictionType::CamDistMin, 31, vec![RlvValue::Real(3.0)], )) .unwrap(); manager .add_restriction(restriction( RlvRestrictionType::CamDrawColor, 32, vec![ RlvValue::Real(0.0), RlvValue::Real(0.5), RlvValue::Real(1.0), ], )) .unwrap(); manager .add_restriction(restriction( RlvRestrictionType::CamDrawColor, 33, vec![ RlvValue::Real(1.0), RlvValue::Real(1.5), RlvValue::Real(-1.0), ], )) .unwrap(); manager .add_restriction(restriction(RlvRestrictionType::CamUnlock, 34, vec![])) .unwrap(); let camera = RlvPermissionsService::new(manager) .get_camera_restrictions() .unwrap(); assert_eq!(camera.zoom_min().flatten(), Some(0.7)); assert_eq!(camera.zoom_max().flatten(), Some(1.2)); assert_eq!(camera.av_dist_min().flatten(), Some(3.0)); assert_eq!(camera.draw_color().flatten().unwrap().0, [0.5, 0.75, 0.5]); assert!(camera.is_locked()); } #[test] fn camera_settings_preserve_constructor_field_order() { let settings = CameraSettings::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).unwrap(); assert_eq!(settings.av_dist_min(), 1.0); assert_eq!(settings.av_dist_max(), 2.0); assert_eq!(settings.fov_min(), 3.0); assert_eq!(settings.fov_max(), 4.0); assert_eq!(settings.zoom_min(), 5.0); assert_eq!(settings.current_fov(), 6.0); } #[test] fn permission_exceptions_are_targeted_and_secure_rules_take_precedence() { let manager = RlvRestrictionManager::new(); let permissions = RlvPermissionsService::new(manager.clone()); let allowed_user = guid(90); let other_user = guid(91); manager .add_restriction(restriction(RlvRestrictionType::SendIm, 1, vec![])) .unwrap(); assert!( !permissions .can_send_im("hello".into(), Some(Some(allowed_user)), None) .unwrap() ); manager .add_restriction(restriction( RlvRestrictionType::SendIm, 1, vec![RlvValue::Uuid(allowed_user)], )) .unwrap(); assert!( permissions .can_send_im("hello".into(), Some(Some(allowed_user)), None) .unwrap() ); assert!( !permissions .can_send_im("hello".into(), Some(Some(other_user)), None) .unwrap() ); manager .add_restriction(restriction(RlvRestrictionType::SendImSec, 2, vec![])) .unwrap(); assert!( !permissions .can_send_im("hello".into(), Some(Some(allowed_user)), None) .unwrap() ); } #[test] fn simple_interaction_and_location_permissions_compose() { let manager = RlvRestrictionManager::new(); let permissions = RlvPermissionsService::new(manager.clone()); assert!(permissions.can_fly().unwrap()); assert!(permissions.can_sit().unwrap()); assert!(permissions.can_rez().unwrap()); assert!( permissions .can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None) .unwrap() ); let interact = restriction(RlvRestrictionType::Interact, 1, vec![]); manager.add_restriction(interact.clone()).unwrap(); assert!(!permissions.can_sit().unwrap()); assert!(!permissions.can_rez().unwrap()); assert!( !permissions .can_edit(RlvPermissionsServiceObjectLocation::Hud, None) .unwrap() ); assert!(manager.remove_restriction(&interact)); manager .add_restriction(restriction(RlvRestrictionType::EditWorld, 2, vec![])) .unwrap(); assert!( !permissions .can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None) .unwrap() ); assert!( permissions .can_edit(RlvPermissionsServiceObjectLocation::Attached, None) .unwrap() ); } #[test] fn distance_and_channel_outputs_apply_defaults_minima_and_precedence() { let manager = RlvRestrictionManager::new(); let permissions = RlvPermissionsService::new(manager.clone()); let mut distance = -1.0; assert!(!permissions.can_sit_tp(&mut distance).unwrap()); assert_eq!(distance, 1.5); manager .add_restriction(restriction( RlvRestrictionType::SitTp, 1, vec![RlvValue::Real(4.0)], )) .unwrap(); manager .add_restriction(restriction( RlvRestrictionType::SitTp, 2, vec![RlvValue::Real(2.0)], )) .unwrap(); assert!(permissions.can_sit_tp(&mut distance).unwrap()); assert_eq!(distance, 2.0); manager .add_restriction(restriction( RlvRestrictionType::RedirChat, 3, vec![RlvValue::Integer(7)], )) .unwrap(); manager .add_restriction(restriction( RlvRestrictionType::RedirChat, 4, vec![RlvValue::Integer(7)], )) .unwrap(); let mut channels = Vec::new(); assert!(permissions.try_get_redir_chat_channels(&mut channels)); assert_eq!(channels, [7]); manager .add_restriction(restriction(RlvRestrictionType::SendChannel, 5, vec![])) .unwrap(); assert!(!permissions.can_chat(8, "private".into()).unwrap()); manager .add_restriction(restriction( RlvRestrictionType::SendChannel, 5, vec![RlvValue::Integer(8)], )) .unwrap(); assert!(permissions.can_chat(8, "private".into()).unwrap()); manager .add_restriction(restriction( RlvRestrictionType::SendChannelExcept, 6, vec![RlvValue::Integer(8)], )) .unwrap(); assert!(!permissions.can_chat(8, "private".into()).unwrap()); } #[test] fn touch_and_hover_rules_apply_early_overrides_and_explicit_targets() { let manager = RlvRestrictionManager::new(); let permissions = RlvPermissionsService::new(manager.clone()); let prim = guid(80); manager .add_restriction(restriction(RlvRestrictionType::TouchAll, 1, vec![])) .unwrap(); assert!( !permissions .can_touch( RlvPermissionsServiceTouchLocation::RezzedInWorld, prim, None, None, ) .unwrap() ); manager .add_restriction(restriction(RlvRestrictionType::TouchMe, 80, vec![])) .unwrap(); assert!( permissions .can_touch( RlvPermissionsServiceTouchLocation::RezzedInWorld, prim, None, None, ) .unwrap() ); manager .add_restriction(restriction( RlvRestrictionType::FarTouch, 2, vec![RlvValue::Real(1.5)], )) .unwrap(); assert!( !permissions .can_touch( RlvPermissionsServiceTouchLocation::RezzedInWorld, prim, None, Some(Some(2.0)), ) .unwrap() ); manager .add_restriction(restriction( RlvRestrictionType::ShowHoverText, 3, vec![RlvValue::Uuid(prim)], )) .unwrap(); assert!( !permissions .can_show_hover_text( RlvPermissionsServiceHoverTextLocation::World, Some(Some(prim)), ) .unwrap() ); assert!( permissions .can_show_hover_text( RlvPermissionsServiceHoverTextLocation::World, Some(Some(guid(81))), ) .unwrap() ); } #[test] fn attach_and_detach_permissions_combine_item_type_source_and_folder_locks() { let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap(); let folder = root.add_child(guid(2), "clothes".into()).unwrap(); let item = folder .add_item( guid(3), "shirt".into(), false, Some(Some(RlvAttachmentPoint::Chest)), Some(Some(guid(30))), Some(Some(RlvWearableType::Shirt)), None, ) .unwrap(); let manager = RlvRestrictionManager::new(); manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap())); let permissions = RlvPermissionsService::new(manager.clone()); assert!( permissions .can_attach_with_rlv_inventory_item_boolean(item.clone(), true) .unwrap() ); assert!( permissions .can_detach_with_rlv_inventory_item(item.clone()) .unwrap() ); manager .add_restriction(restriction(RlvRestrictionType::DetachThis, 30, vec![])) .unwrap(); assert!( !permissions .can_detach_with_rlv_inventory_item(item.clone()) .unwrap() ); manager .add_restriction(restriction( RlvRestrictionType::AttachThis, 31, vec![RlvValue::String("clothes".into())], )) .unwrap(); assert!( !permissions .can_attach_with_rlv_inventory_item_boolean(item.clone(), true) .unwrap() ); manager .add_restriction(restriction( RlvRestrictionType::RemOutfit, 32, vec![RlvValue::WearableType(RlvWearableType::Shirt)], )) .unwrap(); assert!( !permissions .can_detach_with_rlv_inventory_item(item) .unwrap() ); } #[test] fn recursive_folder_locks_and_exceptions_publish_isolated_snapshots() { let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap(); let locked = root.add_child(guid(2), "locked".into()).unwrap(); let nested = locked.add_child(guid(3), "nested".into()).unwrap(); locked .add_item( guid(4), "worn object".into(), false, Some(Some(RlvAttachmentPoint::Chest)), Some(Some(guid(44))), None, None, ) .unwrap(); let inventory = InventoryMap::new(root, vec![]).unwrap(); let manager = RlvRestrictionManager::new(); manager.set_inventory_map(Some(inventory)); let lock = restriction(RlvRestrictionType::DetachAllThis, 44, vec![]); manager.add_restriction(lock.clone()).unwrap(); let first_snapshot = manager.get_locked_folders().unwrap(); assert!(!first_snapshot[&locked.id()].can_detach()); assert!(!first_snapshot[&nested.id()].can_detach()); let exception = restriction( RlvRestrictionType::DetachAllThisExcept, 45, vec![RlvValue::String("locked".into())], ); manager.add_restriction(exception).unwrap(); let second_snapshot = manager.get_locked_folders().unwrap(); assert!(second_snapshot[&locked.id()].can_detach()); assert!(second_snapshot[&nested.id()].can_detach()); // Previously returned snapshots remain immutable after later manager updates. assert!(!first_snapshot[&locked.id()].can_detach()); assert!(manager.remove_restriction(&lock)); } #[test] fn external_inventory_dictionary_membership_is_snapshotted_with_reference_values() { let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap(); let mut external = RlvInventoryItem::new( guid(2), "outside".into(), false, Some(Some(guid(3))), None, None, None, None, ) .unwrap(); let map = InventoryMap::new(root, vec![external.clone()]).unwrap(); external.set_name("changed".into()); // Items use C# reference semantics, while the dictionary's membership is a // fixed immutable snapshot. Mutating the referenced item is visible. assert_eq!(map.external_items().len(), 1); assert_eq!( map.external_items().get(&guid(2)).unwrap().name(), "changed" ); external.set_attached_prim_id(Some(Some(guid(4)))); assert_eq!( map.external_items() .get(&guid(2)) .unwrap() .attached_prim_id(), Some(Some(guid(4))) ); }