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