toml_edit/parser/
mod.rs

1#![allow(clippy::type_complexity)]
2
3use std::cell::RefCell;
4pub(crate) mod array;
5pub(crate) mod datetime;
6pub(crate) mod document;
7pub(crate) mod error;
8pub(crate) mod inline_table;
9pub(crate) mod key;
10pub(crate) mod numbers;
11pub(crate) mod state;
12pub(crate) mod strings;
13pub(crate) mod table;
14pub(crate) mod trivia;
15pub(crate) mod value;
16
17pub(crate) use crate::error::TomlError;
18
19pub(crate) fn parse_document<S: AsRef<str>>(raw: S) -> Result<crate::ImDocument<S>, TomlError> {
20    use prelude::*;
21
22    let b = new_input(raw.as_ref());
23    let state = RefCell::new(state::ParseState::new());
24    let state_ref = &state;
25    document::document(state_ref)
26        .parse(b.clone())
27        .map_err(|e| TomlError::new(e, b))?;
28    let doc = state
29        .into_inner()
30        .into_document(raw)
31        .map_err(|e| TomlError::custom(e.to_string(), None))?;
32    Ok(doc)
33}
34
35pub(crate) fn parse_key(raw: &str) -> Result<crate::Key, TomlError> {
36    use prelude::*;
37
38    let b = new_input(raw);
39    let result = key::simple_key.parse(b.clone());
40    match result {
41        Ok((raw, key)) => {
42            Ok(crate::Key::new(key).with_repr_unchecked(crate::Repr::new_unchecked(raw)))
43        }
44        Err(e) => Err(TomlError::new(e, b)),
45    }
46}
47
48pub(crate) fn parse_key_path(raw: &str) -> Result<Vec<crate::Key>, TomlError> {
49    use prelude::*;
50
51    let b = new_input(raw);
52    let result = key::key.parse(b.clone());
53    match result {
54        Ok(keys) => Ok(keys),
55        Err(e) => Err(TomlError::new(e, b)),
56    }
57}
58
59pub(crate) fn parse_value(raw: &str) -> Result<crate::Value, TomlError> {
60    use prelude::*;
61
62    let b = new_input(raw);
63    let parsed = value::value.parse(b.clone());
64    match parsed {
65        Ok(value) => Ok(value),
66        Err(e) => Err(TomlError::new(e, b)),
67    }
68}
69
70pub(crate) mod prelude {
71    pub(crate) use winnow::combinator::dispatch;
72    pub(crate) use winnow::error::ContextError;
73    pub(crate) use winnow::error::FromExternalError;
74    pub(crate) use winnow::error::StrContext;
75    pub(crate) use winnow::error::StrContextValue;
76    pub(crate) use winnow::ModalParser;
77    pub(crate) use winnow::ModalResult;
78    pub(crate) use winnow::Parser as _;
79
80    pub(crate) type Input<'b> =
81        winnow::Stateful<winnow::LocatingSlice<&'b winnow::BStr>, RecursionCheck>;
82
83    pub(crate) fn new_input(s: &str) -> Input<'_> {
84        winnow::Stateful {
85            input: winnow::LocatingSlice::new(winnow::BStr::new(s)),
86            state: Default::default(),
87        }
88    }
89
90    #[derive(Clone, Debug, Default, PartialEq, Eq)]
91    pub(crate) struct RecursionCheck {
92        #[cfg(not(feature = "unbounded"))]
93        current: usize,
94    }
95
96    #[cfg(not(feature = "unbounded"))]
97    const LIMIT: usize = 80;
98
99    impl RecursionCheck {
100        pub(crate) fn check_depth(_depth: usize) -> Result<(), super::error::CustomError> {
101            #[cfg(not(feature = "unbounded"))]
102            if LIMIT <= _depth {
103                return Err(super::error::CustomError::RecursionLimitExceeded);
104            }
105
106            Ok(())
107        }
108
109        fn enter(&mut self) -> Result<(), super::error::CustomError> {
110            #[cfg(not(feature = "unbounded"))]
111            {
112                self.current += 1;
113                if LIMIT <= self.current {
114                    return Err(super::error::CustomError::RecursionLimitExceeded);
115                }
116            }
117            Ok(())
118        }
119
120        fn exit(&mut self) {
121            #[cfg(not(feature = "unbounded"))]
122            {
123                self.current -= 1;
124            }
125        }
126    }
127
128    pub(crate) fn check_recursion<'b, O>(
129        mut parser: impl ModalParser<Input<'b>, O, ContextError>,
130    ) -> impl ModalParser<Input<'b>, O, ContextError> {
131        move |input: &mut Input<'b>| {
132            input
133                .state
134                .enter()
135                .map_err(|err| winnow::error::ErrMode::from_external_error(input, err).cut())?;
136            let result = parser.parse_next(input);
137            input.state.exit();
138            result
139        }
140    }
141}
142
143#[cfg(test)]
144#[cfg(feature = "parse")]
145#[cfg(feature = "display")]
146mod test {
147    use super::*;
148    use snapbox::assert_data_eq;
149    use snapbox::prelude::*;
150
151    #[test]
152    fn documents() {
153        let documents = [
154            "",
155            r#"
156# This is a TOML document.
157
158title = "TOML Example"
159
160    [owner]
161    name = "Tom Preston-Werner"
162    dob = 1979-05-27T07:32:00-08:00 # First class dates
163
164    [database]
165    server = "192.168.1.1"
166    ports = [ 8001, 8001, 8002 ]
167    connection_max = 5000
168    enabled = true
169
170    [servers]
171
172    # Indentation (tabs and/or spaces) is allowed but not required
173[servers.alpha]
174    ip = "10.0.0.1"
175    dc = "eqdc10"
176
177    [servers.beta]
178    ip = "10.0.0.2"
179    dc = "eqdc10"
180
181    [clients]
182    data = [ ["gamma", "delta"], [1, 2] ]
183
184    # Line breaks are OK when inside arrays
185hosts = [
186    "alpha",
187    "omega"
188]
189
190   'some.weird .stuff'   =  """
191                         like
192                         that
193                      #   """ # this broke my syntax highlighting
194   " also. like " = '''
195that
196'''
197   double = 2e39 # this number looks familiar
198# trailing comment"#,
199            r#""#,
200            r#"  "#,
201            r#" hello = 'darkness' # my old friend
202"#,
203            r#"[parent . child]
204key = "value"
205"#,
206            r#"hello.world = "a"
207"#,
208            r#"foo = 1979-05-27 # Comment
209"#,
210        ];
211        for input in documents {
212            dbg!(input);
213            let parsed = parse_document(input).map(|d| d.into_mut());
214            let doc = match parsed {
215                Ok(doc) => doc,
216                Err(err) => {
217                    panic!("Parse error: {err:?}\nFailed to parse:\n```\n{input}\n```")
218                }
219            };
220
221            assert_data_eq!(doc.to_string(), input.raw());
222        }
223    }
224
225    #[test]
226    fn documents_parse_only() {
227        let parse_only = ["\u{FEFF}
228[package]
229name = \"foo\"
230version = \"0.0.1\"
231authors = []
232"];
233        for input in parse_only {
234            dbg!(input);
235            let parsed = parse_document(input).map(|d| d.into_mut());
236            match parsed {
237                Ok(_) => (),
238                Err(err) => {
239                    panic!("Parse error: {err:?}\nFailed to parse:\n```\n{input}\n```")
240                }
241            }
242        }
243    }
244
245    #[test]
246    fn invalid_documents() {
247        let invalid_inputs = [r#" hello = 'darkness' # my old friend
248$"#];
249        for input in invalid_inputs {
250            dbg!(input);
251            let parsed = parse_document(input).map(|d| d.into_mut());
252            assert!(parsed.is_err(), "Input: {input:?}");
253        }
254    }
255}