//! Bounded structural LSL source validation for native `MetaCrate` consumers. use crate::MAX_SOURCE_UNITS; use std::fmt; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LslSyntaxError { Empty, Oversized, MissingDefaultState, UnterminatedString, UnterminatedComment, MismatchedDelimiter, InvalidCharacter, } impl fmt::Display for LslSyntaxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } } impl std::error::Error for LslSyntaxError {} /// Performs the bounded lexical and delimiter parse required before generated /// LSL may cross an inventory boundary. This rejects malformed lexical states /// and structural parse failures; it does not claim simulator runtime safety. /// /// # Errors /// /// Returns [`LslSyntaxError`] when the source exceeds its hard bound or its /// lexical/structural parse does not form a complete LSL default state. pub fn validate_lsl_source(source: &str) -> Result<(), LslSyntaxError> { if source.trim().is_empty() { return Err(LslSyntaxError::Empty); } if source.len() > MAX_SOURCE_UNITS { return Err(LslSyntaxError::Oversized); } if !source .split(|c: char| !c.is_alphanumeric() && c != '_') .any(|token| token == "default") { return Err(LslSyntaxError::MissingDefaultState); } let mut stack = Vec::new(); let mut chars = source.chars().peekable(); let mut string = false; let mut escaped = false; while let Some(ch) = chars.next() { if ch == '\0' { return Err(LslSyntaxError::InvalidCharacter); } if string { if escaped { escaped = false; } else if ch == '\\' { escaped = true; } else if ch == '"' { string = false; } continue; } if ch == '"' { string = true; continue; } if ch == '/' && chars.peek() == Some(&'/') { chars.next(); for next in chars.by_ref() { if next == '\n' { break; } } continue; } if ch == '/' && chars.peek() == Some(&'*') { chars.next(); let mut closed = false; while let Some(next) = chars.next() { if next == '*' && chars.peek() == Some(&'/') { chars.next(); closed = true; break; } } if !closed { return Err(LslSyntaxError::UnterminatedComment); } continue; } match ch { '{' | '(' | '[' => stack.push(ch), '}' | ')' | ']' => { let expected = match ch { '}' => '{', ')' => '(', _ => '[', }; if stack.pop() != Some(expected) { return Err(LslSyntaxError::MismatchedDelimiter); } } _ => {} } } if string { Err(LslSyntaxError::UnterminatedString) } else if stack.is_empty() { Ok(()) } else { Err(LslSyntaxError::MismatchedDelimiter) } } #[cfg(test)] mod tests { use super::*; #[test] fn lexical_and_structural_states_are_bounded() { assert!(validate_lsl_source("default { state_entry() { llSay(0, \"hi\"); } }").is_ok()); assert_eq!( validate_lsl_source("default { \"x"), Err(LslSyntaxError::UnterminatedString) ); assert_eq!( validate_lsl_source("default { /*"), Err(LslSyntaxError::UnterminatedComment) ); assert_eq!( validate_lsl_source("default { ]"), Err(LslSyntaxError::MismatchedDelimiter) ); } }