Implement native RLV state and permissions (#79)
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

This commit is contained in:
2026-08-10 23:55:11 +00:00
parent 468a0f0619
commit 362fa62059
12 changed files with 3429 additions and 347 deletions

View File

@@ -0,0 +1,602 @@
#![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)))
);
}