Files
MetaCrate/crates/metacrate-lsl-tools/src/lsl_validation.rs
Chili Palmer a749111657
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m45s
CI / required (push) Failing after 1m0s
Implement safe public LSL delivery (#129)
2026-08-18 10:38:12 +02:00

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)
);
}
}