bevy_reflect/path/
parse.rs

1use core::{
2    fmt::{self, Write},
3    num::ParseIntError,
4    str::from_utf8_unchecked,
5};
6use thiserror::Error;
7
8use super::{Access, ReflectPathError};
9
10/// An error that occurs when parsing reflect path strings.
11#[derive(Debug, PartialEq, Eq, Error)]
12#[error(transparent)]
13pub struct ParseError<'a>(Error<'a>);
14
15/// A parse error for a path string.
16#[derive(Debug, PartialEq, Eq, Error)]
17enum Error<'a> {
18    #[error("expected an identifier, but reached end of path string")]
19    NoIdent,
20
21    #[error("expected an identifier, got '{0}' instead")]
22    ExpectedIdent(Token<'a>),
23
24    #[error("failed to parse index as integer")]
25    InvalidIndex(#[from] ParseIntError),
26
27    #[error("a '[' wasn't closed, reached end of path string before finding a ']'")]
28    Unclosed,
29
30    #[error("a '[' wasn't closed properly, got '{0}' instead")]
31    BadClose(Token<'a>),
32
33    #[error("a ']' was found before an opening '['")]
34    CloseBeforeOpen,
35}
36
37pub(super) struct PathParser<'a> {
38    path: &'a str,
39    remaining: &'a [u8],
40}
41impl<'a> PathParser<'a> {
42    pub(super) fn new(path: &'a str) -> Self {
43        let remaining = path.as_bytes();
44        PathParser { path, remaining }
45    }
46
47    fn next_token(&mut self) -> Option<Token<'a>> {
48        let to_parse = self.remaining;
49
50        // Return with `None` if empty.
51        let (first_byte, remaining) = to_parse.split_first()?;
52
53        if let Some(token) = Token::symbol_from_byte(*first_byte) {
54            self.remaining = remaining; // NOTE: all symbols are ASCII
55            return Some(token);
56        }
57        // We are parsing either `0123` or `field`.
58        // If we do not find a subsequent token, we are at the end of the parse string.
59        let ident_len = to_parse.iter().position(|t| Token::SYMBOLS.contains(t));
60        let (ident, remaining) = to_parse.split_at(ident_len.unwrap_or(to_parse.len()));
61        // SAFETY: This relies on `self.remaining` always remaining valid UTF8:
62        // - self.remaining is a slice derived from self.path (valid &str)
63        // - The slice's end is either the same as the valid &str or
64        //   the last byte before an ASCII utf-8 character (ie: it is a char
65        //   boundary).
66        // - The slice always starts after a symbol ie: an ASCII character's boundary.
67        #[expect(
68            unsafe_code,
69            reason = "We have fulfilled the Safety requirements for `from_utf8_unchecked`."
70        )]
71        let ident = unsafe { from_utf8_unchecked(ident) };
72
73        self.remaining = remaining;
74        Some(Token::Ident(Ident(ident)))
75    }
76
77    fn next_ident(&mut self) -> Result<Ident<'a>, Error<'a>> {
78        match self.next_token() {
79            Some(Token::Ident(ident)) => Ok(ident),
80            Some(other) => Err(Error::ExpectedIdent(other)),
81            None => Err(Error::NoIdent),
82        }
83    }
84
85    fn access_following(&mut self, token: Token<'a>) -> Result<Access<'a>, Error<'a>> {
86        match token {
87            Token::Dot => Ok(self.next_ident()?.field()),
88            Token::Pound => self.next_ident()?.field_index(),
89            Token::Ident(ident) => Ok(ident.field()),
90            Token::CloseBracket => Err(Error::CloseBeforeOpen),
91            Token::OpenBracket => {
92                let index_ident = self.next_ident()?.list_index()?;
93                match self.next_token() {
94                    Some(Token::CloseBracket) => Ok(index_ident),
95                    Some(other) => Err(Error::BadClose(other)),
96                    None => Err(Error::Unclosed),
97                }
98            }
99        }
100    }
101
102    fn offset(&self) -> usize {
103        self.path.len() - self.remaining.len()
104    }
105}
106impl<'a> Iterator for PathParser<'a> {
107    type Item = (Result<Access<'a>, ReflectPathError<'a>>, usize);
108
109    fn next(&mut self) -> Option<Self::Item> {
110        let token = self.next_token()?;
111        let offset = self.offset();
112        Some((
113            self.access_following(token)
114                .map_err(|error| ReflectPathError::ParseError {
115                    offset,
116                    path: self.path,
117                    error: ParseError(error),
118                }),
119            offset,
120        ))
121    }
122}
123
124#[derive(Debug, PartialEq, Eq)]
125struct Ident<'a>(&'a str);
126
127impl<'a> Ident<'a> {
128    fn field(self) -> Access<'a> {
129        let field = |_| Access::Field(self.0.into());
130        self.0.parse().map(Access::TupleIndex).unwrap_or_else(field)
131    }
132    fn field_index(self) -> Result<Access<'a>, Error<'a>> {
133        Ok(Access::FieldIndex(self.0.parse()?))
134    }
135    fn list_index(self) -> Result<Access<'a>, Error<'a>> {
136        Ok(Access::ListIndex(self.0.parse()?))
137    }
138}
139
140// NOTE: We use repr(u8) so that the `match byte` in `Token::symbol_from_byte`
141// becomes a "check `byte` is one of SYMBOLS and forward its value" this makes
142// the optimizer happy, and shaves off a few cycles.
143#[derive(Debug, PartialEq, Eq)]
144#[repr(u8)]
145enum Token<'a> {
146    Dot = b'.',
147    Pound = b'#',
148    OpenBracket = b'[',
149    CloseBracket = b']',
150    Ident(Ident<'a>),
151}
152impl fmt::Display for Token<'_> {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Token::Dot => f.write_char('.'),
156            Token::Pound => f.write_char('#'),
157            Token::OpenBracket => f.write_char('['),
158            Token::CloseBracket => f.write_char(']'),
159            Token::Ident(ident) => f.write_str(ident.0),
160        }
161    }
162}
163impl<'a> Token<'a> {
164    const SYMBOLS: &'static [u8] = b".#[]";
165    fn symbol_from_byte(byte: u8) -> Option<Self> {
166        match byte {
167            b'.' => Some(Self::Dot),
168            b'#' => Some(Self::Pound),
169            b'[' => Some(Self::OpenBracket),
170            b']' => Some(Self::CloseBracket),
171            _ => None,
172        }
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::*;
179    use crate::path::ParsedPath;
180
181    #[test]
182    fn parse_invalid() {
183        assert_eq!(
184            ParsedPath::parse_static("x.."),
185            Err(ReflectPathError::ParseError {
186                error: ParseError(Error::ExpectedIdent(Token::Dot)),
187                offset: 2,
188                path: "x..",
189            }),
190        );
191        assert!(matches!(
192            ParsedPath::parse_static("y[badindex]"),
193            Err(ReflectPathError::ParseError {
194                error: ParseError(Error::InvalidIndex(_)),
195                offset: 2,
196                path: "y[badindex]",
197            }),
198        ));
199    }
200}