#![allow(clippy::float_cmp)] // Fixture values are exactly representable protocol literals. use libremetaverse_rlv::{ RLV_RESTRICTION_NAMES, RlvAction, RlvAttachmentPoint, RlvCameraQuery, RlvCommon, RlvDirective, RlvGroupTarget, RlvParseErrorKind, RlvParser, RlvQuery, RlvRestriction, RlvRestrictionOperation, RlvRestrictionType, RlvTarget, RlvValue, RlvWearableType, attachment_from_name, restriction_from_name, restriction_name, wearable_from_name, }; use libremetaverse_types::UUID; use libremetaverse_types::compat::{Guid, Object}; fn sender() -> Guid { UUID::parse("ffffffff-ffff-4fff-8fff-ffffffffffff".to_owned()) .expect("fixture UUID") .guid() } fn parse(message: &str) -> libremetaverse_rlv::RlvMessage { RlvParser::parse_message(message, sender(), "Sender Name").expect("valid RLV message") } #[test] fn message_preserves_original_casing_options_and_byte_locations() { let parsed = parse("@FlY=ADD,getdebug_RenderMode:MiXeD=+123"); assert_eq!(parsed.commands.len(), 2); let first = &parsed.commands[0]; assert_eq!(first.raw_behavior, "FlY"); assert_eq!(first.behavior, "fly"); assert_eq!(first.raw_parameter, "ADD"); assert_eq!(first.parameter, "add"); assert_eq!(first.source.start, 1); assert_eq!(first.source.end, 8); assert!(matches!( first.directive, RlvDirective::Restriction { behavior: RlvRestrictionType::Fly, operation: RlvRestrictionOperation::Add, ref values, } if values.is_empty() )); let second = &parsed.commands[1]; assert_eq!(second.raw_behavior, "getdebug_RenderMode"); assert_eq!(second.behavior, "getdebug_rendermode"); assert_eq!(second.option, "MiXeD"); assert!(matches!( second.directive, RlvDirective::Query { channel: 123, query: RlvQuery::Debug(ref name), } if name == "rendermode" )); } #[test] fn bare_clear_and_all_parameter_classes_are_typed() { assert!(matches!( parse("@ClEaR").commands[0].directive, RlvDirective::Clear )); assert!(matches!( parse("@unsit=FoRcE").commands[0].directive, RlvDirective::Action(RlvAction::Unsit) )); assert!(matches!( parse("@jump=Y").commands[0].directive, RlvDirective::Restriction { operation: RlvRestrictionOperation::Remove, .. } )); assert!(matches!( parse("@version= 42 ").commands[0].directive, RlvDirective::Query { channel: 42, query: RlvQuery::Version { modern: false } } )); } #[test] fn action_aliases_and_targets_remain_distinct() { let attach = &parse("@addoutfitallover:Clothing/Hats=force").commands[0].directive; assert!(matches!( attach, RlvDirective::Action(RlvAction::Attach { folder_path, replace: false, recursive: true, }) if folder_path == "Clothing/Hats" )); let this = &parse("@attachthisoverorreplace:R UPPER ARM=force").commands[0].directive; assert!(matches!( this, RlvDirective::Action(RlvAction::AttachThis { target: RlvTarget::AttachmentPoint(RlvAttachmentPoint::RightUpperArm), replace: true, recursive: false, }) )); let remove = &parse("@remoutfit:Tattoo=force").commands[0].directive; assert!(matches!( remove, RlvDirective::Action(RlvAction::RemoveOutfit(RlvTarget::WearableType( RlvWearableType::Tattoo ))) )); } #[test] fn numeric_uuid_group_and_teleport_options_are_validated() { let adjusted = &parse("@adjustheight:1.25;2;bad;ignored=force").commands[0].directive; assert!(matches!( adjusted, RlvDirective::Action(RlvAction::AdjustHeight { distance, factor, delta }) if *distance == 1.25 && *factor == 2.0 && *delta == 0.0 )); let teleported = &parse("@tpto:Region Name/1/2/3;4=force").commands[0].directive; assert!(matches!( teleported, RlvDirective::Action(RlvAction::Teleport { x, y, z, region: Some(region), look_at: Some(look_at), }) if *x == 1.0 && *y == 2.0 && *z == 3.0 && region == "Region Name" && *look_at == 4.0 )); let grouped = &parse("@setgroup:My Group;Role Name=force").commands[0].directive; assert!(matches!( grouped, RlvDirective::Action(RlvAction::SetGroup { target: RlvGroupTarget::Name(name), role: Some(role), }) if name == "My Group" && role == "Role Name" )); } #[test] fn query_variants_preserve_filters_separators_paths_and_aliases() { let status = &parse("@getstatus:TP; ! =7").commands[0].directive; assert!(matches!( status, RlvDirective::Query { query: RlvQuery::Status { all_senders: false, filter, separator }, channel: 7, } if filter == "tp" && separator == " ! " )); let folders = &parse("@findfolders:Hat&&Red;|=8").commands[0].directive; assert!(matches!( folders, RlvDirective::Query { query: RlvQuery::FindFolders { first_only: false, terms, separator }, .. } if terms == &["Hat", "Red"] && separator == "|" )); let path = &parse("@getpathnew:root=9").commands[0].directive; assert!(matches!( path, RlvDirective::Query { query: RlvQuery::Path { legacy: false, target: RlvTarget::AttachmentPoint(RlvAttachmentPoint::AvatarCenter), }, .. } )); assert!(matches!( parse("@getcam_fov=10").commands[0].directive, RlvDirective::Query { query: RlvQuery::Camera(RlvCameraQuery::CurrentFov), .. } )); } #[test] fn restriction_options_are_strongly_typed() { let notify = &parse("@notify:1234;sendim=add").commands[0].directive; assert!(matches!( notify, RlvDirective::Restriction { values, .. } if values == &[RlvValue::Integer(1234), RlvValue::String("sendim".to_owned())] )); let attachment = &parse("@detach:R Upper Arm=n").commands[0].directive; assert!(matches!( attachment, RlvDirective::Restriction { values, .. } if values == &[RlvValue::AttachmentPoint(RlvAttachmentPoint::RightUpperArm)] )); let wearable = &parse("@remoutfit:UNIVERSAL=n").commands[0].directive; assert!(matches!( wearable, RlvDirective::Restriction { values, .. } if values == &[RlvValue::WearableType(RlvWearableType::Universal)] )); let uuid = "00000000-0000-4000-8000-000000000000"; assert!(matches!( &parse(&format!("@accepttp:{uuid}=add")).commands[0].directive, RlvDirective::Restriction { values, .. } if matches!(values.as_slice(), [RlvValue::Uuid(_)]) )); // The pinned parser reads the first camera scalar and deliberately ignores // later semicolon fields for this family. assert!(matches!( &parse("@camzoommax:2.5;ignored;3=n").commands[0].directive, RlvDirective::Restriction { values, .. } if values == &[RlvValue::Real(2.5)] )); } #[test] fn every_pinned_restriction_name_round_trips_without_normalization() { assert_eq!(RLV_RESTRICTION_NAMES.len(), 119); for (name, restriction) in RLV_RESTRICTION_NAMES { assert_eq!(restriction_from_name(name), Some(*restriction)); assert_eq!(restriction_name(*restriction), *name); } assert_eq!( restriction_from_name("TouchFar"), Some(RlvRestrictionType::TouchFar) ); assert_eq!(restriction_from_name("touch-far"), None); } #[test] fn every_pinned_restriction_has_at_least_one_typed_valid_option_form() { let uuid = "00000000-0000-4000-8000-000000000000"; let candidates = [ "", "1", "1.0", "1;1;1", uuid, "chest", "shirt", "Folder Name", ]; for (name, expected) in RLV_RESTRICTION_NAMES { let accepted = candidates.iter().any(|option| { let message = format!("@{name}:{option}=n"); RlvParser::parse_message(&message, sender(), "sender") .ok() .is_some_and(|message| { matches!( message.commands[0].directive, RlvDirective::Restriction { behavior, .. } if behavior == *expected ) }) }); assert!(accepted, "no valid typed option form for {name}"); } } #[test] fn attachment_and_wearable_alias_tables_are_case_insensitive_but_not_trimmed() { assert_eq!( attachment_from_name("ROOT"), Some(RlvAttachmentPoint::AvatarCenter) ); assert_eq!( attachment_from_name("l upper leg"), Some(RlvAttachmentPoint::LeftUpperLeg) ); assert_eq!(attachment_from_name(" chest "), None); assert_eq!( wearable_from_name("UnderShirt"), Some(RlvWearableType::Undershirt) ); assert_eq!( wearable_from_name("universal"), Some(RlvWearableType::Universal) ); assert_eq!(wearable_from_name(" shirt "), None); } #[test] fn mapped_attachment_tag_helper_uses_last_recognized_tag() { let mut output = None; assert!(RlvCommon::try_get_attachment_point_from_item_name( "Item (mouth) (unknown) (SPINE)".to_owned(), &mut output, )); assert_eq!(output, Some(Some(RlvAttachmentPoint::Spine))); assert!(!RlvCommon::try_get_attachment_point_from_item_name( "Item (unknown)".to_owned(), &mut output, )); assert_eq!(output, None); } #[test] fn malformed_corpus_has_stable_categories_locations_and_never_panics() { let cases = [ ("", RlvParseErrorKind::MissingPrefix), ("fly=n", RlvParseErrorKind::MissingPrefix), ("@", RlvParseErrorKind::EmptyCommand), ("@fly", RlvParseErrorKind::MissingEquals), ("@=n", RlvParseErrorKind::EmptyBehavior), ("@fly=", RlvParseErrorKind::EmptyParameter), ("@unknown=force", RlvParseErrorKind::UnknownAction), ("@unknown=n", RlvParseErrorKind::UnknownRestriction), ("@unknown=123", RlvParseErrorKind::UnknownQuery), ("@fly=maybe", RlvParseErrorKind::UnsupportedParameter), ("@version=0", RlvParseErrorKind::ZeroQueryChannel), ("@sit:not-a-uuid=force", RlvParseErrorKind::InvalidUuid), ("@camdrawmin:0.39=n", RlvParseErrorKind::InvalidOption), ("@detach: chest =n", RlvParseErrorKind::InvalidOption), ("@fly=n,", RlvParseErrorKind::EmptyCommand), ]; for (message, expected) in cases { let result = std::panic::catch_unwind(|| RlvParser::parse_message(message, sender(), "sender")); let error = result.expect("parser never panics").expect_err(message); assert_eq!(error.kind, expected, "{message}"); assert!(error.span.start <= error.span.end); assert!(error.span.end <= message.len()); } } #[test] fn deterministic_single_byte_mutations_are_bounded_and_never_panic() { const SEEDS: &[&str] = &[ "@fly=n", "@version=42", "@notify:1234;sendim=add", "@attachthisoverorreplace:r upper arm=force", "@sit:00000000-0000-4000-8000-000000000000=force", "@getstatus:tp;|=7,@getcam_fov=8", ]; const REPLACEMENTS: &[u8] = b"@,:=;0An?-_ "; let mut corpus = Vec::new(); for seed in SEEDS { for index in 0..seed.len() { let mut deleted = seed.as_bytes().to_vec(); deleted.remove(index); corpus.push(String::from_utf8(deleted).expect("ASCII fixture")); for replacement in REPLACEMENTS { let mut replaced = seed.as_bytes().to_vec(); replaced[index] = *replacement; corpus.push(String::from_utf8(replaced).expect("ASCII fixture")); } } for index in 0..=seed.len() { for insertion in REPLACEMENTS { let mut inserted = seed.as_bytes().to_vec(); inserted.insert(index, *insertion); corpus.push(String::from_utf8(inserted).expect("ASCII fixture")); } } } assert!(corpus.len() >= 4_000, "mutation corpus unexpectedly small"); for message in corpus { let result = std::panic::catch_unwind(|| { RlvParser::parse_message(&message, sender(), "mutated sender") }); let parsed = result.expect("bounded parser must not panic for any mutation"); if let Err(error) = parsed { assert!(error.span.start <= error.span.end, "{message:?}"); assert!(error.span.end <= message.len(), "{message:?}"); } } } #[test] fn command_count_and_message_size_are_bounded() { let oversized_count = format!( "@{}", std::iter::repeat_n("fly=n", 129) .collect::>() .join(",") ); assert_eq!( RlvParser::parse_message(&oversized_count, sender(), "sender") .expect_err("too many commands") .kind, RlvParseErrorKind::TooManyCommands ); let oversized_message = format!("@getinv:{}=1", "x".repeat(64 * 1024)); assert_eq!( RlvParser::parse_message(&oversized_message, sender(), "sender") .expect_err("oversized message") .kind, RlvParseErrorKind::MessageTooLong ); } #[test] fn mapped_restriction_preserves_alias_exception_equality_and_snapshot_args() { let first = RlvRestriction::new( RlvRestrictionType::FarTouch, sender(), "Object A".to_owned(), vec![Object::Real(2.5)], ) .expect("restriction"); let second = RlvRestriction::new( RlvRestrictionType::FarTouch, sender(), "Renamed Object".to_owned(), vec![Object::Real(2.5)], ) .expect("restriction"); assert_eq!(first.behavior(), RlvRestrictionType::TouchFar); assert_eq!(first.original_behavior(), RlvRestrictionType::FarTouch); assert!(!first.is_exception()); assert_eq!(first, second); assert_eq!(first.get_hash_code(), second.get_hash_code()); assert_eq!(first.args().as_slice(), &[Object::Real(2.5)]); assert!(first.to_string().contains("Behavior=TouchFar")); assert!(first.equals(Some(Object::opaque(second)))); let exception = RlvRestriction::from_values( RlvRestrictionType::RecvChat, sender(), "Object".to_owned(), vec![RlvValue::Uuid(sender())], ); assert!(exception.is_exception()); let exact_real = 2.500_000_000_000_001_f64; let exact = RlvRestriction::new( RlvRestrictionType::CamZoomMax, sender(), "Exact value".to_owned(), vec![Object::Real(exact_real)], ) .expect("restriction"); assert_eq!(exact.args().as_slice(), &[Object::Real(exact_real)]); let rounded = RlvRestriction::new( RlvRestrictionType::CamZoomMax, sender(), "Rounded value".to_owned(), vec![Object::Real(2.5)], ) .expect("restriction"); assert_ne!( exact, rounded, "mapped equality uses the exact Args snapshot" ); }