135 lines
3.9 KiB
Rust
135 lines
3.9 KiB
Rust
//! 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)
|
|
);
|
|
}
|
|
}
|