//! Pure RLV message parsing and typed protocol values. #![allow(clippy::missing_errors_doc)] // Error variants carry stable source locations. #![allow(clippy::missing_panics_doc)] // Parser code contains no intentional panics. #![allow(clippy::must_use_candidate)] // Mapped methods preserve their source signatures. #![allow(clippy::needless_pass_by_value)] // Owned values mirror mapped C# signatures. #![allow(clippy::option_option)] // Nullable out parameters require three states. #![allow(clippy::unnecessary_wraps)] // Mapped constructors are fallible at the API boundary. #![allow(clippy::cast_possible_truncation)] // Mapped numeric object coercion intentionally narrows to Single. use std::fmt; use std::hash::{Hash, Hasher}; use libremetaverse_types::compat::{Guid, ImmutableList, Object}; use libremetaverse_types::{Error, UUID}; use crate::{RlvAttachmentPoint, RlvRestrictionType, RlvWearableType}; /// Maximum accepted chat/instant-message payload size. pub const MAX_RLV_MESSAGE_BYTES: usize = 64 * 1024; /// Maximum comma-separated commands accepted in one payload. pub const MAX_RLV_COMMANDS: usize = 128; /// Byte location in the original UTF-8 message. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct RlvSourceSpan { pub start: usize, pub end: usize, } /// Stable malformed-input categories exposed by the native parser. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum RlvParseErrorKind { MessageTooLong, MissingPrefix, TooManyCommands, EmptyCommand, MissingEquals, EmptyBehavior, EmptyParameter, UnknownAction, UnknownRestriction, UnknownQuery, UnsupportedParameter, InvalidOption, InvalidNumber, InvalidUuid, InvalidChannel, ZeroQueryChannel, } /// A typed parse failure with command index and exact byte span. #[derive(Clone, Debug, Eq, PartialEq)] pub struct RlvParseError { pub kind: RlvParseErrorKind, pub command_index: usize, pub span: RlvSourceSpan, } impl fmt::Display for RlvParseError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, "RLV {:?} at command {} bytes {}..{}", self.kind, self.command_index, self.span.start, self.span.end ) } } impl std::error::Error for RlvParseError {} /// Parsed RLV scalar with protocol type retained. #[derive(Clone, Debug)] pub enum RlvValue { Integer(i32), Real(f32), Uuid(Guid), String(String), AttachmentPoint(RlvAttachmentPoint), WearableType(RlvWearableType), Object(Object), } impl PartialEq for RlvValue { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Integer(left), Self::Integer(right)) => left == right, (Self::Real(left), Self::Real(right)) => left.to_bits() == right.to_bits(), (Self::Uuid(left), Self::Uuid(right)) => left == right, (Self::String(left), Self::String(right)) => left == right, (Self::AttachmentPoint(left), Self::AttachmentPoint(right)) => left == right, (Self::WearableType(left), Self::WearableType(right)) => left == right, (Self::Object(left), Self::Object(right)) => left == right, _ => false, } } } impl Eq for RlvValue {} impl Hash for RlvValue { fn hash(&self, state: &mut H) { std::mem::discriminant(self).hash(state); match self { Self::Integer(value) => value.hash(state), Self::Real(value) => value.to_bits().hash(state), Self::Uuid(value) => value.hash(state), Self::String(value) => value.hash(state), Self::AttachmentPoint(value) => value.hash(state), Self::WearableType(value) => value.hash(state), Self::Object(value) => value.hash(state), } } } impl fmt::Display for RlvValue { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Integer(value) => value.fmt(formatter), Self::Real(value) => value.fmt(formatter), Self::Uuid(value) => formatter.write_str(&guid_to_string(*value)), Self::String(value) => formatter.write_str(value), Self::AttachmentPoint(value) => write!(formatter, "{value:?}"), Self::WearableType(value) => write!(formatter, "{value:?}"), Self::Object(value) => write!(formatter, "{value:?}"), } } } /// Restriction add/remove operation selected by the RLV parameter. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum RlvRestrictionOperation { Add, Remove, } /// A UUID, wearable, attachment point, folder path, or implicit sender target. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RlvTarget { Sender, Uuid(Guid), AttachmentPoint(RlvAttachmentPoint), WearableType(RlvWearableType), FolderPath(String), } /// A group selected either by UUID or by its case-preserved name. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RlvGroupTarget { Uuid(Guid), Name(String), } /// Fully parsed side-effect-free action request. #[derive(Clone, Debug, PartialEq)] pub enum RlvAction { SetRotation(f32), AdjustHeight { distance: f32, factor: f32, delta: f32, }, SetCameraFov(f32), Teleport { x: f32, y: f32, z: f32, region: Option, look_at: Option, }, Sit(Guid), Unsit, SitGround, RemoveOutfit(RlvTarget), DetachMe, RemoveAttachment(RlvTarget), DetachAll { folder_path: String, }, DetachThis { target: RlvTarget, recursive: bool, }, SetGroup { target: RlvGroupTarget, role: Option, }, SetDebug { name: String, value: String, }, SetEnvironment { name: String, value: String, }, Attach { folder_path: String, replace: bool, recursive: bool, }, AttachThis { target: RlvTarget, replace: bool, recursive: bool, }, } /// Camera value requested by a numeric-channel query. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum RlvCameraQuery { AvatarDistanceMin, AvatarDistanceMax, FovMin, FovMax, ZoomMin, CurrentFov, } /// Fully parsed side-effect-free query request. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RlvQuery { Version { modern: bool, }, VersionNumber { include_blacklist: bool, }, Blacklist { filter: String, }, Status { all_senders: bool, filter: String, separator: String, }, Camera(RlvCameraQuery), SitId, Outfit(Option), Attachment(Option), Inventory { path: String, worn: bool, }, FindFolders { first_only: bool, terms: Vec, separator: String, }, Path { legacy: bool, target: RlvTarget, }, Group, Debug(String), Environment(String), } /// Classified meaning of one syntactically valid RLV command. #[derive(Clone, Debug, PartialEq)] pub enum RlvDirective { Clear, Action(RlvAction), Restriction { behavior: RlvRestrictionType, operation: RlvRestrictionOperation, values: Vec, }, Query { channel: i32, query: RlvQuery, }, } /// One parsed command, retaining original and normalized token forms. #[derive(Clone, Debug, PartialEq)] pub struct RlvCommand { pub raw_behavior: String, pub behavior: String, pub option: String, pub raw_parameter: String, pub parameter: String, pub sender: Guid, pub sender_name: String, pub source: RlvSourceSpan, pub directive: RlvDirective, } /// Parsed comma-separated RLV message. #[derive(Clone, Debug, PartialEq)] pub struct RlvMessage { pub commands: Vec, } /// Pure parser for RLV chat messages. pub struct RlvParser; impl RlvParser { pub fn parse_message( message: &str, sender: Guid, sender_name: impl Into, ) -> Result { if message.len() > MAX_RLV_MESSAGE_BYTES { return Err(parse_error( RlvParseErrorKind::MessageTooLong, 0, 0, message.len(), )); } if !message.starts_with('@') { return Err(parse_error( RlvParseErrorKind::MissingPrefix, 0, 0, message.len(), )); } let body = &message[1..]; let count = body.bytes().filter(|byte| *byte == b',').count() + 1; if count > MAX_RLV_COMMANDS { return Err(parse_error( RlvParseErrorKind::TooManyCommands, 0, 1, message.len(), )); } let sender_name = sender_name.into(); let mut commands = Vec::with_capacity(count); let mut relative_start = 0usize; for (command_index, raw) in body.split(',').enumerate() { let start = 1 + relative_start; let end = start + raw.len(); let span = RlvSourceSpan { start, end }; commands.push(parse_command( raw, sender, &sender_name, command_index, span, )?); relative_start += raw.len() + 1; } Ok(RlvMessage { commands }) } } fn parse_command( raw: &str, sender: Guid, sender_name: &str, command_index: usize, span: RlvSourceSpan, ) -> Result { if raw.is_empty() { return Err(error_for( RlvParseErrorKind::EmptyCommand, command_index, span, )); } if raw.eq_ignore_ascii_case("clear") { return Ok(RlvCommand { raw_behavior: raw.to_owned(), behavior: "clear".to_owned(), option: String::new(), raw_parameter: String::new(), parameter: String::new(), sender, sender_name: sender_name.to_owned(), source: span, directive: RlvDirective::Clear, }); } let equals = raw .find('=') .ok_or_else(|| error_for(RlvParseErrorKind::MissingEquals, command_index, span))?; let left = &raw[..equals]; let raw_parameter = &raw[equals + 1..]; if raw_parameter.is_empty() { return Err(error_for( RlvParseErrorKind::EmptyParameter, command_index, span, )); } let (raw_behavior, option) = left .split_once(':') .map_or((left, ""), |(behavior, option)| (behavior, option)); if raw_behavior.is_empty() || raw_behavior.contains(':') || option.contains('=') { return Err(error_for( RlvParseErrorKind::EmptyBehavior, command_index, span, )); } let behavior = raw_behavior.to_lowercase(); let parameter = raw_parameter.to_lowercase(); let directive = classify(&behavior, option, ¶meter) .map_err(|kind| error_for(kind, command_index, span))?; Ok(RlvCommand { raw_behavior: raw_behavior.to_owned(), behavior, option: option.to_owned(), raw_parameter: raw_parameter.to_owned(), parameter, sender, sender_name: sender_name.to_owned(), source: span, directive, }) } fn classify( behavior: &str, option: &str, parameter: &str, ) -> Result { match parameter { "force" => parse_action(behavior, option).map(RlvDirective::Action), "n" | "add" | "y" | "rem" => { let restriction = restriction_from_name(behavior).ok_or(RlvParseErrorKind::UnknownRestriction)?; let values = parse_restriction_values(restriction, option)?; let operation = if matches!(parameter, "n" | "add") { RlvRestrictionOperation::Add } else { RlvRestrictionOperation::Remove }; Ok(RlvDirective::Restriction { behavior: restriction, operation, values, }) } _ => { let channel = parse_i32(parameter).ok_or(RlvParseErrorKind::UnsupportedParameter)?; if channel == 0 { return Err(RlvParseErrorKind::ZeroQueryChannel); } let query = parse_query(behavior, option)?; Ok(RlvDirective::Query { channel, query }) } } } fn parse_action(behavior: &str, option: &str) -> Result { match behavior { "setrot" => Ok(RlvAction::SetRotation(parse_f32(option)?)), "adjustheight" => parse_adjust_height(option), "setcam_fov" => Ok(RlvAction::SetCameraFov(parse_f32(option)?)), "tpto" => parse_teleport(option), "sit" => Ok(RlvAction::Sit(parse_guid(option)?)), "unsit" => Ok(RlvAction::Unsit), "sitground" => Ok(RlvAction::SitGround), "remoutfit" => Ok(RlvAction::RemoveOutfit(parse_general_target(option))), "detachme" => Ok(RlvAction::DetachMe), "remattach" | "detach" => Ok(RlvAction::RemoveAttachment(parse_general_target(option))), "detachall" => Ok(RlvAction::DetachAll { folder_path: option.to_owned(), }), "detachthis" => Ok(RlvAction::DetachThis { target: parse_this_target(option)?, recursive: false, }), "detachallthis" => Ok(RlvAction::DetachThis { target: parse_this_target(option)?, recursive: true, }), "setgroup" => parse_group(option), _ if behavior.starts_with("setdebug_") => parse_setting(behavior, option, true), _ if behavior.starts_with("setenv_") => parse_setting(behavior, option, false), "attach" | "addoutfit" | "attachoverorreplace" => Ok(RlvAction::Attach { folder_path: option.to_owned(), replace: true, recursive: false, }), "attachall" | "addoutfitall" | "attachalloverorreplace" => Ok(RlvAction::Attach { folder_path: option.to_owned(), replace: true, recursive: true, }), "attachover" | "addoutfitover" => Ok(RlvAction::Attach { folder_path: option.to_owned(), replace: false, recursive: false, }), "attachallover" | "addoutfitallover" => Ok(RlvAction::Attach { folder_path: option.to_owned(), replace: false, recursive: true, }), "attachthis" | "addoutfitthis" | "attachthisoverorreplace" => Ok(RlvAction::AttachThis { target: parse_this_target(option)?, replace: true, recursive: false, }), "attachallthis" | "addoutfitallthis" | "attachallthisoverorreplace" => { Ok(RlvAction::AttachThis { target: parse_this_target(option)?, replace: true, recursive: true, }) } "attachthisover" | "addoutfitthisover" => Ok(RlvAction::AttachThis { target: parse_this_target(option)?, replace: false, recursive: false, }), "attachallthisover" | "addoutfitallthisover" => Ok(RlvAction::AttachThis { target: parse_this_target(option)?, replace: false, recursive: true, }), _ => Err(RlvParseErrorKind::UnknownAction), } } fn parse_adjust_height(option: &str) -> Result { let parts = nonempty_parts(option); let Some(distance) = parts.first().and_then(|value| parse_f32_value(value)) else { return Err(RlvParseErrorKind::InvalidNumber); }; let factor = parts .get(1) .and_then(|value| parse_f32_value(value)) .unwrap_or(1.0); let delta = parts .get(2) .and_then(|value| parse_f32_value(value)) .unwrap_or(0.0); Ok(RlvAction::AdjustHeight { distance, factor, delta, }) } fn parse_teleport(option: &str) -> Result { let parts = nonempty_parts(option); let location = parts.first().ok_or(RlvParseErrorKind::InvalidOption)?; let location: Vec<_> = location.split('/').collect(); if !(3..=4).contains(&location.len()) { return Err(RlvParseErrorKind::InvalidOption); } let base = usize::from(location.len() == 4); let region = (base == 1).then(|| location[0].to_owned()); let x = parse_f32(location[base])?; let y = parse_f32(location[base + 1])?; let z = parse_f32(location[base + 2])?; let look_at = parts.get(1).map(|value| parse_f32(value)).transpose()?; Ok(RlvAction::Teleport { x, y, z, region, look_at, }) } fn parse_group(option: &str) -> Result { let parts = nonempty_parts(option); let name = parts.first().ok_or(RlvParseErrorKind::InvalidOption)?; let target = parse_guid(name).map_or_else( |_| RlvGroupTarget::Name((*name).to_owned()), RlvGroupTarget::Uuid, ); Ok(RlvAction::SetGroup { target, role: parts.get(1).map(|value| (*value).to_owned()), }) } fn parse_setting( behavior: &str, option: &str, debug: bool, ) -> Result { let name = behavior .split_once('_') .map(|(_, name)| name) .unwrap_or_default(); if name.is_empty() { return Err(RlvParseErrorKind::InvalidOption); } if debug { Ok(RlvAction::SetDebug { name: name.to_owned(), value: option.to_owned(), }) } else { Ok(RlvAction::SetEnvironment { name: name.to_owned(), value: option.to_owned(), }) } } fn parse_general_target(option: &str) -> RlvTarget { if option.is_empty() { RlvTarget::Sender } else if let Ok(value) = parse_guid(option) { RlvTarget::Uuid(value) } else if let Some(value) = wearable_from_name(option) { RlvTarget::WearableType(value) } else if let Some(value) = attachment_from_name(option) { RlvTarget::AttachmentPoint(value) } else { RlvTarget::FolderPath(option.to_owned()) } } fn parse_this_target(option: &str) -> Result { let target = parse_general_target(option); if matches!(target, RlvTarget::FolderPath(_)) { Err(RlvParseErrorKind::InvalidOption) } else { Ok(target) } } fn parse_query(behavior: &str, option: &str) -> Result { let query = match behavior { "version" => RlvQuery::Version { modern: false }, "versionnew" => RlvQuery::Version { modern: true }, "versionnum" => RlvQuery::VersionNumber { include_blacklist: false, }, "versionnumbl" => RlvQuery::VersionNumber { include_blacklist: true, }, "getblacklist" => RlvQuery::Blacklist { filter: option.to_owned(), }, "getstatus" | "getstatusall" => { let mut parts = option.split(';'); let filter = parts.next().unwrap_or_default().to_lowercase(); let separator = parts.next().unwrap_or("/").to_owned(); RlvQuery::Status { all_senders: behavior == "getstatusall", filter, separator, } } "getcam_avdistmin" => RlvQuery::Camera(RlvCameraQuery::AvatarDistanceMin), "getcam_avdistmax" => RlvQuery::Camera(RlvCameraQuery::AvatarDistanceMax), "getcam_fovmin" => RlvQuery::Camera(RlvCameraQuery::FovMin), "getcam_fovmax" => RlvQuery::Camera(RlvCameraQuery::FovMax), "getcam_zoommin" => RlvQuery::Camera(RlvCameraQuery::ZoomMin), "getcam_fov" => RlvQuery::Camera(RlvCameraQuery::CurrentFov), "getsitid" => RlvQuery::SitId, "getoutfit" => RlvQuery::Outfit(wearable_from_name(option)), "getattach" => RlvQuery::Attachment(attachment_from_name(option)), "getinv" => RlvQuery::Inventory { path: option.to_owned(), worn: false, }, "getinvworn" => RlvQuery::Inventory { path: option.to_owned(), worn: true, }, "findfolder" | "findfolders" => { let mut parts = option.split(';'); let terms = parts .next() .unwrap_or_default() .split("&&") .filter(|value| !value.is_empty()) .map(str::to_owned) .collect(); let separator = parts.next().unwrap_or(",").to_owned(); RlvQuery::FindFolders { first_only: behavior == "findfolder", terms, separator, } } "getpath" | "getpathnew" => RlvQuery::Path { legacy: behavior == "getpath", target: parse_query_path_target(option)?, }, "getgroup" => RlvQuery::Group, _ if behavior.starts_with("getdebug_") => { RlvQuery::Debug(behavior["getdebug_".len()..].to_owned()) } _ if behavior.starts_with("getenv_") => { RlvQuery::Environment(behavior["getenv_".len()..].to_owned()) } _ => return Err(RlvParseErrorKind::UnknownQuery), }; Ok(query) } fn parse_query_path_target(option: &str) -> Result { let parts = nonempty_parts(option); match parts.as_slice() { [] => Ok(RlvTarget::Sender), [value] => parse_this_target(value), _ => Err(RlvParseErrorKind::InvalidOption), } } #[allow(clippy::too_many_lines)] // One exhaustive match documents every protocol option grammar. fn parse_restriction_values( behavior: RlvRestrictionType, option: &str, ) -> Result, RlvParseErrorKind> { use RlvRestrictionType as T; let args = nonempty_parts(option); let first_float = || { args.first() .ok_or(RlvParseErrorKind::InvalidOption) .and_then(|value| parse_f32(value)) .map(|value| vec![RlvValue::Real(value)]) }; let one_int = || { one(&args) .and_then(|value| parse_i32(value).ok_or(RlvParseErrorKind::InvalidNumber)) .map(|value| vec![RlvValue::Integer(value)]) }; let optional_guid = || { optional_one(&args) .and_then(|value| value.map(parse_guid).transpose()) .map(|value| value.into_iter().map(RlvValue::Uuid).collect()) }; match behavior { T::Notify => { let channel = args .first() .and_then(|value| parse_i32(value)) .ok_or(RlvParseErrorKind::InvalidChannel)?; let mut values = vec![RlvValue::Integer(channel)]; if args.len() == 2 { values.push(RlvValue::String(args[1].to_owned())); } Ok(values) } T::CamDrawMin | T::CamDrawMax => { let values = first_float()?; if matches!(values.first(), Some(RlvValue::Real(value)) if *value < 0.40) { Err(RlvParseErrorKind::InvalidOption) } else { Ok(values) } } T::CamZoomMax | T::CamZoomMin | T::SetCamFovMin | T::SetCamFovMax | T::CamDistMax | T::SetCamAvDistMax | T::CamDistMin | T::SetCamAvDistMin | T::CamDrawAlphaMin | T::CamDrawAlphaMax | T::CamAvDist => first_float(), T::SitTp | T::FarTouch | T::TouchFar | T::TpLocal => optional_one(&args) .and_then(|value| value.map(parse_f32).transpose()) .map(|value| value.into_iter().map(RlvValue::Real).collect()), T::CamDrawColor => { if args.len() != 3 { return Err(RlvParseErrorKind::InvalidOption); } args.iter() .map(|value| parse_f32(value).map(RlvValue::Real)) .collect() } T::RedirChat | T::RedirEmote | T::SendChannelExcept => one_int(), T::SendChannel | T::SendChannelSec => optional_one(&args) .and_then(|value| { value .map(|value| parse_i32(value).ok_or(RlvParseErrorKind::InvalidNumber)) .transpose() }) .map(|value| value.into_iter().map(RlvValue::Integer).collect()), T::SendImTo | T::RecvImFrom => one(&args).map(|value| { vec![ parse_guid(value) .map_or_else(|_| RlvValue::String(value.to_owned()), RlvValue::Uuid), ] }), T::SendIm | T::RecvIm => optional_one(&args).map(|value| { value .map(|value| { parse_guid(value) .map_or_else(|_| RlvValue::String(value.to_owned()), RlvValue::Uuid) }) .into_iter() .collect() }), T::Detach | T::AddAttach | T::RemAttach => optional_one(&args) .and_then(|value| { value .map(|value| { attachment_from_name(value).ok_or(RlvParseErrorKind::InvalidOption) }) .transpose() }) .map(|value| value.into_iter().map(RlvValue::AttachmentPoint).collect()), T::AddOutfit | T::RemOutfit => optional_one(&args) .and_then(|value| { value .map(|value| wearable_from_name(value).ok_or(RlvParseErrorKind::InvalidOption)) .transpose() }) .map(|value| value.into_iter().map(RlvValue::WearableType).collect()), T::DetachThis | T::DetachAllThis | T::AttachThis | T::AttachAllThis => optional_one(&args) .map(|value| { value .map(|value| { wearable_from_name(value) .map(RlvValue::WearableType) .or_else(|| attachment_from_name(value).map(RlvValue::AttachmentPoint)) .unwrap_or_else(|| RlvValue::String(value.to_owned())) }) .into_iter() .collect() }), T::DetachThisExcept | T::DetachAllThisExcept | T::AttachThisExcept | T::AttachAllThisExcept => { one(&args).map(|value| vec![RlvValue::String(value.to_owned())]) } T::CamTextures | T::SetCamTextures | T::RecvChat | T::RecvEmote | T::StartIm | T::TpLure | T::AcceptTp | T::AcceptTpRequest | T::TpRequest | T::Edit | T::Share | T::TouchWorld | T::TouchAttachOther | T::TouchHud | T::ShowNames | T::ShowNamesSec | T::ShowNameTags => optional_guid(), T::RecvChatFrom | T::RecvEmoteFrom | T::StartImTo | T::EditObj | T::TouchThis | T::ShowHoverText => one(&args) .and_then(parse_guid) .map(|value| vec![RlvValue::Uuid(value)]), _ if restriction_takes_no_values(behavior) => { if args.is_empty() { Ok(Vec::new()) } else { Err(RlvParseErrorKind::InvalidOption) } } _ => Err(RlvParseErrorKind::InvalidOption), } } fn restriction_takes_no_values(value: RlvRestrictionType) -> bool { use RlvRestrictionType as T; matches!( value, T::Permissive | T::SendChat | T::ChatShout | T::ChatNormal | T::ChatWhisper | T::Emote | T::RecvChatSec | T::RecvEmoteSec | T::SendGesture | T::SendImSec | T::RecvImSec | T::TpLureSec | T::TpRequestSec | T::ShareSec | T::Fly | T::Jump | T::TempRun | T::AlwaysRun | T::CamUnlock | T::SetCamUnlock | T::TpLm | T::TpLoc | T::StandTp | T::ShowInv | T::ViewNote | T::ViewScript | T::ViewTexture | T::Unsit | T::Sit | T::DefaultWear | T::SetGroup | T::SetDebug | T::SetEnv | T::AllowIdle | T::ShowWorldMap | T::ShowMiniMap | T::ShowLoc | T::ShowNearby | T::EditWorld | T::EditAttach | T::Rez | T::DenyPermission | T::AcceptPermission | T::UnsharedWear | T::UnsharedUnwear | T::SharedWear | T::SharedUnwear | T::TouchAll | T::TouchMe | T::TouchAttach | T::TouchAttachSelf | T::Interact | T::ShowHoverTextAll | T::ShowHoverTextHud | T::ShowHoverTextWorld ) } /// Native mapped RLV helpers. pub struct RlvCommon; impl RlvCommon { pub(crate) fn native_try_get_attachment_point_from_item_name( item_name: String, output: &mut Option>, ) -> bool { *output = None; let mut remaining = item_name.as_str(); let mut found = None; while let Some(open) = remaining.find('(') { let after_open = &remaining[open + 1..]; let Some(close) = after_open.find(')') else { break; }; if let Some(point) = attachment_from_name(&after_open[..close]) { found = Some(point); } remaining = &after_open[close + 1..]; } if let Some(point) = found { *output = Some(Some(point)); true } else { false } } } /// Native mapped immutable restriction value. #[derive(Clone, Debug)] pub struct RlvRestriction { behavior: RlvRestrictionType, original_behavior: RlvRestrictionType, sender: Guid, sender_name: String, args: ImmutableList, values: Vec, } impl RlvRestriction { pub(crate) fn native_new( behavior: RlvRestrictionType, sender: Guid, sender_name: String, args: Vec, ) -> Result { let values = args .iter() .cloned() .map(|value| value_from_object(behavior, value)) .collect(); Ok(Self::from_parts( behavior, sender, sender_name, args.into(), values, )) } pub fn from_values( original_behavior: RlvRestrictionType, sender: Guid, sender_name: String, values: Vec, ) -> Self { let args = values .iter() .cloned() .map(value_to_object) .collect::>() .into(); Self::from_parts(original_behavior, sender, sender_name, args, values) } fn from_parts( original_behavior: RlvRestrictionType, sender: Guid, sender_name: String, args: ImmutableList, values: Vec, ) -> Self { let mut behavior = real_restriction(original_behavior); if behavior == RlvRestrictionType::SendChannelSec && args.len() == 1 { behavior = RlvRestrictionType::SendChannel; } else if behavior == RlvRestrictionType::ShowNamesSec && args.len() == 1 { behavior = RlvRestrictionType::ShowNames; } Self { behavior, original_behavior, sender, sender_name, args, values, } } pub fn values(&self) -> &[RlvValue] { &self.values } pub(crate) fn native_equals(&self, obj: Option) -> bool { obj.as_ref().and_then(Object::downcast_ref::) == Some(self) } pub(crate) fn native_get_hash_code(&self) -> i32 { let mut hash = StableHasher::default(); self.hash(&mut hash); let bytes = hash.finish().to_le_bytes(); i32::from_le_bytes([ bytes[0] ^ bytes[4], bytes[1] ^ bytes[5], bytes[2] ^ bytes[6], bytes[3] ^ bytes[7], ]) } pub(crate) fn native_to_string(&self) -> String { let args = self .args .iter() .map(object_to_string) .collect::>() .join(", "); format!( "RlvRestriction: Behavior={:?} SenderName=\"{}\" Args=[{}]", self.behavior, self.sender_name, args ) } pub(crate) fn native_args(&self) -> ImmutableList { self.args.clone() } pub(crate) const fn native_behavior(&self) -> RlvRestrictionType { self.behavior } pub(crate) fn native_is_exception(&self) -> bool { is_exception(self.behavior, self.args.len()) } pub(crate) const fn native_original_behavior(&self) -> RlvRestrictionType { self.original_behavior } pub(crate) const fn native_sender(&self) -> Guid { self.sender } pub(crate) fn native_sender_name(&self) -> String { self.sender_name.clone() } } impl PartialEq for RlvRestriction { fn eq(&self, other: &Self) -> bool { self.behavior == other.behavior && self.sender == other.sender && self.args == other.args } } impl Eq for RlvRestriction {} impl Hash for RlvRestriction { fn hash(&self, state: &mut H) { self.behavior.hash(state); self.sender.hash(state); self.args.hash(state); } } fn is_exception(behavior: RlvRestrictionType, value_count: usize) -> bool { use RlvRestrictionType as T; matches!( behavior, T::DetachThisExcept | T::DetachAllThisExcept | T::AttachThisExcept | T::AttachAllThisExcept ) || (value_count > 0 && matches!( behavior, T::RecvEmote | T::RecvChat | T::SendIm | T::StartIm | T::RecvIm | T::SendChannel | T::TpRequest | T::TpLure | T::Edit | T::Share | T::TouchWorld | T::ShowNamesSec | T::ShowNames | T::ShowNameTags | T::AcceptTp | T::AcceptTpRequest )) } const fn real_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, } } macro_rules! restriction_table { ($macro:ident) => { $macro! { "notify" => Notify, "permissive" => Permissive, "fly" => Fly, "jump" => Jump, "temprun" => TempRun, "alwaysrun" => AlwaysRun, "camzoommax" => CamZoomMax, "camzoommin" => CamZoomMin, "camdrawmin" => CamDrawMin, "camdrawmax" => CamDrawMax, "setcam_fovmin" => SetCamFovMin, "setcam_fovmax" => SetCamFovMax, "camdistmax" => CamDistMax, "camdistmin" => CamDistMin, "camdrawalphamin" => CamDrawAlphaMin, "camdrawalphamax" => CamDrawAlphaMax, "setcam_avdistmax" => SetCamAvDistMax, "setcam_avdistmin" => SetCamAvDistMin, "camdrawcolor" => CamDrawColor, "camunlock" => CamUnlock, "setcam_unlock" => SetCamUnlock, "camavdist" => CamAvDist, "camtextures" => CamTextures, "setcam_textures" => SetCamTextures, "sendchat" => SendChat, "chatshout" => ChatShout, "chatnormal" => ChatNormal, "chatwhisper" => ChatWhisper, "redirchat" => RedirChat, "recvchat" => RecvChat, "recvchat_sec" => RecvChatSec, "recvchatfrom" => RecvChatFrom, "sendgesture" => SendGesture, "emote" => Emote, "rediremote" => RedirEmote, "recvemote" => RecvEmote, "recvemotefrom" => RecvEmoteFrom, "recvemote_sec" => RecvEmoteSec, "sendchannel" => SendChannel, "sendchannel_sec" => SendChannelSec, "sendchannel_except" => SendChannelExcept, "sendim" => SendIm, "sendim_sec" => SendImSec, "sendimto" => SendImTo, "startim" => StartIm, "startimto" => StartImTo, "recvim" => RecvIm, "recvim_sec" => RecvImSec, "recvimfrom" => RecvImFrom, "tplocal" => TpLocal, "tplm" => TpLm, "tploc" => TpLoc, "tplure" => TpLure, "tplure_sec" => TpLureSec, "sittp" => SitTp, "standtp" => StandTp, "accepttp" => AcceptTp, "accepttprequest" => AcceptTpRequest, "tprequest" => TpRequest, "tprequest_sec" => TpRequestSec, "showinv" => ShowInv, "viewnote" => ViewNote, "viewscript" => ViewScript, "viewtexture" => ViewTexture, "edit" => Edit, "rez" => Rez, "editobj" => EditObj, "editworld" => EditWorld, "editattach" => EditAttach, "share" => Share, "share_sec" => ShareSec, "unsit" => Unsit, "sit" => Sit, "detach" => Detach, "addattach" => AddAttach, "remattach" => RemAttach, "defaultwear" => DefaultWear, "addoutfit" => AddOutfit, "remoutfit" => RemOutfit, "acceptpermission" => AcceptPermission, "denypermission" => DenyPermission, "unsharedwear" => UnsharedWear, "unsharedunwear" => UnsharedUnwear, "sharedwear" => SharedWear, "sharedunwear" => SharedUnwear, "detachthis" => DetachThis, "detachallthis" => DetachAllThis, "attachthis" => AttachThis, "attachallthis" => AttachAllThis, "detachthis_except" => DetachThisExcept, "detachallthis_except" => DetachAllThisExcept, "attachthis_except" => AttachThisExcept, "attachallthis_except" => AttachAllThisExcept, "fartouch" => FarTouch, "touchfar" => TouchFar, "touchall" => TouchAll, "touchworld" => TouchWorld, "touchthis" => TouchThis, "touchme" => TouchMe, "touchattach" => TouchAttach, "touchattachself" => TouchAttachSelf, "touchattachother" => TouchAttachOther, "touchhud" => TouchHud, "interact" => Interact, "showworldmap" => ShowWorldMap, "showminimap" => ShowMiniMap, "showloc" => ShowLoc, "shownames" => ShowNames, "shownames_sec" => ShowNamesSec, "shownametags" => ShowNameTags, "shownearby" => ShowNearby, "showhovertextall" => ShowHoverTextAll, "showhovertext" => ShowHoverText, "showhovertexthud" => ShowHoverTextHud, "showhovertextworld" => ShowHoverTextWorld, "setgroup" => SetGroup, "setdebug" => SetDebug, "setenv" => SetEnv, "allowidle" => AllowIdle } }; } macro_rules! make_restriction_lookup { ($($name:literal => $variant:ident),+ $(,)?) => { /// Complete pinned behavior-name table in protocol order. pub const RLV_RESTRICTION_NAMES: &[(&str, RlvRestrictionType)] = &[ $(($name, RlvRestrictionType::$variant),)+ ]; /// Maps the exact RLV behavior spelling to its typed restriction. pub fn restriction_from_name(name: &str) -> Option { match name.to_ascii_lowercase().as_str() { $($name => Some(RlvRestrictionType::$variant),)+ _ => None, } } /// Returns the canonical protocol spelling for a restriction. pub const fn restriction_name(value: RlvRestrictionType) -> &'static str { match value { $(RlvRestrictionType::$variant => $name,)+ } } }; } restriction_table!(make_restriction_lookup); /// Maps a case-insensitive attachment alias without trimming or punctuation changes. pub fn attachment_from_name(name: &str) -> Option { use RlvAttachmentPoint as P; Some(match name.to_ascii_lowercase().as_str() { "none" => P::Default, "chest" => P::Chest, "skull" => P::Skull, "left shoulder" => P::LeftShoulder, "right shoulder" => P::RightShoulder, "left hand" => P::LeftHand, "right hand" => P::RightHand, "left foot" => P::LeftFoot, "right foot" => P::RightFoot, "spine" => P::Spine, "pelvis" => P::Pelvis, "mouth" => P::Mouth, "chin" => P::Chin, "left ear" => P::LeftEar, "right ear" => P::RightEar, "left eyeball" => P::LeftEyeball, "right eyeball" => P::RightEyeball, "nose" => P::Nose, "r upper arm" => P::RightUpperArm, "r forearm" => P::RightForearm, "l upper arm" => P::LeftUpperArm, "l forearm" => P::LeftForearm, "right hip" => P::RightHip, "r upper leg" => P::RightUpperLeg, "r lower leg" => P::RightLowerLeg, "left hip" => P::LeftHip, "l upper leg" => P::LeftUpperLeg, "l lower leg" => P::LeftLowerLeg, "stomach" => P::Stomach, "left pec" => P::LeftPec, "right pec" => P::RightPec, "center 2" => P::HUDCenter2, "top right" => P::HUDTopRight, "top" => P::HUDTop, "top left" => P::HUDTopLeft, "center" => P::HUDCenter, "bottom left" => P::HUDBottomLeft, "bottom" => P::HUDBottom, "bottom right" => P::HUDBottomRight, "neck" => P::Neck, "root" | "avatar center" => P::AvatarCenter, "left ring finger" => P::LeftHandRing, "right ring finger" => P::RightHandRing, "tail base" => P::TailBase, "tail tip" => P::TailTip, "left wing" => P::LeftWing, "right wing" => P::RightWing, "jaw" => P::Jaw, "alt left ear" => P::AltLeftEar, "alt right ear" => P::AltRightEar, "alt left eye" => P::AltLeftEye, "alt right eye" => P::AltRightEye, "tongue" => P::Tongue, "groin" => P::Groin, "left hind foot" => P::LeftHindFoot, "right hind foot" => P::RightHindFoot, _ => return None, }) } /// Maps a case-insensitive wearable alias without trimming. pub fn wearable_from_name(name: &str) -> Option { use RlvWearableType as W; Some(match name.to_ascii_lowercase().as_str() { "gloves" => W::Gloves, "jacket" => W::Jacket, "pants" => W::Pants, "shirt" => W::Shirt, "shoes" => W::Shoes, "skirt" => W::Skirt, "socks" => W::Socks, "underpants" => W::Underpants, "undershirt" => W::Undershirt, "skin" => W::Skin, "eyes" => W::Eyes, "hair" => W::Hair, "shape" => W::Shape, "alpha" => W::Alpha, "tattoo" => W::Tattoo, "physics" => W::Physics, "universal" => W::Universal, _ => return None, }) } fn one<'a>(args: &[&'a str]) -> Result<&'a str, RlvParseErrorKind> { if let [value] = args { Ok(value) } else { Err(RlvParseErrorKind::InvalidOption) } } fn optional_one<'a>(args: &[&'a str]) -> Result, RlvParseErrorKind> { match args { [] => Ok(None), [value] => Ok(Some(value)), _ => Err(RlvParseErrorKind::InvalidOption), } } fn nonempty_parts(value: &str) -> Vec<&str> { value.split(';').filter(|part| !part.is_empty()).collect() } fn parse_f32(value: &str) -> Result { parse_f32_value(value).ok_or(RlvParseErrorKind::InvalidNumber) } fn parse_f32_value(value: &str) -> Option { value.trim().parse::().ok() } fn parse_i32(value: &str) -> Option { value.trim().parse::().ok() } fn parse_guid(value: &str) -> Result { UUID::parse(value.to_owned()) .map(|value| value.guid()) .map_err(|_| RlvParseErrorKind::InvalidUuid) } fn guid_to_string(value: Guid) -> String { UUID::new_with_guid(value).map_or_else( |_| "00000000-0000-0000-0000-000000000000".to_owned(), |value| value.to_string(), ) } fn value_to_object(value: RlvValue) -> Object { match value { RlvValue::Integer(value) => Object::Integer(value), RlvValue::Real(value) => Object::Real(f64::from(value)), RlvValue::Uuid(value) => UUID::new_with_guid(value).map_or(Object::Undefined, Object::UUID), RlvValue::String(value) => Object::String(value), RlvValue::AttachmentPoint(value) => Object::Integer(value as i32), RlvValue::WearableType(value) => Object::Integer(value as i32), RlvValue::Object(value) => value, } } fn object_to_string(value: &Object) -> String { match value { Object::Undefined => String::new(), Object::Boolean(value) => value.to_string(), Object::Integer(value) => value.to_string(), Object::UInteger(value) => value.to_string(), Object::Long(value) => value.to_string(), Object::ULong(value) => value.to_string(), Object::Real(value) => value.to_string(), Object::String(value) => value.clone(), Object::UUID(value) => value.to_string(), Object::Opaque(value) => format!("{value:?}"), other => format!("{other:?}"), } } fn value_from_object(behavior: RlvRestrictionType, value: Object) -> RlvValue { if let Some(value) = value.downcast_ref::() { return RlvValue::AttachmentPoint(*value); } if let Some(value) = value.downcast_ref::() { return RlvValue::WearableType(*value); } match value { Object::Integer(value) if matches!( behavior, RlvRestrictionType::Detach | RlvRestrictionType::AddAttach | RlvRestrictionType::RemAttach ) => { attachment_from_discriminant(value) .map_or(RlvValue::Integer(value), RlvValue::AttachmentPoint) } Object::Integer(value) if matches!( behavior, RlvRestrictionType::AddOutfit | RlvRestrictionType::RemOutfit ) => { wearable_from_discriminant(value) .map_or(RlvValue::Integer(value), RlvValue::WearableType) } Object::Integer(value) => RlvValue::Integer(value), Object::Real(value) => RlvValue::Real(value as f32), Object::UUID(value) => RlvValue::Uuid(value.guid()), Object::String(value) => RlvValue::String(value), other => RlvValue::Object(other), } } fn attachment_from_discriminant(value: i32) -> Option { (0..=55).find_map(|candidate| { let point = attachment_from_name(restriction_attachment_name(candidate)?)?; (point as i32 == value).then_some(point) }) } fn restriction_attachment_name(value: i32) -> Option<&'static str> { const NAMES: [&str; 56] = [ "none", "chest", "skull", "left shoulder", "right shoulder", "left hand", "right hand", "left foot", "right foot", "spine", "pelvis", "mouth", "chin", "left ear", "right ear", "left eyeball", "right eyeball", "nose", "r upper arm", "r forearm", "l upper arm", "l forearm", "right hip", "r upper leg", "r lower leg", "left hip", "l upper leg", "l lower leg", "stomach", "left pec", "right pec", "center 2", "top right", "top", "top left", "center", "bottom left", "bottom", "bottom right", "neck", "avatar center", "left ring finger", "right ring finger", "tail base", "tail tip", "left wing", "right wing", "jaw", "alt left ear", "alt right ear", "alt left eye", "alt right eye", "tongue", "groin", "left hind foot", "right hind foot", ]; usize::try_from(value) .ok() .and_then(|index| NAMES.get(index).copied()) } fn wearable_from_discriminant(value: i32) -> Option { [ RlvWearableType::Shape, RlvWearableType::Skin, RlvWearableType::Hair, RlvWearableType::Eyes, RlvWearableType::Shirt, RlvWearableType::Pants, RlvWearableType::Shoes, RlvWearableType::Socks, RlvWearableType::Jacket, RlvWearableType::Gloves, RlvWearableType::Undershirt, RlvWearableType::Underpants, RlvWearableType::Skirt, RlvWearableType::Alpha, RlvWearableType::Tattoo, RlvWearableType::Physics, RlvWearableType::Universal, RlvWearableType::Invalid, ] .into_iter() .find(|item| *item as i32 == value) } const fn parse_error( kind: RlvParseErrorKind, command_index: usize, start: usize, end: usize, ) -> RlvParseError { RlvParseError { kind, command_index, span: RlvSourceSpan { start, end }, } } const fn error_for( kind: RlvParseErrorKind, command_index: usize, span: RlvSourceSpan, ) -> RlvParseError { RlvParseError { kind, command_index, span, } } #[derive(Default)] struct StableHasher(u64); impl Hasher for StableHasher { fn finish(&self) -> u64 { self.0 } fn write(&mut self, bytes: &[u8]) { let mut hash = if self.0 == 0 { 0xcbf2_9ce4_8422_2325 } else { self.0 }; for byte in bytes { hash ^= u64::from(*byte); hash = hash.wrapping_mul(0x1000_0000_01b3); } self.0 = hash; } }