Some checks failed
Native code generation / deterministic (push) Failing after 2m1s
Imaging and meshing gate / native (push) Successful in 5m22s
JPEG 2000 feature / linux (push) Successful in 2m45s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Successful in 30m44s
603 lines
19 KiB
Rust
603 lines
19 KiB
Rust
#![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<RlvValue>) -> RlvRestriction {
|
|
RlvRestriction::from_values(behavior, guid(sender), format!("sender-{sender}"), values)
|
|
}
|
|
|
|
fn poll_ready<F: Future>(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::<Vec<_>>(),
|
|
[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::<Vec<_>>();
|
|
tracked.sort_by_key(|value| value.0);
|
|
assert_eq!(tracked, (1..=24).map(guid).collect::<Vec<_>>());
|
|
}
|
|
|
|
#[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)))
|
|
);
|
|
}
|