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