diff --git a/.gitea/workflows/rust-workspace.yml b/.gitea/workflows/rust-workspace.yml index a3dd63d..2967de2 100644 --- a/.gitea/workflows/rust-workspace.yml +++ b/.gitea/workflows/rust-workspace.yml @@ -10,6 +10,7 @@ on: - "tools/check_milestone_10_issue_76.py" - "tools/check_milestone_10_issue_77.py" - "tools/check_milestone_10_issue_78.py" + - "tools/check_milestone_10_issue_79.py" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -22,6 +23,7 @@ on: - "tools/check_milestone_10_issue_76.py" - "tools/check_milestone_10_issue_77.py" - "tools/check_milestone_10_issue_78.py" + - "tools/check_milestone_10_issue_79.py" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -60,6 +62,7 @@ jobs: python3 tools/check_milestone_10_issue_76.py python3 tools/check_milestone_10_issue_77.py python3 tools/check_milestone_10_issue_78.py + python3 tools/check_milestone_10_issue_79.py - name: Test the complete native world milestone run: python3 tools/test_milestone_09.py - name: Compile every workspace target with bounded memory diff --git a/README.md b/README.md index 51bfddb..3f8a40d 100644 --- a/README.md +++ b/README.md @@ -463,14 +463,17 @@ reports decode allocations and timing without replacing correctness gates. The format, ownership, error, and tangent-output contracts are documented in the [`MeshFoundry` guide](crates/libremetaverse-rendering-mesh-foundry/README.md). -The native RLV protocol layer parses bounded chat messages into typed clear, -action, restriction, and query directives without changing client state. It +The native RLV layer parses bounded chat messages into typed clear, action, +restriction, and query directives, then provides independent thread-safe +restriction, inventory-map, folder-lock, camera, blacklist, and permission +providers without network I/O. It preserves source casing, folder paths, separators, numeric channels, UUIDs, and attachment/wearable aliases while exposing canonical behavior names and source-located typed errors. All 119 restriction names from the pinned LibreMetaverse snapshot are covered, and deterministic malformed-input -mutations exercise the parser without an external fuzzing runtime. State, -camera, inventory, callback, and transport effects remain explicit consumers -of this pure layer. The grammar, normalization rules, limits, and ownership -boundary are documented in the +mutations exercise the parser without an external fuzzing runtime. Immutable +snapshots and post-lock update events make concurrent policy reads +deterministic; callback execution and transport effects remain explicit +consumers of the layer. The grammar, state precedence, normalization rules, +limits, and ownership boundary are documented in the [`RLV protocol guide`](crates/libremetaverse-rlv/README.md). diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 9713bbe..fa89c46 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -9,7 +9,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims | -| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 2 types / 11 members; remaining surface is callable failure-only shims | +| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 12 types / 147 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | native implementation: 1 type / 14 members; no generated shims remain | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain | | `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain | diff --git a/crates/libremetaverse-rlv/Cargo.toml b/crates/libremetaverse-rlv/Cargo.toml index ed2ee15..9d6f093 100644 --- a/crates/libremetaverse-rlv/Cargo.toml +++ b/crates/libremetaverse-rlv/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "Bounded native RLV protocol parsing and compatibility types for MetaCrate" +description = "Bounded native RLV protocol, state, inventory, lock, camera, and permission layer" [dependencies] libremetaverse-types = { path = "../libremetaverse-types" } diff --git a/crates/libremetaverse-rlv/README.md b/crates/libremetaverse-rlv/README.md index 8d9b141..c9d6e02 100644 --- a/crates/libremetaverse-rlv/README.md +++ b/crates/libremetaverse-rlv/README.md @@ -1,11 +1,13 @@ # Native RLV protocol layer -`libremetaverse-rlv` implements the side-effect-free protocol boundary for the +`libremetaverse-rlv` implements the protocol and pure state boundary for the Restrained Love Viewer support in the pinned LibreMetaverse snapshot. The -parser converts one bounded chat message into typed commands. It does not -mutate restrictions, move the camera, touch inventory, send replies, or invoke -callbacks; those stateful responsibilities are separate milestone work that -consumes this layer. +side-effect-free parser converts one bounded chat message into typed commands. +Independent, thread-safe managers then store restrictions and evaluate +inventory, folder locks, camera limits, blacklists, and permissions without +performing network I/O. Command execution, viewer callbacks, replies, and +`GridClient` integration remain separate milestone work that consumes this +layer. ## Message contract @@ -57,6 +59,33 @@ insertion mutations of actions, restrictions, queries, UUIDs, aliases, and multi-command messages. Every mutation must return a value or a positioned error without panicking. The limits apply before large parser allocations. +## State, inventory, and permission contract + +`RlvSharedFolder` and `RlvInventoryItem` preserve the reference semantics of +their C# counterparts. `InventoryMap` walks a bounded shared-inventory tree and +publishes immutable dictionary/list membership snapshots. Path lookup handles +hidden/private prefixes, exact names containing forward slashes, and the +reference implementation's longest matching segment rule. Lookup by item, +attached prim, attachment point, and wearable type does not call a client or +inventory service. + +`RlvRestrictionManager` deduplicates exact restrictions, retains deterministic +insertion order, removes all state for selected object sources, and rebuilds +immutable locked-folder snapshots when either restrictions or inventory +changes. Recursive and non-recursive attach/detach locks support sender-item, +attachment, wearable, and path targets plus their exception variants. Update +handlers run only after manager locks are released, so callbacks can safely +query the manager again. Poisoned synchronization primitives recover their +owned state instead of making later reads fail. + +`RlvPermissionsService` evaluates simple restrictions, secure and explicit +target rules, permissive exception precedence, IM/chat/channel behavior, +teleport limits, edit/touch/hover decisions, shared and unshared wear, and +folder attachment locks. Camera aggregation applies the reference min/max, +clamping, averaging, alias, texture, and lock rules. The case-insensitive +blacklist returns a sorted snapshot. These providers are deterministic, +thread-safe, and contain no network or callback dependencies. + ## Reproducible verification Run the issue-owned checks with one build job: @@ -64,9 +93,11 @@ Run the issue-owned checks with one build job: ```sh CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rlv --locked CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_common_semantics --locked +CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_inventory_map_semantics --locked CARGO_BUILD_JOBS=1 cargo clippy -p libremetaverse-rlv --all-targets --locked -- -D warnings RUSTDOCFLAGS='-D warnings' CARGO_BUILD_JOBS=1 cargo doc -p libremetaverse-rlv --no-deps --locked python3 tools/check_milestone_10_issue_78.py +python3 tools/check_milestone_10_issue_79.py ``` These commands are cross-platform. The Gitea workflow runs the audit and diff --git a/crates/libremetaverse-rlv/src/generated.rs b/crates/libremetaverse-rlv/src/generated.rs index d1bfa71..bda600c 100644 --- a/crates/libremetaverse-rlv/src/generated.rs +++ b/crates/libremetaverse-rlv/src/generated.rs @@ -49,78 +49,68 @@ impl AttachmentRequest { } /// C# type: `T:LibreMetaverse.RLV.CameraRestrictions`. -pub struct CameraRestrictions; +pub use crate::state::CameraRestrictions; impl CameraRestrictions { /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.AvDist`. pub fn av_dist(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.AvDist") + self.native_av_dist() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.AvDistMax`. pub fn av_dist_max(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.CameraRestrictions.AvDistMax" - ) + self.native_av_dist_max() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.AvDistMin`. pub fn av_dist_min(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.CameraRestrictions.AvDistMin" - ) + self.native_av_dist_min() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.DrawAlphaMax`. pub fn draw_alpha_max(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.CameraRestrictions.DrawAlphaMax" - ) + self.native_draw_alpha_max() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.DrawAlphaMin`. pub fn draw_alpha_min(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.CameraRestrictions.DrawAlphaMin" - ) + self.native_draw_alpha_min() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.DrawColor`. pub fn draw_color(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.CameraRestrictions.DrawColor" - ) + self.native_draw_color() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.DrawMax`. pub fn draw_max(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.DrawMax") + self.native_draw_max() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.DrawMin`. pub fn draw_min(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.DrawMin") + self.native_draw_min() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.FovMax`. pub fn fov_max(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.FovMax") + self.native_fov_max() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.FovMin`. pub fn fov_min(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.FovMin") + self.native_fov_min() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.IsLocked`. pub fn is_locked(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.IsLocked") + self.native_is_locked() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.Texture`. pub fn texture(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.Texture") + self.native_texture() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.ZoomMax`. pub fn zoom_max(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.ZoomMax") + self.native_zoom_max() } /// C# member: `P:LibreMetaverse.RLV.CameraRestrictions.ZoomMin`. pub fn zoom_min(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraRestrictions.ZoomMin") + self.native_zoom_min() } } /// C# type: `T:LibreMetaverse.RLV.CameraSettings`. -pub struct CameraSettings; +pub use crate::state::CameraSettings; impl CameraSettings { /// C# member: `M:LibreMetaverse.RLV.CameraSettings.#ctor(System.Single,System.Single,System.Single,System.Single,System.Single,System.Single)`. pub fn new( @@ -131,33 +121,38 @@ impl CameraSettings { zoom_min: f32, current_fov: f32, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.CameraSettings.#ctor(System.Single,System.Single,System.Single,System.Single,System.Single,System.Single)", + Self::native_new( + av_dist_min, + av_dist_max, + fov_min, + fov_max, + zoom_min, + current_fov, ) } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.AvDistMax`. pub fn av_dist_max(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.AvDistMax") + self.native_av_dist_max() } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.AvDistMin`. pub fn av_dist_min(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.AvDistMin") + self.native_av_dist_min() } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.CurrentFov`. pub fn current_fov(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.CurrentFov") + self.native_current_fov() } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.FovMax`. pub fn fov_max(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.FovMax") + self.native_fov_max() } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.FovMin`. pub fn fov_min(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.FovMin") + self.native_fov_min() } /// C# member: `P:LibreMetaverse.RLV.CameraSettings.ZoomMin`. pub fn zoom_min(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.CameraSettings.ZoomMin") + self.native_zoom_min() } } @@ -511,16 +506,14 @@ pub trait IRlvQueryCallbacks { } /// C# type: `T:LibreMetaverse.RLV.InventoryMap`. -pub struct InventoryMap; +pub use crate::state::InventoryMap; impl InventoryMap { /// C# member: `M:LibreMetaverse.RLV.InventoryMap.#ctor(LibreMetaverse.RLV.RlvSharedFolder,System.Collections.Generic.IReadOnlyCollection{LibreMetaverse.RLV.RlvInventoryItem})`. pub fn new( root: libremetaverse_rlv::RlvSharedFolder, external_items: Vec, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.#ctor(LibreMetaverse.RLV.RlvSharedFolder,System.Collections.Generic.IReadOnlyCollection{LibreMetaverse.RLV.RlvInventoryItem})", - ) + Self::native_new(root, external_items) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.FindFoldersContaining(System.Boolean,System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})`. pub fn find_folders_containing( @@ -530,8 +523,11 @@ impl InventoryMap { attachment_point: Option>, wearable_type: Option>, ) -> Result>, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.FindFoldersContaining(System.Boolean,System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})", + self.native_find_folders_containing( + limit_to_one_result, + attached_prim_id, + attachment_point, + wearable_type, ) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.GetItemsByAttachmentPoint(LibreMetaverse.RLV.RlvAttachmentPoint)`. @@ -539,36 +535,28 @@ impl InventoryMap { &self, attachment_point: libremetaverse_rlv::RlvAttachmentPoint, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.GetItemsByAttachmentPoint(LibreMetaverse.RLV.RlvAttachmentPoint)", - ) + self.native_get_items_by_attachment_point(attachment_point) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.GetItemsById(System.Guid)`. pub fn get_items_by_id( &self, item_id: libremetaverse_types::compat::Guid, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.GetItemsById(System.Guid)", - ) + self.native_get_items_by_id(item_id) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.GetItemsByPrimId(System.Guid)`. pub fn get_items_by_prim_id( &self, prim_id: libremetaverse_types::compat::Guid, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.GetItemsByPrimId(System.Guid)", - ) + self.native_get_items_by_prim_id(prim_id) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.GetItemsByWearableType(LibreMetaverse.RLV.RlvWearableType)`. pub fn get_items_by_wearable_type( &self, wearable_type: libremetaverse_rlv::RlvWearableType, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.InventoryMap.GetItemsByWearableType(LibreMetaverse.RLV.RlvWearableType)", - ) + self.native_get_items_by_wearable_type(wearable_type) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.TryBuildPathToFolder(System.Guid,System.String@)`. pub fn try_build_path_to_folder( @@ -576,9 +564,7 @@ impl InventoryMap { folder_id: libremetaverse_types::compat::Guid, final_path: &mut Option, ) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.InventoryMap.TryBuildPathToFolder(System.Guid,System.String@)" - ) + self.native_try_build_path_to_folder(folder_id, final_path) } /// C# member: `M:LibreMetaverse.RLV.InventoryMap.TryGetFolderFromPath(System.String,System.Boolean,LibreMetaverse.RLV.RlvSharedFolder@)`. pub fn try_get_folder_from_path( @@ -587,9 +573,7 @@ impl InventoryMap { skip_private_folders: bool, folder: &mut Option, ) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.InventoryMap.TryGetFolderFromPath(System.String,System.Boolean,LibreMetaverse.RLV.RlvSharedFolder@)" - ) + self.native_try_get_folder_from_path(path, skip_private_folders, folder) } /// C# member: `P:LibreMetaverse.RLV.InventoryMap.ExternalItems`. pub fn external_items( @@ -598,7 +582,7 @@ impl InventoryMap { libremetaverse_types::compat::Guid, libremetaverse_rlv::RlvInventoryItem, > { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.InventoryMap.ExternalItems") + self.native_external_items() } /// C# member: `P:LibreMetaverse.RLV.InventoryMap.Folders`. pub fn folders( @@ -607,7 +591,7 @@ impl InventoryMap { libremetaverse_types::compat::Guid, libremetaverse_rlv::RlvSharedFolder, > { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.InventoryMap.Folders") + self.native_folders() } /// C# member: `P:LibreMetaverse.RLV.InventoryMap.Items`. pub fn items( @@ -616,68 +600,56 @@ impl InventoryMap { libremetaverse_types::compat::Guid, libremetaverse_types::compat::ImmutableList, > { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.InventoryMap.Items") + self.native_items() } /// C# member: `P:LibreMetaverse.RLV.InventoryMap.Root`. pub fn root(&self) -> libremetaverse_rlv::RlvSharedFolder { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.InventoryMap.Root") + self.native_root() } } /// C# type: `T:LibreMetaverse.RLV.LockedFolderPublic`. -pub struct LockedFolderPublic; +pub use crate::state::LockedFolderPublic; impl LockedFolderPublic { /// C# member: `M:LibreMetaverse.RLV.LockedFolderPublic.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.RLV.LockedFolderPublic.ToString") + self.native_to_string() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.AttachExceptions`. pub fn attach_exceptions(&self) -> Vec { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.AttachExceptions" - ) + self.native_attach_exceptions() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.AttachRestrictions`. pub fn attach_restrictions(&self) -> Vec { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.AttachRestrictions" - ) + self.native_attach_restrictions() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.CanAttach`. pub fn can_attach(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.CanAttach" - ) + self.native_can_attach() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.CanDetach`. pub fn can_detach(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.CanDetach" - ) + self.native_can_detach() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.DetachExceptions`. pub fn detach_exceptions(&self) -> Vec { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.DetachExceptions" - ) + self.native_detach_exceptions() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.DetachRestrictions`. pub fn detach_restrictions(&self) -> Vec { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.LockedFolderPublic.DetachRestrictions" - ) + self.native_detach_restrictions() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.Id`. pub fn id(&self) -> libremetaverse_types::compat::Guid { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.LockedFolderPublic.Id") + self.native_id() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.IsLocked`. pub fn is_locked(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.LockedFolderPublic.IsLocked") + self.native_is_locked() } /// C# member: `P:LibreMetaverse.RLV.LockedFolderPublic.Name`. pub fn name(&self) -> String { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.LockedFolderPublic.Name") + self.native_name() } } @@ -982,29 +954,23 @@ pub enum RlvAttachmentPoint { } /// C# type: `T:LibreMetaverse.RLV.RlvBlacklist`. -pub struct RlvBlacklist; +pub use crate::state::RlvBlacklist; impl RlvBlacklist { /// C# member: `M:LibreMetaverse.RLV.RlvBlacklist.BlacklistBehavior(System.String)`. pub fn blacklist_behavior(&self, behavior: String) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvBlacklist.BlacklistBehavior(System.String)", - ) + self.native_blacklist_behavior(behavior) } /// C# member: `M:LibreMetaverse.RLV.RlvBlacklist.GetBlacklist`. pub fn get_blacklist(&self) -> Result, crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvBlacklist.GetBlacklist") + self.native_get_blacklist() } /// C# member: `M:LibreMetaverse.RLV.RlvBlacklist.IsBlacklisted(System.String)`. pub fn is_blacklisted(&self, behavior: String) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvBlacklist.IsBlacklisted(System.String)", - ) + self.native_is_blacklisted(behavior) } /// C# member: `M:LibreMetaverse.RLV.RlvBlacklist.UnBlacklistBehavior(System.String)`. pub fn un_blacklist_behavior(&self, behavior: String) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvBlacklist.UnBlacklistBehavior(System.String)", - ) + self.native_un_blacklist_behavior(behavior) } } @@ -1283,7 +1249,7 @@ pub enum RlvGetEnvType { } /// C# type: `T:LibreMetaverse.RLV.RlvInventoryItem`. -pub struct RlvInventoryItem; +pub use crate::state::RlvInventoryItem; impl RlvInventoryItem { /// C# member: `M:LibreMetaverse.RLV.RlvInventoryItem.#ctor(System.Guid,System.String,System.Boolean,System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvWearableType},System.Nullable{LibreMetaverse.RLV.RlvGestureState})`. pub fn new( @@ -1296,107 +1262,102 @@ impl RlvInventoryItem { worn_on: Option>, gesture_state: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvInventoryItem.#ctor(System.Guid,System.String,System.Boolean,System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvWearableType},System.Nullable{LibreMetaverse.RLV.RlvGestureState})", + Self::native_new( + id, + name, + is_link, + external_folder_id, + attached_to, + attached_prim_id, + worn_on, + gesture_state, ) } /// C# member: `M:LibreMetaverse.RLV.RlvInventoryItem.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.RLV.RlvInventoryItem.ToString") + self.native_to_string() } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.AttachedPrimId`. pub fn attached_prim_id(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvInventoryItem.AttachedPrimId" - ) + self.native_attached_prim_id() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.AttachedPrimId`. pub fn set_attached_prim_id( &mut self, value: Option>, ) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvInventoryItem.AttachedPrimId" - ) + self.native_set_attached_prim_id(value) } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.AttachedTo`. pub fn attached_to(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.AttachedTo") + self.native_attached_to() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.AttachedTo`. pub fn set_attached_to( &mut self, value: Option>, ) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.AttachedTo") + self.native_set_attached_to(value) } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.Folder`. pub fn folder(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.Folder") + self.native_folder() } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.FolderId`. pub fn folder_id(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.FolderId") + self.native_folder_id() } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.GestureState`. pub fn gesture_state(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvInventoryItem.GestureState" - ) + self.native_gesture_state() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.GestureState`. pub fn set_gesture_state( &mut self, value: Option>, ) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvInventoryItem.GestureState" - ) + self.native_set_gesture_state(value) } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.Id`. pub fn id(&self) -> libremetaverse_types::compat::Guid { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.Id") + self.native_id() } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.IsLink`. pub fn is_link(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.IsLink") + self.native_is_link() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.IsLink`. pub fn set_is_link(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.IsLink") + self.native_set_is_link(value) } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.Name`. pub fn name(&self) -> String { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.Name") + self.native_name() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.Name`. pub fn set_name(&mut self, value: String) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.Name") + self.native_set_name(value) } /// C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.WornOn`. pub fn worn_on(&self) -> Option> { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.WornOn") + self.native_worn_on() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvInventoryItem.WornOn`. pub fn set_worn_on(&mut self, value: Option>) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvInventoryItem.WornOn") + self.native_set_worn_on(value) } } /// C# type: `T:LibreMetaverse.RLV.RlvPermissionsService`. -pub struct RlvPermissionsService; +pub use crate::state::RlvPermissionsService; impl RlvPermissionsService { /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanAllowIdle`. pub fn can_allow_idle(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanAllowIdle", - ) + self.native_can_allow_idle() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanAlwaysRun`. pub fn can_always_run(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanAlwaysRun", - ) + self.native_can_always_run() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanAttach(LibreMetaverse.RLV.RlvInventoryItem,System.Boolean)`. pub fn can_attach_with_rlv_inventory_item_boolean( @@ -1404,9 +1365,7 @@ impl RlvPermissionsService { item: libremetaverse_rlv::RlvInventoryItem, is_shared: bool, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanAttach(LibreMetaverse.RLV.RlvInventoryItem,System.Boolean)", - ) + self.native_can_attach_with_item(item, is_shared) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanAttach(System.Nullable{System.Guid},System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})`. pub fn can_attach_with_nullable_boolean_nullable_nullable( @@ -1416,48 +1375,34 @@ impl RlvPermissionsService { attachment_point: Option>, wearable_type: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanAttach(System.Nullable{System.Guid},System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})", - ) + self.native_can_attach(object_folder_id, is_shared, attachment_point, wearable_type) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanChat(System.Int32,System.String)`. pub fn can_chat(&self, channel: i32, message: String) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanChat(System.Int32,System.String)", - ) + self.native_can_chat(channel, message) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanChatNormal`. pub fn can_chat_normal(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanChatNormal", - ) + self.native_can_chat_normal() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanChatShout`. pub fn can_chat_shout(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanChatShout", - ) + self.native_can_chat_shout() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanChatWhisper`. pub fn can_chat_whisper(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanChatWhisper", - ) + self.native_can_chat_whisper() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanDefaultWear`. pub fn can_default_wear(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanDefaultWear", - ) + self.native_can_default_wear() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanDetach(LibreMetaverse.RLV.RlvInventoryItem)`. pub fn can_detach_with_rlv_inventory_item( &self, item: libremetaverse_rlv::RlvInventoryItem, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanDetach(LibreMetaverse.RLV.RlvInventoryItem)", - ) + self.native_can_detach_with_item(item) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanDetach(System.Nullable{System.Guid},System.Nullable{System.Guid},System.Nullable{System.Guid},System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})`. pub fn can_detach_with_nullable_nullable_nullable_boolean_nullable_nullable( @@ -1469,8 +1414,13 @@ impl RlvPermissionsService { attachment_point: Option>, wearable_type: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanDetach(System.Nullable{System.Guid},System.Nullable{System.Guid},System.Nullable{System.Guid},System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{LibreMetaverse.RLV.RlvWearableType})", + self.native_can_detach( + item_id, + prim_id, + folder_id, + is_shared, + attachment_point, + wearable_type, ) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanEdit(LibreMetaverse.RLV.RlvPermissionsService.ObjectLocation,System.Nullable{System.Guid})`. @@ -1479,27 +1429,23 @@ impl RlvPermissionsService { object_location: libremetaverse_rlv::RlvPermissionsServiceObjectLocation, object_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanEdit(LibreMetaverse.RLV.RlvPermissionsService.ObjectLocation,System.Nullable{System.Guid})", - ) + self.native_can_edit(object_location, object_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanEmote`. pub fn can_emote(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanEmote") + self.native_can_emote() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanFly`. pub fn can_fly(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanFly") + self.native_can_fly() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanInteract`. pub fn can_interact(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanInteract", - ) + self.native_can_interact() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanJump`. pub fn can_jump(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanJump") + self.native_can_jump() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanReceiveChat(System.String,System.Guid)`. pub fn can_receive_chat( @@ -1507,9 +1453,7 @@ impl RlvPermissionsService { message: String, user_id: libremetaverse_types::compat::Guid, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanReceiveChat(System.String,System.Guid)", - ) + self.native_can_receive_chat(message, user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanReceiveIM(System.String,System.Nullable{System.Guid},System.String)`. pub fn can_receive_im( @@ -1518,25 +1462,19 @@ impl RlvPermissionsService { user_id: Option>, group_name: Option, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanReceiveIM(System.String,System.Nullable{System.Guid},System.String)", - ) + self.native_can_receive_im(message, user_id, group_name) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanRez`. pub fn can_rez(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanRez") + self.native_can_rez() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSendChat`. pub fn can_send_chat(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSendChat", - ) + self.native_can_send_chat() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSendGesture`. pub fn can_send_gesture(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSendGesture", - ) + self.native_can_send_gesture() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSendIM(System.String,System.Nullable{System.Guid},System.String)`. pub fn can_send_im( @@ -1545,36 +1483,26 @@ impl RlvPermissionsService { user_id: Option>, group_name: Option, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSendIM(System.String,System.Nullable{System.Guid},System.String)", - ) + self.native_can_send_im(message, user_id, group_name) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSetDebug`. pub fn can_set_debug(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSetDebug", - ) + self.native_can_set_debug() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSetEnv`. pub fn can_set_env(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSetEnv", - ) + self.native_can_set_env() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSetGroup`. pub fn can_set_group(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSetGroup", - ) + self.native_can_set_group() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShare(System.Nullable{System.Guid})`. pub fn can_share( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShare(System.Nullable{System.Guid})", - ) + self.native_can_share(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowHoverText(LibreMetaverse.RLV.RlvPermissionsService.HoverTextLocation,System.Nullable{System.Guid})`. pub fn can_show_hover_text( @@ -1582,97 +1510,71 @@ impl RlvPermissionsService { location: libremetaverse_rlv::RlvPermissionsServiceHoverTextLocation, object_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowHoverText(LibreMetaverse.RLV.RlvPermissionsService.HoverTextLocation,System.Nullable{System.Guid})", - ) + self.native_can_show_hover_text(location, object_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowInv`. pub fn can_show_inv(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowInv", - ) + self.native_can_show_inv() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowLoc`. pub fn can_show_loc(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowLoc", - ) + self.native_can_show_loc() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowMiniMap`. pub fn can_show_mini_map(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowMiniMap", - ) + self.native_can_show_mini_map() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNameTags(System.Nullable{System.Guid})`. pub fn can_show_name_tags( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNameTags(System.Nullable{System.Guid})", - ) + self.native_can_show_name_tags(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNames(System.Nullable{System.Guid})`. pub fn can_show_names( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNames(System.Nullable{System.Guid})", - ) + self.native_can_show_names(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNearby`. pub fn can_show_nearby(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowNearby", - ) + self.native_can_show_nearby() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanShowWorldMap`. pub fn can_show_world_map(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanShowWorldMap", - ) + self.native_can_show_world_map() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSit`. pub fn can_sit(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanSit") + self.native_can_sit() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanSitTp(System.Single@)`. pub fn can_sit_tp(&self, max_distance: &mut f32) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanSitTp(System.Single@)", - ) + self.native_can_sit_tp(max_distance) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanStandTp`. pub fn can_stand_tp(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanStandTp", - ) + self.native_can_stand_tp() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanStartIM(System.Guid)`. pub fn can_start_im( &self, user_id: libremetaverse_types::compat::Guid, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanStartIM(System.Guid)", - ) + self.native_can_start_im(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTPLure(System.Nullable{System.Guid})`. pub fn can_tp_lure( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanTPLure(System.Nullable{System.Guid})", - ) + self.native_can_tp_lure(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTempRun`. pub fn can_temp_run(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanTempRun", - ) + self.native_can_temp_run() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTouch(LibreMetaverse.RLV.RlvPermissionsService.TouchLocation,System.Guid,System.Nullable{System.Guid},System.Nullable{System.Single})`. pub fn can_touch( @@ -1682,116 +1584,86 @@ impl RlvPermissionsService { user_id: Option>, distance: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanTouch(LibreMetaverse.RLV.RlvPermissionsService.TouchLocation,System.Guid,System.Nullable{System.Guid},System.Nullable{System.Single})", - ) + self.native_can_touch(location, prim_id, user_id, distance) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLm`. pub fn can_tp_lm(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLm") + self.native_can_tp_lm() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLoc`. pub fn can_tp_loc(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLoc") + self.native_can_tp_loc() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLocal(System.Single@)`. pub fn can_tp_local(&self, max_distance: &mut f32) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanTpLocal(System.Single@)", - ) + self.native_can_tp_local(max_distance) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanTpRequest(System.Nullable{System.Guid})`. pub fn can_tp_request( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanTpRequest(System.Nullable{System.Guid})", - ) + self.native_can_tp_request(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanUnsit`. pub fn can_unsit(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvPermissionsService.CanUnsit") + self.native_can_unsit() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanViewNote`. pub fn can_view_note(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanViewNote", - ) + self.native_can_view_note() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanViewScript`. pub fn can_view_script(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanViewScript", - ) + self.native_can_view_script() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.CanViewTexture`. pub fn can_view_texture(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.CanViewTexture", - ) + self.native_can_view_texture() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.GetCameraRestrictions`. pub fn get_camera_restrictions( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.GetCameraRestrictions", - ) + self.native_get_camera_restrictions() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptPermissions`. pub fn is_auto_accept_permissions(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptPermissions", - ) + self.native_is_auto_accept_permissions() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptTp(System.Nullable{System.Guid})`. pub fn is_auto_accept_tp( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptTp(System.Nullable{System.Guid})", - ) + self.native_is_auto_accept_tp(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptTpRequest(System.Nullable{System.Guid})`. pub fn is_auto_accept_tp_request( &self, user_id: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoAcceptTpRequest(System.Nullable{System.Guid})", - ) + self.native_is_auto_accept_tp_request(user_id) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoDenyPermissions`. pub fn is_auto_deny_permissions(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.IsAutoDenyPermissions", - ) + self.native_is_auto_deny_permissions() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.IsPermissive`. pub fn is_permissive(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvPermissionsService.IsPermissive", - ) + self.native_is_permissive() } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.TryGetMaxFarTouchDistance(System.Single@)`. pub fn try_get_max_far_touch_distance(&self, max_distance: &mut f32) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.RlvPermissionsService.TryGetMaxFarTouchDistance(System.Single@)" - ) + self.native_try_get_max_far_touch_distance(max_distance) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.TryGetRedirChatChannels(System.Collections.Generic.IReadOnlyList{System.Int32}@)`. pub fn try_get_redir_chat_channels(&self, channels: &mut Vec) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.RlvPermissionsService.TryGetRedirChatChannels(System.Collections.Generic.IReadOnlyList{System.Int32}@)" - ) + self.native_try_get_redir_chat_channels(channels) } /// C# member: `M:LibreMetaverse.RLV.RlvPermissionsService.TryGetRedirEmoteChannels(System.Collections.Generic.IReadOnlyList{System.Int32}@)`. pub fn try_get_redir_emote_channels(&self, channels: &mut Vec) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.RlvPermissionsService.TryGetRedirEmoteChannels(System.Collections.Generic.IReadOnlyList{System.Int32}@)" - ) + self.native_try_get_redir_emote_channels(channels) } } @@ -1884,7 +1756,7 @@ impl RlvRestriction { } /// C# type: `T:LibreMetaverse.RLV.RlvRestrictionManager`. -pub struct RlvRestrictionManager; +pub use crate::state::RlvRestrictionManager; impl RlvRestrictionManager { /// C# member: `E:LibreMetaverse.RLV.RlvRestrictionManager.RestrictionUpdated`. pub fn subscribe_restriction_updated( @@ -1895,9 +1767,7 @@ impl RlvRestrictionManager { >, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!( - "E:LibreMetaverse.RLV.RlvRestrictionManager.RestrictionUpdated" - ) + self.native_subscribe_restriction_updated(handler) } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.FindRestrictions(System.String,System.Nullable{System.Guid})`. pub fn find_restrictions( @@ -1905,9 +1775,7 @@ impl RlvRestrictionManager { behavior_name_filter: Option, sender_filter: Option>, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvRestrictionManager.FindRestrictions(System.String,System.Nullable{System.Guid})", - ) + self.native_find_restrictions(behavior_name_filter, sender_filter) } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.GetLockedFolders`. pub fn get_locked_folders( @@ -1919,26 +1787,20 @@ impl RlvRestrictionManager { >, crate::Error, > { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvRestrictionManager.GetLockedFolders", - ) + self.native_get_locked_folders() } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.GetRestrictionsByType(LibreMetaverse.RLV.RlvRestrictionType)`. pub fn get_restrictions_by_type( &self, restriction_type: libremetaverse_rlv::RlvRestrictionType, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvRestrictionManager.GetRestrictionsByType(LibreMetaverse.RLV.RlvRestrictionType)", - ) + self.native_get_restrictions_by_type(restriction_type) } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.GetTrackedPrimIds`. pub fn get_tracked_prim_ids( &self, ) -> Result>, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvRestrictionManager.GetTrackedPrimIds", - ) + self.native_get_tracked_prim_ids() } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.RemoveRestrictionsForObjectsAsync(System.Collections.Generic.IEnumerable{System.Guid},System.Threading.CancellationToken)`. pub async fn remove_restrictions_for_objects( @@ -1946,9 +1808,8 @@ impl RlvRestrictionManager { prim_ids: Box>, cancellation_token: Option, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvRestrictionManager.RemoveRestrictionsForObjectsAsync(System.Collections.Generic.IEnumerable{System.Guid},System.Threading.CancellationToken)", - ) + self.native_remove_restrictions_for_objects(prim_ids, cancellation_token) + .await } /// C# member: `M:LibreMetaverse.RLV.RlvRestrictionManager.TryGetLockedFolder(System.Guid,LibreMetaverse.RLV.LockedFolderPublic@)`. pub fn try_get_locked_folder( @@ -1956,9 +1817,7 @@ impl RlvRestrictionManager { folder_id: libremetaverse_types::compat::Guid, locked_folder: &mut Option, ) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.RlvRestrictionManager.TryGetLockedFolder(System.Guid,LibreMetaverse.RLV.LockedFolderPublic@)" - ) + self.native_try_get_locked_folder(folder_id, locked_folder) } } @@ -2386,13 +2245,11 @@ impl RlvService { } /// C# type: `T:LibreMetaverse.RLV.RlvSharedFolder`. -pub struct RlvSharedFolder; +pub use crate::state::RlvSharedFolder; impl RlvSharedFolder { /// C# member: `M:LibreMetaverse.RLV.RlvSharedFolder.#ctor(System.Guid,System.String)`. pub fn new(id: libremetaverse_types::compat::Guid, name: String) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvSharedFolder.#ctor(System.Guid,System.String)", - ) + Self::native_new(id, name) } /// C# member: `M:LibreMetaverse.RLV.RlvSharedFolder.AddChild(System.Guid,System.String)`. pub fn add_child( @@ -2400,9 +2257,7 @@ impl RlvSharedFolder { id: libremetaverse_types::compat::Guid, name: String, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvSharedFolder.AddChild(System.Guid,System.String)", - ) + self.native_add_child(id, name) } /// C# member: `M:LibreMetaverse.RLV.RlvSharedFolder.AddItem(System.Guid,System.String,System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvWearableType},System.Nullable{LibreMetaverse.RLV.RlvGestureState})`. pub fn add_item( @@ -2415,41 +2270,47 @@ impl RlvSharedFolder { worn_on: Option>, gesture_state: Option>, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvSharedFolder.AddItem(System.Guid,System.String,System.Boolean,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint},System.Nullable{System.Guid},System.Nullable{LibreMetaverse.RLV.RlvWearableType},System.Nullable{LibreMetaverse.RLV.RlvGestureState})", + self.native_add_item( + id, + name, + is_link, + attached_to, + attached_prim_id, + worn_on, + gesture_state, ) } /// C# member: `M:LibreMetaverse.RLV.RlvSharedFolder.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.RLV.RlvSharedFolder.ToString") + self.native_to_string() } /// C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Children`. pub fn children(&self) -> Vec { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Children") + self.native_children() } /// C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Id`. pub fn id(&self) -> libremetaverse_types::compat::Guid { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Id") + self.native_id() } /// C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Items`. pub fn items(&self) -> Vec { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Items") + self.native_items() } /// C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Name`. pub fn name(&self) -> String { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Name") + self.native_name() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Name`. pub fn set_name(&mut self, value: String) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Name") + self.native_set_name(value) } /// C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Parent`. pub fn parent(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Parent") + self.native_parent() } /// Setter for C# member: `P:LibreMetaverse.RLV.RlvSharedFolder.Parent`. pub fn set_parent(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvSharedFolder.Parent") + self.native_set_parent(value) } } @@ -2497,7 +2358,7 @@ pub enum RlvWearableType { pub mod event_arguments { /// C# type: `T:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs`. - pub struct RestrictionUpdatedEventArgs; + pub use crate::state::RestrictionUpdatedEventArgs; impl RestrictionUpdatedEventArgs { /// C# member: `M:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.#ctor(LibreMetaverse.RLV.RlvRestriction,System.Boolean,System.Boolean)`. pub fn new( @@ -2505,27 +2366,19 @@ pub mod event_arguments { is_new: bool, is_deleted: bool, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.#ctor(LibreMetaverse.RLV.RlvRestriction,System.Boolean,System.Boolean)", - ) + Self::native_new(restriction, is_new, is_deleted) } /// C# member: `P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.IsDeleted`. pub fn is_deleted(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.IsDeleted" - ) + self.native_is_deleted() } /// C# member: `P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.IsNew`. pub fn is_new(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.IsNew" - ) + self.native_is_new() } /// C# member: `P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.Restriction`. pub fn restriction(&self) -> libremetaverse_rlv::RlvRestriction { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs.Restriction" - ) + self.native_restriction() } } } diff --git a/crates/libremetaverse-rlv/src/lib.rs b/crates/libremetaverse-rlv/src/lib.rs index dea1d19..4d7ae65 100644 --- a/crates/libremetaverse-rlv/src/lib.rs +++ b/crates/libremetaverse-rlv/src/lib.rs @@ -4,7 +4,9 @@ extern crate self as libremetaverse_rlv; mod generated; mod protocol; +mod state; pub use generated::*; pub use libremetaverse_types::Error; pub use protocol::*; +pub use state::*; diff --git a/crates/libremetaverse-rlv/src/state.rs b/crates/libremetaverse-rlv/src/state.rs new file mode 100644 index 0000000..1c9bac9 --- /dev/null +++ b/crates/libremetaverse-rlv/src/state.rs @@ -0,0 +1,2174 @@ +//! Pure, synchronized RLV restriction, inventory, lock, and permission state. + +#![allow(clippy::missing_errors_doc)] // Mapped Result signatures use the shared compatibility error. +#![allow(clippy::must_use_candidate)] // Mapped property signatures intentionally mirror C#. +#![allow(clippy::needless_pass_by_value)] // Owned arguments preserve mapped signatures. +#![allow(clippy::option_option)] // Nullable value types retain the generated three-state signature. +#![allow(clippy::unnecessary_wraps)] // Constructors retain mapped fallibility. + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt; +use std::sync::{ + Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak, + atomic::{AtomicU64, Ordering}, +}; + +use libremetaverse_types::compat::{ + CancellationToken, EventHandler, Guid, ImmutableDictionary, ImmutableList, Subscription, + Vector3, +}; +use libremetaverse_types::{Error, UUID}; + +use crate::{ + RlvAttachmentPoint, RlvGestureState, RlvPermissionsServiceHoverTextLocation, + RlvPermissionsServiceObjectLocation, RlvPermissionsServiceTouchLocation, RlvRestriction, + RlvRestrictionType, RlvValue, RlvWearableType, restriction_name, +}; + +/// Maximum number of folders accepted in one immutable inventory snapshot. +pub const MAX_RLV_INVENTORY_FOLDERS: usize = 100_000; +/// Maximum number of internal and external items accepted in one snapshot. +pub const MAX_RLV_INVENTORY_ITEMS: usize = 1_000_000; +/// Maximum number of simultaneously retained restrictions. +pub const MAX_RLV_RESTRICTIONS: usize = 100_000; + +fn read_lock(lock: &RwLock) -> RwLockReadGuard<'_, T> { + lock.read() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn write_lock(lock: &RwLock) -> RwLockWriteGuard<'_, T> { + lock.write() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn nullable(value: Option>) -> Option { + value.flatten() +} + +fn mapped_nullable(value: Option) -> Option> { + Some(value) +} + +fn starts_with_ignore_case(value: &str, prefix: &str) -> bool { + value + .get(..prefix.len()) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix)) + || value.to_lowercase().starts_with(&prefix.to_lowercase()) +} + +#[derive(Debug)] +struct FolderData { + id: Guid, + name: String, + parent: Option>>, + children: Vec, + items: Vec, +} + +/// Shared RLV inventory folder with reference semantics and short-held locks. +#[derive(Clone)] +pub struct RlvSharedFolder { + inner: Arc>, +} + +impl fmt::Debug for RlvSharedFolder { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RlvSharedFolder") + .field("id", &self.native_id()) + .field("name", &self.native_name()) + .finish_non_exhaustive() + } +} + +impl RlvSharedFolder { + pub(crate) fn native_new(id: Guid, name: String) -> Result { + Ok(Self { + inner: Arc::new(RwLock::new(FolderData { + id, + name, + parent: None, + children: Vec::new(), + items: Vec::new(), + })), + }) + } + + pub(crate) fn native_add_child(&self, id: Guid, name: String) -> Result { + if name.is_empty() { + return Err(Error::Argument); + } + let child = Self { + inner: Arc::new(RwLock::new(FolderData { + id, + name, + parent: Some(Arc::downgrade(&self.inner)), + children: Vec::new(), + items: Vec::new(), + })), + }; + write_lock(&self.inner).children.push(child.clone()); + Ok(child) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn native_add_item( + &self, + id: Guid, + name: String, + is_link: bool, + attached_to: Option>, + attached_prim_id: Option>, + worn_on: Option>, + gesture_state: Option>, + ) -> Result { + let item = RlvInventoryItem::new_internal( + id, + name, + is_link, + self, + nullable(attached_to), + nullable(attached_prim_id), + nullable(worn_on), + nullable(gesture_state), + )?; + write_lock(&self.inner).items.push(item.clone()); + Ok(item) + } + + pub(crate) fn native_to_string(&self) -> String { + let data = read_lock(&self.inner); + format!( + "{} (Children: {}, Items: {})", + data.name, + data.children.len(), + data.items.len() + ) + } + + pub(crate) fn native_children(&self) -> Vec { + read_lock(&self.inner).children.clone() + } + + pub(crate) fn native_id(&self) -> Guid { + read_lock(&self.inner).id + } + + pub(crate) fn native_items(&self) -> Vec { + read_lock(&self.inner).items.clone() + } + + pub(crate) fn native_name(&self) -> String { + read_lock(&self.inner).name.clone() + } + + pub(crate) fn native_set_name(&mut self, value: String) { + write_lock(&self.inner).name = value; + } + + pub(crate) fn native_parent(&self) -> Option { + read_lock(&self.inner) + .parent + .as_ref() + .and_then(Weak::upgrade) + .map(|inner| Self { inner }) + } + + pub(crate) fn native_set_parent(&mut self, value: Option) { + write_lock(&self.inner).parent = value.map(|parent| Arc::downgrade(&parent.inner)); + } +} + +#[derive(Debug)] +struct InventoryItemData { + id: Guid, + name: String, + is_link: bool, + folder: Option>>, + folder_id: Option, + attached_to: Option, + attached_prim_id: Option, + worn_on: Option, + gesture_state: Option, +} + +/// Shared mutable inventory item corresponding to the C# reference type. +#[derive(Clone)] +pub struct RlvInventoryItem { + inner: Arc>, +} + +impl fmt::Debug for RlvInventoryItem { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RlvInventoryItem") + .field("id", &self.native_id()) + .field("name", &self.native_name()) + .finish_non_exhaustive() + } +} + +impl RlvInventoryItem { + #[allow(clippy::too_many_arguments)] + pub(crate) fn native_new( + id: Guid, + name: String, + is_link: bool, + external_folder_id: Option>, + attached_to: Option>, + attached_prim_id: Option>, + worn_on: Option>, + gesture_state: Option>, + ) -> Result { + if name.is_empty() { + return Err(Error::Argument); + } + Ok(Self { + inner: Arc::new(RwLock::new(InventoryItemData { + id, + name, + is_link, + folder: None, + folder_id: nullable(external_folder_id), + attached_to: nullable(attached_to), + attached_prim_id: nullable(attached_prim_id), + worn_on: nullable(worn_on), + gesture_state: nullable(gesture_state), + })), + }) + } + + #[allow(clippy::too_many_arguments)] + fn new_internal( + id: Guid, + name: String, + is_link: bool, + folder: &RlvSharedFolder, + attached_to: Option, + attached_prim_id: Option, + worn_on: Option, + gesture_state: Option, + ) -> Result { + if name.is_empty() { + return Err(Error::Argument); + } + Ok(Self { + inner: Arc::new(RwLock::new(InventoryItemData { + id, + name, + is_link, + folder: Some(Arc::downgrade(&folder.inner)), + folder_id: Some(folder.native_id()), + attached_to, + attached_prim_id, + worn_on, + gesture_state, + })), + }) + } + + pub(crate) fn native_to_string(&self) -> String { + self.native_name() + } + + pub(crate) fn native_attached_prim_id(&self) -> Option> { + mapped_nullable(read_lock(&self.inner).attached_prim_id) + } + + pub(crate) fn native_set_attached_prim_id(&mut self, value: Option>) { + write_lock(&self.inner).attached_prim_id = nullable(value); + } + + pub(crate) fn native_attached_to(&self) -> Option> { + mapped_nullable(read_lock(&self.inner).attached_to) + } + + pub(crate) fn native_set_attached_to(&mut self, value: Option>) { + write_lock(&self.inner).attached_to = nullable(value); + } + + pub(crate) fn native_folder(&self) -> Option { + read_lock(&self.inner) + .folder + .as_ref() + .and_then(Weak::upgrade) + .map(|inner| RlvSharedFolder { inner }) + } + + pub(crate) fn native_folder_id(&self) -> Option> { + mapped_nullable(read_lock(&self.inner).folder_id) + } + + pub(crate) fn native_gesture_state(&self) -> Option> { + mapped_nullable(read_lock(&self.inner).gesture_state) + } + + pub(crate) fn native_set_gesture_state(&mut self, value: Option>) { + write_lock(&self.inner).gesture_state = nullable(value); + } + + pub(crate) fn native_id(&self) -> Guid { + read_lock(&self.inner).id + } + + pub(crate) fn native_is_link(&self) -> bool { + read_lock(&self.inner).is_link + } + + pub(crate) fn native_set_is_link(&mut self, value: bool) { + write_lock(&self.inner).is_link = value; + } + + pub(crate) fn native_name(&self) -> String { + read_lock(&self.inner).name.clone() + } + + pub(crate) fn native_set_name(&mut self, value: String) { + write_lock(&self.inner).name = value; + } + + pub(crate) fn native_worn_on(&self) -> Option> { + mapped_nullable(read_lock(&self.inner).worn_on) + } + + pub(crate) fn native_set_worn_on(&mut self, value: Option>) { + write_lock(&self.inner).worn_on = nullable(value); + } + + pub fn attached_prim_id_value(&self) -> Option { + read_lock(&self.inner).attached_prim_id + } + + pub fn attached_to_value(&self) -> Option { + read_lock(&self.inner).attached_to + } + + pub fn folder_id_value(&self) -> Option { + read_lock(&self.inner).folder_id + } + + pub fn gesture_state_value(&self) -> Option { + read_lock(&self.inner).gesture_state + } + + pub fn worn_on_value(&self) -> Option { + read_lock(&self.inner).worn_on + } +} + +/// Immutable index over one shared-folder tree and external worn items. +#[derive(Clone, Debug)] +pub struct InventoryMap { + root: RlvSharedFolder, + items: ImmutableDictionary>, + external_items: ImmutableDictionary, + folders: ImmutableDictionary, + all_items: Arc<[RlvInventoryItem]>, + external_ordered: Arc<[RlvInventoryItem]>, +} + +impl InventoryMap { + pub(crate) fn native_new( + root: RlvSharedFolder, + external_items: Vec, + ) -> Result { + let mut folders = HashMap::new(); + let mut items: HashMap> = HashMap::new(); + let mut all_items = Vec::new(); + // The pinned compatibility surface observes child-folder matches + // before parent-folder matches for the deprecated single-result query. + // An explicit post-order stack preserves that behavior without + // recursive call-stack growth. + let mut stack = vec![(root.clone(), false)]; + while let Some((folder, expanded)) = stack.pop() { + if expanded { + let folder_items = folder.native_items(); + if all_items.len().saturating_add(folder_items.len()) > MAX_RLV_INVENTORY_ITEMS { + return Err(Error::Argument); + } + for item in folder_items { + items + .entry(item.native_id()) + .or_default() + .push(item.clone()); + all_items.push(item); + } + continue; + } + if folders.contains_key(&folder.native_id()) { + continue; + } + if folders.len() >= MAX_RLV_INVENTORY_FOLDERS { + return Err(Error::Argument); + } + folders.insert(folder.native_id(), folder.clone()); + stack.push((folder.clone(), true)); + let mut children = folder.native_children(); + children.reverse(); + stack.extend(children.into_iter().map(|child| (child, false))); + } + if all_items.len().saturating_add(external_items.len()) > MAX_RLV_INVENTORY_ITEMS { + return Err(Error::Argument); + } + let mut external_map = HashMap::new(); + for item in &external_items { + if external_map + .insert(item.native_id(), item.clone()) + .is_some() + { + return Err(Error::Argument); + } + } + let items = items + .into_iter() + .map(|(id, values)| (id, values.into())) + .collect(); + Ok(Self { + root, + items: ImmutableDictionary::from_map(items), + external_items: ImmutableDictionary::from_map(external_map), + folders: ImmutableDictionary::from_map(folders), + all_items: all_items.into(), + external_ordered: external_items.into(), + }) + } + + pub(crate) fn native_find_folders_containing( + &self, + limit_to_one_result: bool, + attached_prim_id: Option>, + attachment_point: Option>, + wearable_type: Option>, + ) -> Result>, Error> { + let attached_prim_id = nullable(attached_prim_id); + let attachment_point = nullable(attachment_point); + let wearable_type = nullable(wearable_type); + let found = if let Some(prim_id) = attached_prim_id { + self.items_by(|item| item.attached_prim_id_value() == Some(prim_id)) + } else if let Some(point) = attachment_point { + self.items_by(|item| item.attached_to_value() == Some(point)) + } else if let Some(wearable) = wearable_type { + self.items_by(|item| item.worn_on_value() == Some(wearable)) + } else { + Vec::new() + }; + let mut seen = HashSet::new(); + let mut folders = Vec::new(); + for item in found { + let Some(folder) = item.native_folder() else { + return Ok(Box::new(Vec::new().into_iter())); + }; + if seen.insert(folder.native_id()) { + folders.push(folder); + if limit_to_one_result { + break; + } + } + } + Ok(Box::new(folders.into_iter())) + } + + pub(crate) fn native_get_items_by_attachment_point( + &self, + attachment_point: RlvAttachmentPoint, + ) -> Result, Error> { + Ok(self.items_by(|item| item.attached_to_value() == Some(attachment_point))) + } + + pub(crate) fn native_get_items_by_id( + &self, + item_id: Guid, + ) -> Result, Error> { + let mut found = self + .items + .get(&item_id) + .map_or_else(Vec::new, |values| values.as_slice().to_vec()); + if let Some(item) = self.external_items.get(&item_id) { + found.push(item.clone()); + } + Ok(found) + } + + pub(crate) fn native_get_items_by_prim_id( + &self, + prim_id: Guid, + ) -> Result, Error> { + Ok(self.items_by(|item| item.attached_prim_id_value() == Some(prim_id))) + } + + pub(crate) fn native_get_items_by_wearable_type( + &self, + wearable_type: RlvWearableType, + ) -> Result, Error> { + Ok(self.items_by(|item| item.worn_on_value() == Some(wearable_type))) + } + + pub(crate) fn native_try_build_path_to_folder( + &self, + folder_id: Guid, + final_path: &mut Option, + ) -> bool { + *final_path = None; + let Some(mut folder) = self.folders.get(&folder_id).cloned() else { + return false; + }; + let mut path = Vec::new(); + let mut visited = HashSet::new(); + loop { + if !visited.insert(folder.native_id()) { + return false; + } + let Some(parent) = folder.native_parent() else { + break; + }; + path.push(folder.native_name()); + folder = parent; + } + path.reverse(); + *final_path = Some(path.join("/")); + true + } + + pub(crate) fn native_try_get_folder_from_path( + &self, + path: String, + skip_private_folders: bool, + folder: &mut Option, + ) -> bool { + *folder = None; + if path.is_empty() { + return false; + } + let mut variants = vec![path.as_str()]; + if let Some(value) = path.strip_prefix('/') { + variants.push(value); + } + if let Some(value) = path.strip_suffix('/') { + variants.push(value); + } + if let Some(value) = path + .strip_prefix('/') + .and_then(|value| value.strip_suffix('/')) + { + variants.push(value); + } + let found = variants + .into_iter() + .find_map(|candidate| self.folder_from_path(candidate, skip_private_folders)); + *folder = found; + folder.is_some() + } + + pub(crate) fn native_external_items(&self) -> ImmutableDictionary { + self.external_items.clone() + } + + pub(crate) fn native_folders(&self) -> ImmutableDictionary { + self.folders.clone() + } + + pub(crate) fn native_items( + &self, + ) -> ImmutableDictionary> { + self.items.clone() + } + + pub(crate) fn native_root(&self) -> RlvSharedFolder { + self.root.clone() + } + + pub fn current_outfit(&self) -> Vec { + self.items_by(|item| { + item.worn_on_value().is_some() + || item.attached_to_value().is_some() + || item.gesture_state_value() == Some(RlvGestureState::Active) + }) + } + + fn items_by(&self, predicate: impl Fn(&RlvInventoryItem) -> bool) -> Vec { + self.all_items + .iter() + .chain(self.external_ordered.iter()) + .filter(|item| predicate(item)) + .cloned() + .collect() + } + + fn folder_from_path(&self, path: &str, skip_private: bool) -> Option { + if path.is_empty() { + return None; + } + let mut remaining = path; + let mut current = self.root.clone(); + let mut visited = HashSet::new(); + loop { + if !visited.insert(current.native_id()) { + return None; + } + let mut selected: Option<(RlvSharedFolder, usize, bool)> = None; + for child in current.native_children() { + let name = child.native_name(); + if skip_private && name.starts_with('.') { + continue; + } + let exact = starts_with_ignore_case(remaining, &name); + let prefixed = + !exact && matches!(name.as_bytes().first(), Some(b'.' | b'~' | b'+')); + let fixed = if prefixed { &name[1..] } else { &name }; + if !starts_with_ignore_case(remaining, fixed) { + continue; + } + let length = fixed.len(); + let valid_boundary = remaining.len() == length + || remaining.as_bytes().get(length).copied() == Some(b'/'); + if !valid_boundary { + continue; + } + let replace = selected + .as_ref() + .is_none_or(|(_, best_len, best_prefixed)| { + length > *best_len || (length == *best_len && !prefixed && *best_prefixed) + }); + if replace { + selected = Some((child, length, prefixed)); + if remaining.len() == length && !prefixed { + break; + } + } + } + let (folder, consumed, _) = selected?; + if remaining.len() == consumed { + return Some(folder); + } + remaining = remaining.get(consumed + 1..)?; + current = folder; + } + } +} + +/// Thread-safe, case-insensitive behavior blacklist. +#[derive(Clone, Debug, Default)] +pub struct RlvBlacklist { + entries: Arc>>, +} + +impl RlvBlacklist { + pub fn new() -> Self { + Self::default() + } + + pub(crate) fn native_blacklist_behavior(&self, behavior: String) -> Result<(), Error> { + if behavior.trim().is_empty() { + return Err(Error::Argument); + } + write_lock(&self.entries).insert(behavior.to_lowercase()); + Ok(()) + } + + pub(crate) fn native_get_blacklist(&self) -> Result, Error> { + Ok(read_lock(&self.entries).iter().cloned().collect()) + } + + pub(crate) fn native_is_blacklisted(&self, behavior: String) -> Result { + if behavior.trim().is_empty() { + return Ok(false); + } + Ok(read_lock(&self.entries).contains(&behavior.to_lowercase())) + } + + pub(crate) fn native_un_blacklist_behavior(&self, behavior: String) -> Result<(), Error> { + if behavior.trim().is_empty() { + return Err(Error::Argument); + } + write_lock(&self.entries).remove(&behavior.to_lowercase()); + Ok(()) + } +} + +/// Snapshot of effective camera restrictions. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CameraRestrictions { + is_locked: bool, + zoom_min: Option, + zoom_max: Option, + draw_min: Option, + draw_max: Option, + draw_alpha_min: Option, + draw_alpha_max: Option, + fov_min: Option, + fov_max: Option, + av_dist_min: Option, + av_dist_max: Option, + av_dist: Option, + draw_color: Option, + texture: Option, +} + +impl CameraRestrictions { + fn from_manager(manager: &RlvRestrictionManager) -> Self { + let maximum = |kind| manager.float_values(kind).into_iter().reduce(f32::max); + let minimum = |kind| manager.float_values(kind).into_iter().reduce(f32::min); + let color_values: Vec<[f32; 3]> = manager + .restrictions_by_type(RlvRestrictionType::CamDrawColor) + .into_iter() + .filter_map(|restriction| match restriction.values() { + [RlvValue::Real(x), RlvValue::Real(y), RlvValue::Real(z)] => { + Some([x.clamp(0.0, 1.0), y.clamp(0.0, 1.0), z.clamp(0.0, 1.0)]) + } + _ => None, + }) + .collect(); + let draw_color = (!color_values.is_empty()).then(|| { + let (sums, divisor) = + color_values + .iter() + .fold(([0.0; 3], 0.0_f32), |(mut sums, count), color| { + sums[0] += color[0]; + sums[1] += color[1]; + sums[2] += color[2]; + (sums, count + 1.0) + }); + Vector3([sums[0] / divisor, sums[1] / divisor, sums[2] / divisor]) + }); + let texture = manager + .restrictions_by_type(RlvRestrictionType::SetCamTextures) + .last() + .and_then(|restriction| match restriction.values() { + [] => Some(Guid([0; 16])), + [RlvValue::Uuid(value)] => Some(*value), + _ => None, + }); + Self { + zoom_min: maximum(RlvRestrictionType::CamZoomMin), + fov_min: maximum(RlvRestrictionType::SetCamFovMin), + av_dist_min: maximum(RlvRestrictionType::SetCamAvDistMin), + zoom_max: minimum(RlvRestrictionType::CamZoomMax), + draw_min: minimum(RlvRestrictionType::CamDrawMin), + draw_max: minimum(RlvRestrictionType::CamDrawMax), + draw_alpha_min: minimum(RlvRestrictionType::CamDrawAlphaMin), + draw_alpha_max: minimum(RlvRestrictionType::CamDrawAlphaMax), + fov_max: minimum(RlvRestrictionType::SetCamFovMax), + av_dist_max: minimum(RlvRestrictionType::SetCamAvDistMax), + av_dist: minimum(RlvRestrictionType::CamAvDist), + draw_color, + texture, + is_locked: !manager + .restrictions_by_type(RlvRestrictionType::SetCamUnlock) + .is_empty(), + } + } + + pub(crate) fn native_av_dist(&self) -> Option> { + mapped_nullable(self.av_dist) + } + pub(crate) fn native_av_dist_max(&self) -> Option> { + mapped_nullable(self.av_dist_max) + } + pub(crate) fn native_av_dist_min(&self) -> Option> { + mapped_nullable(self.av_dist_min) + } + pub(crate) fn native_draw_alpha_max(&self) -> Option> { + mapped_nullable(self.draw_alpha_max) + } + pub(crate) fn native_draw_alpha_min(&self) -> Option> { + mapped_nullable(self.draw_alpha_min) + } + pub(crate) fn native_draw_color(&self) -> Option> { + mapped_nullable(self.draw_color) + } + pub(crate) fn native_draw_max(&self) -> Option> { + mapped_nullable(self.draw_max) + } + pub(crate) fn native_draw_min(&self) -> Option> { + mapped_nullable(self.draw_min) + } + pub(crate) fn native_fov_max(&self) -> Option> { + mapped_nullable(self.fov_max) + } + pub(crate) fn native_fov_min(&self) -> Option> { + mapped_nullable(self.fov_min) + } + pub(crate) fn native_is_locked(&self) -> bool { + self.is_locked + } + pub(crate) fn native_texture(&self) -> Option> { + mapped_nullable(self.texture) + } + pub(crate) fn native_zoom_max(&self) -> Option> { + mapped_nullable(self.zoom_max) + } + pub(crate) fn native_zoom_min(&self) -> Option> { + mapped_nullable(self.zoom_min) + } +} + +/// Immutable current viewer camera settings supplied to query handlers. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CameraSettings { + av_dist_min: f32, + av_dist_max: f32, + fov_min: f32, + fov_max: f32, + zoom_min: f32, + current_fov: f32, +} + +impl CameraSettings { + pub(crate) fn native_new( + av_dist_min: f32, + av_dist_max: f32, + fov_min: f32, + fov_max: f32, + zoom_min: f32, + current_fov: f32, + ) -> Result { + Ok(Self { + av_dist_min, + av_dist_max, + fov_min, + fov_max, + zoom_min, + current_fov, + }) + } + + pub(crate) fn native_av_dist_max(&self) -> f32 { + self.av_dist_max + } + pub(crate) fn native_av_dist_min(&self) -> f32 { + self.av_dist_min + } + pub(crate) fn native_current_fov(&self) -> f32 { + self.current_fov + } + pub(crate) fn native_fov_max(&self) -> f32 { + self.fov_max + } + pub(crate) fn native_fov_min(&self) -> f32 { + self.fov_min + } + pub(crate) fn native_zoom_min(&self) -> f32 { + self.zoom_min + } +} + +/// Immutable event payload emitted after a restriction state update. +#[derive(Clone, Debug)] +pub struct RestrictionUpdatedEventArgs { + restriction: RlvRestriction, + is_new: bool, + is_deleted: bool, +} + +impl RestrictionUpdatedEventArgs { + pub(crate) fn native_new( + restriction: RlvRestriction, + is_new: bool, + is_deleted: bool, + ) -> Result { + Ok(Self { + restriction, + is_new, + is_deleted, + }) + } + + pub(crate) fn native_is_deleted(&self) -> bool { + self.is_deleted + } + + pub(crate) fn native_is_new(&self) -> bool { + self.is_new + } + + pub(crate) fn native_restriction(&self) -> RlvRestriction { + self.restriction.clone() + } +} + +#[derive(Clone, Debug, Default)] +struct LockedFolderAccumulator { + folder: Option, + detach_restrictions: Vec, + attach_restrictions: Vec, + detach_exceptions: Vec, + attach_exceptions: Vec, +} + +/// Public immutable snapshot of one shared folder's effective locks. +#[derive(Clone, Debug)] +pub struct LockedFolderPublic { + id: Guid, + name: String, + detach_restrictions: Arc<[RlvRestriction]>, + attach_restrictions: Arc<[RlvRestriction]>, + detach_exceptions: Arc<[RlvRestriction]>, + attach_exceptions: Arc<[RlvRestriction]>, +} + +impl LockedFolderPublic { + fn from_accumulator(folder: LockedFolderAccumulator) -> Option { + let source = folder.folder?; + Some(Self { + id: source.native_id(), + name: source.native_name(), + detach_restrictions: folder.detach_restrictions.into(), + attach_restrictions: folder.attach_restrictions.into(), + detach_exceptions: folder.detach_exceptions.into(), + attach_exceptions: folder.attach_exceptions.into(), + }) + } + + pub(crate) fn native_to_string(&self) -> String { + format!( + "LockedFolder: {} ({}) - Locked: {}, CanAttach: {}, CanDetach: {}", + self.name, + guid_text(self.id), + self.native_is_locked(), + self.native_can_attach(), + self.native_can_detach() + ) + } + + pub(crate) fn native_attach_exceptions(&self) -> Vec { + self.attach_exceptions.to_vec() + } + pub(crate) fn native_attach_restrictions(&self) -> Vec { + self.attach_restrictions.to_vec() + } + pub(crate) fn native_can_attach(&self) -> bool { + !self.attach_exceptions.is_empty() || self.attach_restrictions.is_empty() + } + pub(crate) fn native_can_detach(&self) -> bool { + !self.detach_exceptions.is_empty() || self.detach_restrictions.is_empty() + } + pub(crate) fn native_detach_exceptions(&self) -> Vec { + self.detach_exceptions.to_vec() + } + pub(crate) fn native_detach_restrictions(&self) -> Vec { + self.detach_restrictions.to_vec() + } + pub(crate) fn native_id(&self) -> Guid { + self.id + } + pub(crate) fn native_is_locked(&self) -> bool { + !self.detach_restrictions.is_empty() || !self.attach_restrictions.is_empty() + } + pub(crate) fn native_name(&self) -> String { + self.name.clone() + } +} + +#[derive(Debug, Default)] +struct RestrictionState { + restrictions: Vec, + locked_folders: HashMap, + inventory: Option, + revision: u64, +} + +struct RestrictionManagerInner { + state: RwLock, + handlers: Arc>>>, + next_handler: AtomicU64, +} + +/// Thread-safe restriction provider with snapshot reads and post-lock events. +#[derive(Clone)] +pub struct RlvRestrictionManager { + inner: Arc, +} + +impl fmt::Debug for RlvRestrictionManager { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("RlvRestrictionManager") + .field( + "restriction_count", + &read_lock(&self.inner.state).restrictions.len(), + ) + .finish_non_exhaustive() + } +} + +impl Default for RlvRestrictionManager { + fn default() -> Self { + Self::new() + } +} + +impl RlvRestrictionManager { + pub fn new() -> Self { + Self { + inner: Arc::new(RestrictionManagerInner { + state: RwLock::new(RestrictionState::default()), + handlers: Arc::new(Mutex::new(BTreeMap::new())), + next_handler: AtomicU64::new(1), + }), + } + } + + /// Replaces the shared-inventory snapshot used for folder-lock decisions. + pub fn set_inventory_map(&self, inventory: Option) { + let mut state = write_lock(&self.inner.state); + state.inventory = inventory; + state.revision = state.revision.wrapping_add(1); + drop(state); + self.rebuild_locked_folders(); + } + + /// Adds a unique restriction and emits an update after releasing state locks. + pub fn add_restriction(&self, restriction: RlvRestriction) -> Result { + let added = { + let mut state = write_lock(&self.inner.state); + if state.restrictions.contains(&restriction) { + false + } else { + if state.restrictions.len() >= MAX_RLV_RESTRICTIONS { + return Err(Error::Argument); + } + state.restrictions.push(restriction.clone()); + state.revision = state.revision.wrapping_add(1); + true + } + }; + if added { + self.rebuild_locked_folders(); + self.emit(RestrictionUpdatedEventArgs { + restriction, + is_new: true, + is_deleted: false, + }); + } + Ok(added) + } + + /// Removes one restriction equal by behavior, sender, and exact arguments. + pub fn remove_restriction(&self, restriction: &RlvRestriction) -> bool { + let removed = { + let mut state = write_lock(&self.inner.state); + let removed = state + .restrictions + .iter() + .position(|candidate| candidate == restriction) + .map(|index| state.restrictions.remove(index)); + if removed.is_some() { + state.revision = state.revision.wrapping_add(1); + } + removed + }; + if let Some(removed) = removed { + self.rebuild_locked_folders(); + self.emit(RestrictionUpdatedEventArgs { + restriction: removed, + is_new: false, + is_deleted: true, + }); + true + } else { + false + } + } + + /// Removes restrictions from one sender whose canonical name contains the filter. + pub fn clear_sender(&self, sender: Guid, behavior_filter: &str) -> Vec { + let filter = behavior_filter.to_lowercase(); + let removed = { + let mut state = write_lock(&self.inner.state); + let mut removed = Vec::new(); + state.restrictions.retain(|restriction| { + let matches = restriction.sender() == sender + && restriction_name(restriction.behavior()) + .to_lowercase() + .contains(&filter); + if matches { + removed.push(restriction.clone()); + } + !matches + }); + if !removed.is_empty() { + state.revision = state.revision.wrapping_add(1); + } + removed + }; + if !removed.is_empty() { + self.rebuild_locked_folders(); + for restriction in &removed { + self.emit(RestrictionUpdatedEventArgs { + restriction: restriction.clone(), + is_new: false, + is_deleted: true, + }); + } + } + removed + } + + pub(crate) fn native_subscribe_restriction_updated( + &self, + handler: Option>, + ) -> Subscription { + let Some(handler) = handler else { + return Subscription::detached(); + }; + let id = self.inner.next_handler.fetch_add(1, Ordering::Relaxed); + self.inner + .handlers + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(id, handler); + let handlers = Arc::downgrade(&self.inner.handlers); + Subscription::new(move || { + let Some(handlers) = handlers.upgrade() else { + return; + }; + handlers + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&id); + }) + } + + pub(crate) fn native_find_restrictions( + &self, + behavior_name_filter: Option, + sender_filter: Option>, + ) -> Result, Error> { + let filter = behavior_name_filter.unwrap_or_default().to_lowercase(); + let sender = nullable(sender_filter); + Ok(read_lock(&self.inner.state) + .restrictions + .iter() + .filter(|restriction| { + restriction_name(restriction.behavior()) + .to_lowercase() + .contains(&filter) + && sender.is_none_or(|sender| sender == restriction.sender()) + }) + .cloned() + .collect()) + } + + pub(crate) fn native_get_locked_folders( + &self, + ) -> Result, Error> { + Ok(read_lock(&self.inner.state).locked_folders.clone()) + } + + pub(crate) fn native_get_restrictions_by_type( + &self, + restriction_type: RlvRestrictionType, + ) -> Result, Error> { + Ok(self.restrictions_by_type(restriction_type)) + } + + pub(crate) fn native_get_tracked_prim_ids( + &self, + ) -> Result>, Error> { + let mut seen = HashSet::new(); + let ids: Vec<_> = read_lock(&self.inner.state) + .restrictions + .iter() + .filter_map(|restriction| { + let sender = restriction.sender(); + seen.insert(sender).then_some(sender) + }) + .collect(); + Ok(Box::new(ids.into_iter())) + } + + #[allow(clippy::unused_async)] // The mapped C# contract is asynchronous. + pub(crate) async fn native_remove_restrictions_for_objects( + &self, + prim_ids: Box>, + cancellation_token: Option, + ) -> Result<(), Error> { + if let Some(token) = &cancellation_token { + token.throw_if_cancellation_requested()?; + } + let ids = prim_ids.take(MAX_RLV_RESTRICTIONS + 1).collect::>(); + if ids.len() > MAX_RLV_RESTRICTIONS { + return Err(Error::Argument); + } + let ids: HashSet<_> = ids.into_iter().collect(); + let removed = { + let mut state = write_lock(&self.inner.state); + let mut removed = Vec::new(); + state.restrictions.retain(|restriction| { + if ids.contains(&restriction.sender()) { + removed.push(restriction.clone()); + false + } else { + true + } + }); + if !removed.is_empty() { + state.revision = state.revision.wrapping_add(1); + } + removed + }; + if !removed.is_empty() { + self.rebuild_locked_folders(); + for restriction in removed { + if let Some(token) = &cancellation_token { + token.throw_if_cancellation_requested()?; + } + self.emit(RestrictionUpdatedEventArgs { + restriction, + is_new: false, + is_deleted: true, + }); + } + } + Ok(()) + } + + pub(crate) fn native_try_get_locked_folder( + &self, + folder_id: Guid, + locked_folder: &mut Option, + ) -> bool { + let value = read_lock(&self.inner.state) + .locked_folders + .get(&folder_id) + .cloned(); + *locked_folder = value; + locked_folder.is_some() + } + + fn restrictions_by_type(&self, restriction_type: RlvRestrictionType) -> Vec { + let canonical = canonical_restriction(restriction_type); + read_lock(&self.inner.state) + .restrictions + .iter() + .filter(|restriction| restriction.behavior() == canonical) + .cloned() + .collect() + } + + fn float_values(&self, restriction_type: RlvRestrictionType) -> Vec { + self.restrictions_by_type(restriction_type) + .into_iter() + .filter_map(|restriction| match restriction.values().first() { + Some(RlvValue::Real(value)) => Some(*value), + _ => None, + }) + .collect() + } + + fn emit(&self, event: RestrictionUpdatedEventArgs) { + let handlers: Vec<_> = self + .inner + .handlers + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .values() + .cloned() + .collect(); + for handler in handlers { + handler(event.clone()); + } + } + + fn rebuild_locked_folders(&self) { + loop { + let (revision, inventory, restrictions) = { + let state = read_lock(&self.inner.state); + ( + state.revision, + state.inventory.clone(), + state.restrictions.clone(), + ) + }; + let locked_folders = inventory.map_or_else(HashMap::new, |inventory| { + build_locked_folders(&inventory, &restrictions) + }); + let mut state = write_lock(&self.inner.state); + if state.revision == revision { + state.locked_folders = locked_folders; + return; + } + } + } +} + +fn build_locked_folders( + inventory: &InventoryMap, + restrictions: &[RlvRestriction], +) -> HashMap { + let mut result: HashMap = HashMap::new(); + for restriction in restrictions { + let behavior = restriction.behavior(); + if !matches!( + behavior, + RlvRestrictionType::DetachThis + | RlvRestrictionType::DetachAllThis + | RlvRestrictionType::AttachThis + | RlvRestrictionType::AttachAllThis + | RlvRestrictionType::DetachThisExcept + | RlvRestrictionType::DetachAllThisExcept + | RlvRestrictionType::AttachThisExcept + | RlvRestrictionType::AttachAllThisExcept + ) { + continue; + } + let folders = folders_for_lock(inventory, restriction); + let recursive = matches!( + behavior, + RlvRestrictionType::DetachAllThis + | RlvRestrictionType::AttachAllThis + | RlvRestrictionType::DetachAllThisExcept + | RlvRestrictionType::AttachAllThisExcept + ); + for folder in folders { + add_folder_lock( + &mut result, + folder, + restriction, + recursive, + &mut HashSet::new(), + ); + } + } + result + .into_iter() + .filter_map(|(id, folder)| { + LockedFolderPublic::from_accumulator(folder).map(|value| (id, value)) + }) + .collect() +} + +fn folders_for_lock( + inventory: &InventoryMap, + restriction: &RlvRestriction, +) -> Vec { + match restriction.values() { + [] => inventory + .native_get_items_by_prim_id(restriction.sender()) + .unwrap_or_default() + .into_iter() + .filter_map(|item| item.native_folder()) + .collect(), + [RlvValue::WearableType(value)] => inventory + .native_get_items_by_wearable_type(*value) + .unwrap_or_default() + .into_iter() + .filter_map(|item| item.native_folder()) + .collect(), + [RlvValue::AttachmentPoint(value)] => inventory + .native_get_items_by_attachment_point(*value) + .unwrap_or_default() + .into_iter() + .filter_map(|item| item.native_folder()) + .collect(), + [RlvValue::String(path)] => { + let mut folder = None; + if inventory.native_try_get_folder_from_path(path.clone(), false, &mut folder) { + folder.into_iter().collect() + } else { + Vec::new() + } + } + _ => Vec::new(), + } +} + +fn add_folder_lock( + result: &mut HashMap, + folder: RlvSharedFolder, + restriction: &RlvRestriction, + recursive: bool, + visited: &mut HashSet, +) { + if !visited.insert(folder.native_id()) { + return; + } + let entry = result.entry(folder.native_id()).or_default(); + entry.folder.get_or_insert_with(|| folder.clone()); + match restriction.behavior() { + RlvRestrictionType::DetachThis | RlvRestrictionType::DetachAllThis => { + entry.detach_restrictions.push(restriction.clone()); + } + RlvRestrictionType::AttachThis | RlvRestrictionType::AttachAllThis => { + entry.attach_restrictions.push(restriction.clone()); + } + RlvRestrictionType::DetachThisExcept | RlvRestrictionType::DetachAllThisExcept => { + entry.detach_exceptions.push(restriction.clone()); + } + RlvRestrictionType::AttachThisExcept | RlvRestrictionType::AttachAllThisExcept => { + entry.attach_exceptions.push(restriction.clone()); + } + _ => {} + } + if recursive { + for child in folder.native_children() { + add_folder_lock(result, child, restriction, true, visited); + } + } +} + +fn canonical_restriction(value: RlvRestrictionType) -> RlvRestrictionType { + match value { + RlvRestrictionType::CamDistMax => RlvRestrictionType::SetCamAvDistMax, + RlvRestrictionType::CamDistMin => RlvRestrictionType::SetCamAvDistMin, + RlvRestrictionType::CamUnlock => RlvRestrictionType::SetCamUnlock, + RlvRestrictionType::CamTextures => RlvRestrictionType::SetCamTextures, + RlvRestrictionType::FarTouch => RlvRestrictionType::TouchFar, + other => other, + } +} + +fn guid_text(value: Guid) -> String { + UUID::new_with_guid(value).map_or_else(|_| format!("{value:?}"), |value| value.to_string()) +} + +/// Pure permission evaluator over a shared restriction manager. +#[derive(Clone, Debug)] +pub struct RlvPermissionsService { + restrictions: RlvRestrictionManager, +} + +impl RlvPermissionsService { + pub fn new(restrictions: RlvRestrictionManager) -> Self { + Self { restrictions } + } + + pub fn restriction_manager(&self) -> RlvRestrictionManager { + self.restrictions.clone() + } + + fn restrictions(&self, kind: RlvRestrictionType) -> Vec { + self.restrictions.restrictions_by_type(kind) + } + + fn absent(&self, kind: RlvRestrictionType) -> bool { + self.restrictions(kind).is_empty() + } + + fn optional_min(&self, kind: RlvRestrictionType, default: f32) -> (bool, f32) { + let restrictions = self.restrictions(kind); + if restrictions.is_empty() { + return (false, default); + } + if restrictions + .iter() + .any(|restriction| restriction.values().is_empty()) + { + return (true, default); + } + let value = restrictions + .iter() + .filter_map(|restriction| match restriction.values().first() { + Some(RlvValue::Real(value)) => Some(*value), + _ => None, + }) + .reduce(f32::min) + .unwrap_or(default); + (true, value) + } + + fn secure_allowed( + &self, + user_id: Option, + group_name: Option<&str>, + normal: RlvRestrictionType, + secure: Option, + explicit: Option, + ) -> bool { + if explicit.is_some_and(|kind| { + self.restrictions(kind) + .iter() + .any(|restriction| restriction_matches_target(restriction, user_id, group_name)) + }) { + return false; + } + let normal_restrictions = self.restrictions(normal); + let exceptions: Vec<_> = normal_restrictions + .iter() + .filter(|restriction| { + restriction.is_exception() + && restriction_matches_target(restriction, user_id, group_name) + }) + .collect(); + if let Some(secure) = secure { + for restriction in self.restrictions(secure) { + if exceptions + .iter() + .any(|exception| exception.sender() == restriction.sender()) + { + continue; + } + return false; + } + } + let permissive = self.is_permissive_value(); + for restriction in normal_restrictions + .iter() + .filter(|restriction| !restriction.is_exception() && restriction.values().is_empty()) + { + if exceptions + .iter() + .any(|exception| permissive || exception.sender() == restriction.sender()) + { + continue; + } + return false; + } + true + } + + fn is_permissive_value(&self) -> bool { + self.absent(RlvRestrictionType::Permissive) + } + + pub(crate) fn native_can_allow_idle(&self) -> Result { + Ok(self.absent(RlvRestrictionType::AllowIdle)) + } + pub(crate) fn native_can_always_run(&self) -> Result { + Ok(self.absent(RlvRestrictionType::AlwaysRun)) + } + pub(crate) fn native_can_fly(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Fly)) + } + pub(crate) fn native_can_jump(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Jump)) + } + pub(crate) fn native_can_temp_run(&self) -> Result { + Ok(self.absent(RlvRestrictionType::TempRun)) + } + pub(crate) fn native_can_unsit(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Unsit)) + } + pub(crate) fn native_can_interact(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Interact)) + } + pub(crate) fn native_can_sit(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Interact) && self.absent(RlvRestrictionType::Sit)) + } + pub(crate) fn native_can_tp_lm(&self) -> Result { + Ok(self.absent(RlvRestrictionType::TpLm)) + } + pub(crate) fn native_can_tp_loc(&self) -> Result { + Ok(self.absent(RlvRestrictionType::TpLoc)) + } + pub(crate) fn native_can_stand_tp(&self) -> Result { + Ok(self.absent(RlvRestrictionType::StandTp)) + } + pub(crate) fn native_can_show_inv(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ShowInv)) + } + pub(crate) fn native_can_view_note(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ViewNote)) + } + pub(crate) fn native_can_view_script(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ViewScript)) + } + pub(crate) fn native_can_view_texture(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ViewTexture)) + } + pub(crate) fn native_can_default_wear(&self) -> Result { + Ok(self.absent(RlvRestrictionType::DefaultWear)) + } + pub(crate) fn native_can_set_group(&self) -> Result { + Ok(self.absent(RlvRestrictionType::SetGroup)) + } + pub(crate) fn native_can_set_debug(&self) -> Result { + Ok(self.absent(RlvRestrictionType::SetDebug)) + } + pub(crate) fn native_can_set_env(&self) -> Result { + Ok(self.absent(RlvRestrictionType::SetEnv)) + } + pub(crate) fn native_can_show_world_map(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ShowWorldMap)) + } + pub(crate) fn native_can_show_mini_map(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ShowMiniMap)) + } + pub(crate) fn native_can_show_loc(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ShowLoc)) + } + pub(crate) fn native_can_show_nearby(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ShowNearby)) + } + pub(crate) fn native_can_chat_shout(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ChatShout)) + } + pub(crate) fn native_can_chat_whisper(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ChatWhisper)) + } + pub(crate) fn native_can_chat_normal(&self) -> Result { + Ok(self.absent(RlvRestrictionType::ChatNormal)) + } + pub(crate) fn native_can_send_chat(&self) -> Result { + Ok(self.absent(RlvRestrictionType::SendChat)) + } + pub(crate) fn native_can_emote(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Emote)) + } + pub(crate) fn native_can_send_gesture(&self) -> Result { + Ok(self.absent(RlvRestrictionType::SendGesture)) + } + + pub(crate) fn native_can_sit_tp(&self, max_distance: &mut f32) -> Result { + let (restricted, value) = self.optional_min(RlvRestrictionType::SitTp, 1.5); + *max_distance = value; + Ok(restricted) + } + + pub(crate) fn native_can_tp_local(&self, max_distance: &mut f32) -> Result { + let (restricted, value) = self.optional_min(RlvRestrictionType::TpLocal, 0.0); + *max_distance = value; + Ok(restricted) + } + + pub(crate) fn native_try_get_max_far_touch_distance(&self, max_distance: &mut f32) -> bool { + let (restricted, value) = self.optional_min(RlvRestrictionType::FarTouch, 1.5); + *max_distance = value; + restricted + } + + pub(crate) fn native_can_tp_lure(&self, user_id: Option>) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + None, + RlvRestrictionType::TpLure, + Some(RlvRestrictionType::TpLureSec), + None, + )) + } + + pub(crate) fn native_can_tp_request( + &self, + user_id: Option>, + ) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + None, + RlvRestrictionType::TpRequest, + Some(RlvRestrictionType::TpRequestSec), + None, + )) + } + + pub(crate) fn native_is_auto_accept_tp( + &self, + user_id: Option>, + ) -> Result { + Ok(self.auto_accept(RlvRestrictionType::AcceptTp, nullable(user_id))) + } + + pub(crate) fn native_is_auto_accept_tp_request( + &self, + user_id: Option>, + ) -> Result { + Ok(self.auto_accept(RlvRestrictionType::AcceptTpRequest, nullable(user_id))) + } + + fn auto_accept(&self, kind: RlvRestrictionType, user_id: Option) -> bool { + self.restrictions(kind).iter().any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::Uuid(value)] if Some(*value) == user_id) + }) + } + + pub(crate) fn native_can_start_im(&self, user_id: Guid) -> Result { + Ok(self.secure_allowed( + Some(user_id), + None, + RlvRestrictionType::StartIm, + None, + Some(RlvRestrictionType::StartImTo), + )) + } + + pub(crate) fn native_can_send_im( + &self, + _message: String, + user_id: Option>, + group_name: Option, + ) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + group_name.as_deref(), + RlvRestrictionType::SendIm, + Some(RlvRestrictionType::SendImSec), + Some(RlvRestrictionType::SendImTo), + )) + } + + pub(crate) fn native_can_receive_im( + &self, + _message: String, + user_id: Option>, + group_name: Option, + ) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + group_name.as_deref(), + RlvRestrictionType::RecvIm, + Some(RlvRestrictionType::RecvImSec), + Some(RlvRestrictionType::RecvImFrom), + )) + } + + pub(crate) fn native_can_receive_chat( + &self, + message: String, + user_id: Guid, + ) -> Result { + let (normal, secure, explicit) = if starts_with_ignore_case(&message, "/me ") { + ( + RlvRestrictionType::RecvEmote, + RlvRestrictionType::RecvEmoteSec, + RlvRestrictionType::RecvEmoteFrom, + ) + } else { + ( + RlvRestrictionType::RecvChat, + RlvRestrictionType::RecvChatSec, + RlvRestrictionType::RecvChatFrom, + ) + }; + Ok(self.secure_allowed(Some(user_id), None, normal, Some(secure), Some(explicit))) + } + + pub(crate) fn native_try_get_redir_chat_channels(&self, channels: &mut Vec) -> bool { + *channels = self.distinct_channels(RlvRestrictionType::RedirChat); + !channels.is_empty() + } + + pub(crate) fn native_try_get_redir_emote_channels(&self, channels: &mut Vec) -> bool { + *channels = self.distinct_channels(RlvRestrictionType::RedirEmote); + !channels.is_empty() + } + + fn distinct_channels(&self, kind: RlvRestrictionType) -> Vec { + let mut seen = HashSet::new(); + self.restrictions(kind) + .iter() + .filter_map(|restriction| match restriction.values() { + [RlvValue::Integer(value)] if seen.insert(*value) => Some(*value), + _ => None, + }) + .collect() + } + + pub(crate) fn native_can_chat(&self, channel: i32, message: String) -> Result { + if channel == 0 { + if starts_with_ignore_case(&message, "/me ") && !self.absent(RlvRestrictionType::Emote) + { + return Ok(false); + } + if !self.absent(RlvRestrictionType::SendChat) { + if message.chars().any(|value| "()\"-*=_^".contains(value)) { + return Ok(false); + } + return Ok(message.starts_with('/')); + } + return Ok(true); + } + Ok(self.can_chat_private(channel)) + } + + fn can_chat_private(&self, channel: i32) -> bool { + if self + .restrictions(RlvRestrictionType::SendChannelExcept) + .iter() + .any(|restriction| { + matches!(restriction.values(), [RlvValue::Integer(value)] if *value == channel) + }) + { + return false; + } + let normal = self.restrictions(RlvRestrictionType::SendChannel); + let exceptions: Vec<_> = normal + .iter() + .filter(|restriction| { + restriction.is_exception() + && matches!(restriction.values(), [RlvValue::Integer(value)] if *value == channel) + }) + .collect(); + for secure in self.restrictions(RlvRestrictionType::SendChannelSec) { + if !exceptions + .iter() + .any(|exception| exception.sender() == secure.sender()) + { + return false; + } + } + let permissive = self.is_permissive_value(); + for restriction in normal + .iter() + .filter(|restriction| !restriction.is_exception() && restriction.values().is_empty()) + { + if !exceptions + .iter() + .any(|exception| permissive || exception.sender() == restriction.sender()) + { + return false; + } + } + true + } + + pub(crate) fn native_can_show_names( + &self, + user_id: Option>, + ) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + None, + RlvRestrictionType::ShowNames, + Some(RlvRestrictionType::ShowNamesSec), + None, + )) + } + + pub(crate) fn native_can_show_name_tags( + &self, + user_id: Option>, + ) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + None, + RlvRestrictionType::ShowNameTags, + None, + None, + )) + } + + pub(crate) fn native_can_share(&self, user_id: Option>) -> Result { + Ok(self.secure_allowed( + nullable(user_id), + None, + RlvRestrictionType::Share, + Some(RlvRestrictionType::ShareSec), + None, + )) + } + + pub(crate) fn native_is_auto_deny_permissions(&self) -> Result { + Ok(!self.absent(RlvRestrictionType::DenyPermission)) + } + + pub(crate) fn native_is_auto_accept_permissions(&self) -> Result { + Ok(self.absent(RlvRestrictionType::DenyPermission) + && !self.absent(RlvRestrictionType::AcceptPermission)) + } + + pub(crate) fn native_is_permissive(&self) -> Result { + Ok(self.is_permissive_value()) + } + + pub(crate) fn native_can_rez(&self) -> Result { + Ok(self.absent(RlvRestrictionType::Interact) && self.absent(RlvRestrictionType::Rez)) + } + + pub(crate) fn native_can_edit( + &self, + object_location: RlvPermissionsServiceObjectLocation, + object_id: Option>, + ) -> Result { + if !self.absent(RlvRestrictionType::Interact) + || !self.secure_allowed( + nullable(object_id), + None, + RlvRestrictionType::Edit, + None, + Some(RlvRestrictionType::EditObj), + ) + { + return Ok(false); + } + Ok(match object_location { + RlvPermissionsServiceObjectLocation::RezzedInWorld => { + self.absent(RlvRestrictionType::EditWorld) + } + RlvPermissionsServiceObjectLocation::Attached => { + self.absent(RlvRestrictionType::EditAttach) + } + RlvPermissionsServiceObjectLocation::Hud => true, + }) + } + + pub(crate) fn native_can_touch( + &self, + location: RlvPermissionsServiceTouchLocation, + prim_id: Guid, + user_id: Option>, + distance: Option>, + ) -> Result { + if let Some(distance) = nullable(distance) + && let Some(maximum) = self + .restrictions + .float_values(RlvRestrictionType::FarTouch) + .into_iter() + .reduce(f32::min) + && distance > maximum + { + return Ok(false); + } + if !self.absent(RlvRestrictionType::Interact) { + return Ok(false); + } + if self + .restrictions(RlvRestrictionType::TouchMe) + .iter() + .any(|restriction| restriction.sender() == prim_id) + { + return Ok(true); + } + if self + .restrictions(RlvRestrictionType::TouchThis) + .iter() + .any(|restriction| { + matches!(restriction.values(), [RlvValue::Uuid(value)] if *value == prim_id) + }) + { + return Ok(false); + } + if location != RlvPermissionsServiceTouchLocation::Hud + && !self.absent(RlvRestrictionType::TouchAll) + { + return Ok(false); + } + match location { + RlvPermissionsServiceTouchLocation::RezzedInWorld => { + let restrictions = self.restrictions(RlvRestrictionType::TouchWorld); + let exception = restrictions.iter().any(|restriction| { + restriction.is_exception() + && matches!(restriction.values(), [RlvValue::Uuid(value)] if *value == prim_id) + }); + if !exception + && restrictions + .iter() + .any(|restriction| restriction.values().is_empty()) + { + return Ok(false); + } + } + RlvPermissionsServiceTouchLocation::AttachedSelf => { + if !self.can_touch_attachment(true, nullable(user_id)) { + return Ok(false); + } + } + RlvPermissionsServiceTouchLocation::AttachedOther => { + if !self.can_touch_attachment(false, nullable(user_id)) { + return Ok(false); + } + } + RlvPermissionsServiceTouchLocation::Hud => { + if self + .restrictions(RlvRestrictionType::TouchHud) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::Uuid(value)] if *value == prim_id) + }) + { + return Ok(false); + } + } + } + Ok(true) + } + + fn can_touch_attachment(&self, attached_to_self: bool, user_id: Option) -> bool { + if !self.absent(RlvRestrictionType::TouchAttach) { + return false; + } + if attached_to_self { + return self.absent(RlvRestrictionType::TouchAttachSelf); + } + !self + .restrictions(RlvRestrictionType::TouchAttachOther) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::Uuid(value)] if Some(*value) == user_id) + }) + } + + pub(crate) fn native_can_show_hover_text( + &self, + location: RlvPermissionsServiceHoverTextLocation, + object_id: Option>, + ) -> Result { + if !self.absent(RlvRestrictionType::ShowHoverTextAll) { + return Ok(false); + } + let object_id = nullable(object_id); + if self + .restrictions(RlvRestrictionType::ShowHoverText) + .iter() + .any(|restriction| { + matches!(restriction.values(), [RlvValue::Uuid(value)] if Some(*value) == object_id) + }) + { + return Ok(false); + } + Ok(match location { + RlvPermissionsServiceHoverTextLocation::Hud => { + self.absent(RlvRestrictionType::ShowHoverTextHud) + } + RlvPermissionsServiceHoverTextLocation::World => { + self.absent(RlvRestrictionType::ShowHoverTextWorld) + } + }) + } + + pub(crate) fn native_can_attach_with_item( + &self, + item: RlvInventoryItem, + is_shared: bool, + ) -> Result { + self.native_can_attach( + mapped_nullable(item.folder_id_value()), + is_shared, + mapped_nullable(item.attached_to_value()), + mapped_nullable(item.worn_on_value()), + ) + } + + pub(crate) fn native_can_attach( + &self, + object_folder_id: Option>, + is_shared: bool, + attachment_point: Option>, + wearable_type: Option>, + ) -> Result { + let folder_id = nullable(object_folder_id); + let attachment_point = nullable(attachment_point); + let wearable_type = nullable(wearable_type); + if wearable_type.is_some_and(|wearable| { + self.restrictions(RlvRestrictionType::AddOutfit) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::WearableType(value)] if *value == wearable) + }) + }) || attachment_point.is_some_and(|point| { + self.restrictions(RlvRestrictionType::AddAttach) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::AttachmentPoint(value)] if *value == point) + }) + }) { + return Ok(false); + } + if is_shared { + if !self.absent(RlvRestrictionType::SharedWear) { + return Ok(false); + } + let Some(folder_id) = folder_id else { + return Ok(false); + }; + let mut locked = None; + if self + .restrictions + .native_try_get_locked_folder(folder_id, &mut locked) + && locked.is_some_and(|folder| !folder.native_can_attach()) + { + return Ok(false); + } + } else if !self.absent(RlvRestrictionType::UnsharedWear) { + return Ok(false); + } + Ok(true) + } + + pub(crate) fn native_can_detach_with_item( + &self, + item: RlvInventoryItem, + ) -> Result { + self.native_can_detach( + mapped_nullable(Some(item.native_id())), + mapped_nullable(item.attached_prim_id_value()), + mapped_nullable(item.folder_id_value()), + item.native_folder().is_some(), + mapped_nullable(item.attached_to_value()), + mapped_nullable(item.worn_on_value()), + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn native_can_detach( + &self, + _item_id: Option>, + prim_id: Option>, + folder_id: Option>, + is_shared: bool, + attachment_point: Option>, + wearable_type: Option>, + ) -> Result { + let prim_id = nullable(prim_id); + let folder_id = nullable(folder_id); + let attachment_point = nullable(attachment_point); + let wearable_type = nullable(wearable_type); + if wearable_type.is_some_and(|wearable| { + self.restrictions(RlvRestrictionType::RemOutfit) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::WearableType(value)] if *value == wearable) + }) + }) || attachment_point.is_some_and(|point| { + self.restrictions(RlvRestrictionType::RemAttach) + .iter() + .any(|restriction| { + restriction.values().is_empty() + || matches!(restriction.values(), [RlvValue::AttachmentPoint(value)] if *value == point) + }) + }) { + return Ok(false); + } + for restriction in self.restrictions(RlvRestrictionType::Detach) { + match restriction.values() { + [] if prim_id == Some(restriction.sender()) => return Ok(false), + [RlvValue::AttachmentPoint(value)] if Some(*value) == attachment_point => { + return Ok(false); + } + _ => {} + } + } + if is_shared { + if !self.absent(RlvRestrictionType::SharedUnwear) { + return Ok(false); + } + let Some(folder_id) = folder_id else { + return Ok(false); + }; + let mut locked = None; + if self + .restrictions + .native_try_get_locked_folder(folder_id, &mut locked) + && locked.is_some_and(|folder| !folder.native_can_detach()) + { + return Ok(false); + } + } else if !self.absent(RlvRestrictionType::UnsharedUnwear) { + return Ok(false); + } + Ok(true) + } + + pub(crate) fn native_get_camera_restrictions(&self) -> Result { + Ok(CameraRestrictions::from_manager(&self.restrictions)) + } +} + +fn restriction_matches_target( + restriction: &RlvRestriction, + user_id: Option, + group_name: Option<&str>, +) -> bool { + match restriction.values() { + [RlvValue::Uuid(value)] => Some(*value) == user_id, + [RlvValue::String(value)] => { + group_name.is_some_and(|group| value == "allgroups" || value == group) + } + _ => false, + } +} diff --git a/crates/libremetaverse-rlv/tests/state_semantics.rs b/crates/libremetaverse-rlv/tests/state_semantics.rs new file mode 100644 index 0000000..0964cb4 --- /dev/null +++ b/crates/libremetaverse-rlv/tests/state_semantics.rs @@ -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) -> 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))) + ); +} diff --git a/crates/libremetaverse-types/src/compat.rs b/crates/libremetaverse-types/src/compat.rs index 6aba83b..05f314a 100644 --- a/crates/libremetaverse-types/src/compat.rs +++ b/crates/libremetaverse-types/src/compat.rs @@ -1151,7 +1151,100 @@ impl Future for AsyncEnumerableNext { } } -pub struct ImmutableDictionary(pub PhantomData); +/// Read-only, cheaply clonable hash-map snapshot corresponding to +/// `System.Collections.Immutable.ImmutableDictionary`. +#[derive(Clone, Debug)] +pub struct ImmutableDictionary(Arc>); + +impl PartialEq for ImmutableDictionary +where + TKey: Eq + std::hash::Hash, + TValue: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for ImmutableDictionary +where + TKey: Eq + std::hash::Hash, + TValue: Eq, +{ +} + +impl ImmutableDictionary { + /// Creates an immutable snapshot from an owned map. + #[must_use] + pub fn from_map(values: std::collections::HashMap) -> Self { + Self(Arc::new(values)) + } + + /// Returns the number of entries in the snapshot. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether the snapshot contains no entries. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Iterates over keys and values in the snapshot. + #[must_use] + pub fn iter(&self) -> std::collections::hash_map::Iter<'_, TKey, TValue> { + self.0.iter() + } + + /// Iterates over values in the snapshot. + #[must_use] + pub fn values(&self) -> std::collections::hash_map::Values<'_, TKey, TValue> { + self.0.values() + } + + /// Looks up a value by borrowed key. + #[must_use] + pub fn get(&self, key: &Q) -> Option<&TValue> + where + TKey: std::borrow::Borrow + Eq + std::hash::Hash, + Q: Eq + std::hash::Hash + ?Sized, + { + self.0.get(key) + } +} + +impl Default for ImmutableDictionary { + fn default() -> Self { + Self(Arc::new(std::collections::HashMap::new())) + } +} + +impl From> + for ImmutableDictionary +{ + fn from(values: std::collections::HashMap) -> Self { + Self::from_map(values) + } +} + +impl std::ops::Deref for ImmutableDictionary { + type Target = std::collections::HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, TKey, TValue> IntoIterator for &'a ImmutableDictionary { + type Item = (&'a TKey, &'a TValue); + type IntoIter = std::collections::hash_map::Iter<'a, TKey, TValue>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} /// Read-only, cheaply clonable snapshot corresponding to /// `System.Collections.Immutable.ImmutableList`. diff --git a/tools/check_milestone_10_issue_79.py b/tools/check_milestone_10_issue_79.py new file mode 100644 index 0000000..d1a5a9c --- /dev/null +++ b/tools/check_milestone_10_issue_79.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Audit issue 79's native RLV state, inventory, lock, and permission boundary.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import generate_api_shims + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "crates" / "libremetaverse-rlv" / "src" / "state.rs" +GENERATED = ROOT / "crates" / "libremetaverse-rlv" / "src" / "generated.rs" +TESTS = ROOT / "crates" / "libremetaverse-rlv" / "tests" / "state_semantics.rs" +COMPAT = ROOT / "tests" / "compat" / "tests" / "rlv_inventory_map_semantics.rs" +DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" +WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +CATALOG = ROOT / "api" / "public-api.json" +STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") + +TYPES = { + "T:LibreMetaverse.RLV.CameraRestrictions": "crate::state::CameraRestrictions", + "T:LibreMetaverse.RLV.CameraSettings": "crate::state::CameraSettings", + "T:LibreMetaverse.RLV.EventArguments.RestrictionUpdatedEventArgs": + "crate::state::RestrictionUpdatedEventArgs", + "T:LibreMetaverse.RLV.InventoryMap": "crate::state::InventoryMap", + "T:LibreMetaverse.RLV.LockedFolderPublic": "crate::state::LockedFolderPublic", + "T:LibreMetaverse.RLV.RlvBlacklist": "crate::state::RlvBlacklist", + "T:LibreMetaverse.RLV.RlvInventoryItem": "crate::state::RlvInventoryItem", + "T:LibreMetaverse.RLV.RlvPermissionsService": "crate::state::RlvPermissionsService", + "T:LibreMetaverse.RLV.RlvRestrictionManager": "crate::state::RlvRestrictionManager", + "T:LibreMetaverse.RLV.RlvSharedFolder": "crate::state::RlvSharedFolder", +} + + +def require_markers(path: Path, markers: tuple[str, ...]) -> None: + text = path.read_text() + missing = [marker for marker in markers if marker not in text] + if missing: + raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing)) + + +def catalog_members() -> set[str]: + catalog = json.loads(CATALOG.read_text()) + assembly = next( + value for value in catalog["assemblies"] + if value["identity"]["name"] == "LibreMetaverse.RLV" + ) + return { + member["doc_id"] + for api_type in assembly["types"] + if api_type["doc_id"] in TYPES + for member in api_type["members"] + } + + +def generated_type_block(text: str, doc_id: str) -> str: + marker = f"/// C# type: `{doc_id}`." + start = text.find(marker) + if start < 0: + raise SystemExit(f"generated declaration is missing for {doc_id}") + next_type = text.find("/// C# type:", start + len(marker)) + return text[start:] if next_type < 0 else text[start:next_type] + + +def main() -> None: + for api_type, declaration in TYPES.items(): + if generate_api_shims.NATIVE_DECLARATIONS.get(api_type) != declaration: + raise SystemExit(f"issue 79 native declaration is missing for {api_type}") + members = catalog_members() + missing = sorted(members - set(generate_api_shims.NATIVE_MEMBER_BODIES)) + if missing: + raise SystemExit("issue 79 native members missing: " + ", ".join(missing)) + + source = SOURCE.read_text() + if STUB_RE.search(source): + raise SystemExit("issue 79 owned Rust stubs remain in state.rs") + generated = GENERATED.read_text() + for api_type in TYPES: + if STUB_RE.search(generated_type_block(generated, api_type)): + raise SystemExit(f"issue 79 owned generated stubs remain for {api_type}") + + require_markers(SOURCE, ( + "MAX_RLV_INVENTORY_FOLDERS", "MAX_RLV_INVENTORY_ITEMS", + "MAX_RLV_RESTRICTIONS", "pub struct InventoryMap", + "pub struct RlvRestrictionManager", "pub struct RlvPermissionsService", + "pub struct CameraRestrictions", "pub struct LockedFolderPublic", + "pub struct RlvBlacklist", "fn build_locked_folders", + "fn secure_allowed", "fn rebuild_locked_folders", "fn emit", + "state.revision", "Arc