Something went wrong. Try again.
Personal fork of prime agent. Mirror of: https://mnemosyne.sigilzero.dev/sigilzero/prime-agent
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256use std::borrow::Cow;use std::fmt;use std::iter::Peekable;use std::str::CharIndices;
use crate::error::{Error, Result};use crate::state::Lua;use crate::traits::IntoLua;use crate::types::Integer;use crate::value::Value;
#[derive(Debug)]pub(crate) enum PathKey<'a> { Str(Cow<'a, str>), Int(Integer),}
impl fmt::Display for PathKey<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PathKey::Str(s) => write!(f, "{}", s), PathKey::Int(i) => write!(f, "{}", i), } }}
impl IntoLua for PathKey<'_> { fn into_lua(self, lua: &Lua) -> Result<Value> { match self { PathKey::Str(s) => Ok(Value::String(lua.create_string(s.as_ref())?)), PathKey::Int(i) => Ok(Value::Integer(i)), } }}
// Parses a path like `a.b[3]?.c["d"]` into segments of `(key, safe_nil)`.pub(crate) fn parse_path<'a>(path: &'a str) -> Result<Vec<(PathKey<'a>, bool)>> { fn read_ident<'a>(path: &'a str, chars: &mut Peekable<CharIndices<'a>>) -> (Cow<'a, str>, bool) { let mut safe_nil = false; let start = chars.peek().map(|&(i, _)| i).unwrap_or(path.len()); let mut end = start; while let Some(&(pos, c)) = chars.peek() { if c == '.' || c == '?' || c.is_ascii_whitespace() || c == '[' { if c == '?' { safe_nil = true; chars.next(); // consume '?' } break; } end = pos + c.len_utf8(); chars.next(); } (Cow::Borrowed(&path[start..end]), safe_nil) }
let mut segments = Vec::new(); let mut chars = path.char_indices().peekable(); while let Some(&(pos, next)) = chars.peek() { match next { '.' => { // Dot notation: identifier chars.next(); let (key, safe_nil) = read_ident(path, &mut chars); if key.is_empty() { return Err(Error::runtime(format!("empty key in path at position {pos}"))); } segments.push((PathKey::Str(key), safe_nil)); } '[' => { // Bracket notation: either integer or quoted string chars.next(); let key = match chars.peek() { Some(&(pos, c @ '0'..='9' | c @ '-')) => { // Integer key let negative = c == '-'; if negative { chars.next(); // consume '-' } let mut num: Option<Integer> = None; while let Some(&(_, c @ '0'..='9')) = chars.peek() { let new_num = num .unwrap_or(0) .checked_mul(10) .and_then(|n| n.checked_add((c as u8 - b'0') as Integer)) .ok_or_else(|| { Error::runtime(format!("integer overflow in path at position {pos}")) })?; num = Some(new_num); chars.next(); // consume digit } match num { Some(n) if negative => PathKey::Int(-n), Some(n) => PathKey::Int(n), None => { let err = format!("invalid integer in path at position {pos}"); return Err(Error::runtime(err)); } } } Some((_, '\'' | '"')) => { // Quoted string PathKey::Str(unquote_string(path, &mut chars)?) } Some((_, ']')) => { return Err(Error::runtime(format!("empty key in path at position {pos}"))); } Some((pos, c)) => { let err = format!("unexpected character '{c}' in path at position {pos}"); return Err(Error::runtime(err)); } None => { return Err(Error::runtime("unexpected end of path")); } }; // Expect closing bracket let mut safe_nil = false; match chars.next() { Some((_, ']')) => { // Check for optional safe-nil operator if let Some(&(_, '?')) = chars.peek() { safe_nil = true; chars.next(); // consume '?' } } Some((pos, c)) => { let err = format!("expected ']' in path at position {pos}, found '{c}'"); return Err(Error::runtime(err)); } None => { return Err(Error::runtime("unexpected end of path")); } } segments.push((key, safe_nil)); } c if c.is_ascii_whitespace() => { chars.next(); // Skip whitespace } _ if segments.is_empty() => { // First segment without dot/bracket notation let (key_cow, safe_nil) = read_ident(path, &mut chars); if key_cow.is_empty() { return Err(Error::runtime(format!("empty key in path at position {pos}"))); } segments.push((PathKey::Str(key_cow), safe_nil)); } c => { let err = format!("unexpected character '{c}' in path at position {pos}"); return Err(Error::runtime(err)); } } } Ok(segments)}
fn unquote_string<'a>(path: &'a str, chars: &mut Peekable<CharIndices<'a>>) -> Result<Cow<'a, str>> { let (start_pos, first_quote) = chars.next().unwrap(); let mut result = String::new(); loop { match chars.next() { Some((pos, '\\')) => { if result.is_empty() { // First escape found, copy everything up to this point result.push_str(&path[start_pos + 1..pos]); } match chars.next() { Some((_, '\\')) => result.push('\\'), Some((_, '"')) => result.push('"'), Some((_, '\'')) => result.push('\''), Some((_, other)) => { result.push('\\'); result.push(other); } None => continue, // will be handled by outer loop } } Some((pos, c)) if c == first_quote => { if !result.is_empty() { return Ok(Cow::Owned(result)); } // No escapes, return borrowed slice return Ok(Cow::Borrowed(&path[start_pos + 1..pos])); } Some((_, c)) => { if !result.is_empty() { result.push(c); } // If no escapes yet, continue tracking for potential borrowed slice } None => { let err = format!("unexpected end of string at position {start_pos}"); return Err(Error::runtime(err)); } } }}
#[cfg(test)]mod tests { use super::{parse_path, PathKey};
#[test] fn test_parse_path() { // Test valid paths let path = parse_path("a.b[3]?.c['d']").unwrap(); assert_eq!(path.len(), 5); assert!(matches!(path[0], (PathKey::Str(ref s), false) if s == "a")); assert!(matches!(path[1], (PathKey::Str(ref s), false) if s == "b")); assert!(matches!(path[2], (PathKey::Int(3), true))); assert!(matches!(path[3], (PathKey::Str(ref s), false) if s == "c")); assert!(matches!(path[4], (PathKey::Str(ref s), false) if s == "d"));
// Test empty path let path = parse_path("").unwrap(); assert_eq!(path.len(), 0); let path = parse_path(" ").unwrap(); assert_eq!(path.len(), 0);
// Test invalid dot syntax let err = parse_path("a..b").unwrap_err().to_string(); assert_eq!(err, "runtime error: empty key in path at position 1"); let err = parse_path("a.b.").unwrap_err().to_string(); assert_eq!(err, "runtime error: empty key in path at position 3");
// Test invalid bracket syntax let err = parse_path("a[unclosed").unwrap_err().to_string(); assert_eq!( err, "runtime error: unexpected character 'u' in path at position 2" ); let err = parse_path("a[]").unwrap_err().to_string(); assert_eq!(err, "runtime error: empty key in path at position 1"); let err = parse_path(r#"a["unclosed"#).unwrap_err().to_string(); assert_eq!(err, "runtime error: unexpected end of string at position 2"); let err = parse_path(r#"a["#).unwrap_err().to_string(); assert_eq!(err, "runtime error: unexpected end of path"); let err = parse_path(r#"a[123"#).unwrap_err().to_string(); assert_eq!(err, "runtime error: unexpected end of path"); let err = parse_path(r#"a['bla'123"#).unwrap_err().to_string(); assert_eq!( err, "runtime error: expected ']' in path at position 7, found '1'" ); let err = parse_path(r#"a["bla"]x"#).unwrap_err().to_string(); assert_eq!( err, "runtime error: unexpected character 'x' in path at position 8" );
// Test bad integers let err = parse_path("a[99999999999999999999]").unwrap_err().to_string(); assert_eq!(err, "runtime error: integer overflow in path at position 2"); let err = parse_path("a[-]").unwrap_err().to_string(); assert_eq!(err, "runtime error: invalid integer in path at position 2"); }}