Skip to main content

textfsm_core/parser/
mod.rs

1//! FSM parser for processing input text.
2
3mod state;
4
5pub use state::ValueState;
6
7use std::collections::HashMap;
8
9use crate::error::ParseError;
10use crate::template::Template;
11use crate::types::{LineOp, RecordOp, Transition, Value};
12
13/// Parser for processing text with a compiled template.
14pub struct Parser<'t> {
15    /// Reference to the compiled template.
16    template: &'t Template,
17
18    /// Current state name.
19    current_state: String,
20
21    /// Runtime state for each value.
22    value_states: Vec<ValueState>,
23
24    /// Accumulated results.
25    results: Vec<Vec<Value>>,
26}
27
28impl<'t> Parser<'t> {
29    /// Create a new parser for the given template.
30    pub fn new(template: &'t Template) -> Self {
31        let value_states = template
32            .values()
33            .iter()
34            .enumerate()
35            .map(|(idx, def)| ValueState::new(def.clone(), idx))
36            .collect();
37
38        Self {
39            template,
40            current_state: "Start".to_string(),
41            value_states,
42            results: Vec::new(),
43        }
44    }
45
46    /// Reset the parser state for reuse.
47    pub fn reset(&mut self) {
48        self.current_state = "Start".to_string();
49        self.results.clear();
50        for vs in &mut self.value_states {
51            vs.clear_all();
52        }
53    }
54
55    /// Parse text and return list of records.
56    pub fn parse_text(&mut self, text: &str) -> Result<Vec<Vec<Value>>, ParseError> {
57        self.parse_text_with_eof(text, true)
58    }
59
60    /// Parse text with explicit EOF control.
61    pub fn parse_text_with_eof(
62        &mut self,
63        text: &str,
64        eof: bool,
65    ) -> Result<Vec<Vec<Value>>, ParseError> {
66        for line in text.lines() {
67            self.process_line(line)?;
68
69            if self.current_state == "End" || self.current_state == "EOF" {
70                break;
71            }
72        }
73
74        // Implicit EOF behavior
75        if self.current_state != "End"
76            && self.template.get_state("EOF").is_none()
77            && eof
78        {
79            self.append_record();
80        }
81
82        Ok(std::mem::take(&mut self.results))
83    }
84
85    /// Parse text and return results as list of dicts.
86    pub fn parse_text_to_dicts(
87        &mut self,
88        text: &str,
89    ) -> Result<Vec<HashMap<String, String>>, ParseError> {
90        let results = self.parse_text(text)?;
91        let header = self.template.header();
92
93        Ok(results
94            .into_iter()
95            .map(|row| {
96                header
97                    .iter()
98                    .zip(row)
99                    .map(|(k, v)| (k.to_lowercase(), v.as_string()))
100                    .collect()
101            })
102            .collect())
103    }
104
105    /// Parse text and deserialize results into typed structs.
106    ///
107    /// This method parses the input text and deserializes each record directly
108    /// into the specified type `T` using serde.
109    ///
110    /// # Example
111    ///
112    /// ```rust,ignore
113    /// use serde::Deserialize;
114    ///
115    /// #[derive(Deserialize)]
116    /// struct Interface {
117    ///     interface: String,
118    ///     status: String,
119    /// }
120    ///
121    /// let interfaces: Vec<Interface> = parser.parse_text_into(input)?;
122    /// ```
123    #[cfg(feature = "serde")]
124    pub fn parse_text_into<T>(&mut self, text: &str) -> Result<Vec<T>, ParseError>
125    where
126        T: serde::de::DeserializeOwned,
127    {
128        let results = self.parse_text(text)?;
129
130        // Pre-compute lowercased headers once, not per-record
131        let header: Vec<String> = self
132            .template
133            .header()
134            .iter()
135            .map(|s| s.to_lowercase())
136            .collect();
137
138        results
139            .into_iter()
140            .map(|record| {
141                // Use optimized path that borrows pre-lowercased headers
142                crate::de::from_record_borrowed(&header, record)
143                    .map_err(|e| ParseError::DeserializeError(e.to_string()))
144            })
145            .collect()
146    }
147
148    /// Process a single input line.
149    fn process_line(&mut self, line: &str) -> Result<(), ParseError> {
150        let state = match self.template.get_state(&self.current_state) {
151            Some(s) => s,
152            None => return Ok(()), // End/EOF state
153        };
154
155        for rule in &state.rules {
156            if let Ok(Some(captures)) = rule.regex.captures(line) {
157                // Extract matched values.
158                //
159                // For each value, if the named group matched, assign the captured
160                // text. If the group exists in the rule but didn't capture (optional
161                // group), call assign_none() to clear the value — matching Python's
162                // behavior where groupdict() yields None and AssignVar(None) is called.
163                for vs in &mut self.value_states {
164                    if let Some(matched) = captures.name(&vs.def.name) {
165                        vs.assign(matched.as_str().to_string(), &mut self.results);
166                    } else if rule.regex_pattern.contains(&format!("(?P<{}>", vs.def.name)) {
167                        vs.assign_none();
168                    }
169                }
170
171                // Apply record operation
172                match rule.record_op {
173                    RecordOp::Record => self.append_record(),
174                    RecordOp::Clear => self.clear_values(),
175                    RecordOp::ClearAll => self.clear_all_values(),
176                    RecordOp::NoRecord => {}
177                }
178
179                // Apply line operation
180                match rule.line_op {
181                    LineOp::Error => {
182                        let message = match &rule.transition {
183                            Transition::State(msg) => msg.clone(),
184                            _ => "state error".into(),
185                        };
186                        return Err(ParseError::RuleError {
187                            rule_line: rule.line_num,
188                            message,
189                        });
190                    }
191                    LineOp::Continue => {
192                        // Don't break, continue checking rules
193                        continue;
194                    }
195                    LineOp::Next => {
196                        // Apply state transition and break
197                        self.apply_transition(&rule.transition);
198                        break;
199                    }
200                }
201            }
202        }
203
204        Ok(())
205    }
206
207    /// Append current record to results.
208    fn append_record(&mut self) {
209        // Check Required constraints
210        for vs in &self.value_states {
211            if !vs.satisfies_required() {
212                // Skip record
213                self.clear_values();
214                return;
215            }
216        }
217
218        // Build record
219        let record: Vec<Value> = self
220            .value_states
221            .iter_mut()
222            .map(|vs| vs.take_for_record())
223            .collect();
224
225        // Don't record if all empty
226        if record.iter().all(|v| v.is_empty()) {
227            return;
228        }
229
230        self.results.push(record);
231        self.clear_values();
232    }
233
234    /// Clear non-Filldown values.
235    fn clear_values(&mut self) {
236        for vs in &mut self.value_states {
237            vs.clear();
238        }
239    }
240
241    /// Clear all values including Filldown.
242    fn clear_all_values(&mut self) {
243        for vs in &mut self.value_states {
244            vs.clear_all();
245        }
246    }
247
248    /// Apply a state transition.
249    fn apply_transition(&mut self, transition: &Transition) {
250        match transition {
251            Transition::Stay => {}
252            Transition::State(name) => {
253                self.current_state = name.clone();
254            }
255            Transition::End => {
256                self.current_state = "End".to_string();
257            }
258            Transition::Eof => {
259                self.current_state = "EOF".to_string();
260            }
261        }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use crate::template::Template;
269
270    #[test]
271    fn test_simple_parse() {
272        let template_str = r#"Value Interface (\S+)
273Value Status (up|down)
274
275Start
276  ^Interface: ${Interface} is ${Status} -> Record
277"#;
278
279        let template = Template::parse_str(template_str).unwrap();
280        let mut parser = template.parser();
281
282        let input = "Interface: eth0 is up\nInterface: eth1 is down\n";
283        let results = parser.parse_text(input).unwrap();
284
285        assert_eq!(results.len(), 2);
286        assert_eq!(results[0][0], Value::Single("eth0".into()));
287        assert_eq!(results[0][1], Value::Single("up".into()));
288        assert_eq!(results[1][0], Value::Single("eth1".into()));
289        assert_eq!(results[1][1], Value::Single("down".into()));
290    }
291
292    #[test]
293    fn test_parse_to_dicts() {
294        let template_str = r#"Value Name (\S+)
295Value Age (\d+)
296
297Start
298  ^Name: ${Name}, Age: ${Age} -> Record
299"#;
300
301        let template = Template::parse_str(template_str).unwrap();
302        let mut parser = template.parser();
303
304        let input = "Name: Alice, Age: 30\nName: Bob, Age: 25\n";
305        let results = parser.parse_text_to_dicts(input).unwrap();
306
307        assert_eq!(results.len(), 2);
308        assert_eq!(results[0].get("name"), Some(&"Alice".to_string()));
309        assert_eq!(results[0].get("age"), Some(&"30".to_string()));
310    }
311
312    #[test]
313    fn test_required_skips_empty() {
314        let template_str = r#"Value Required Name (\S+)
315Value Optional (\S+)
316
317Start
318  ^Name: ${Name}
319  ^Optional: ${Optional}
320  ^--- -> Record
321"#;
322
323        let template = Template::parse_str(template_str).unwrap();
324        let mut parser = template.parser();
325
326        // Record with no Name should be skipped
327        let input = "Optional: foo\n---\nName: bar\n---\n";
328        let results = parser.parse_text(input).unwrap();
329
330        assert_eq!(results.len(), 1);
331        assert_eq!(results[0][0], Value::Single("bar".into()));
332    }
333
334    #[test]
335    fn test_filldown_clears_when_optional_group_unmatched() {
336        // Simulates the bug with templates like arista_eos_show_ip_bgp_detail
337        // where a Filldown value with an optional capture should be cleared
338        // when the group doesn't participate in a match.
339        let template_str = r#"Value Filldown PREFIX (\S+)
340Value Filldown PREFIX_LENGTH (\d+)
341
342Start
343  ^Prefix:\s+${PREFIX}\s*(len:\s*${PREFIX_LENGTH})?
344  ^--- -> Record
345
346EOF
347"#;
348
349        let template = Template::parse_str(template_str).unwrap();
350        let mut parser = template.parser();
351
352        // First record: both groups match (PREFIX and PREFIX_LENGTH)
353        // Second record: PREFIX matches but PREFIX_LENGTH optional group doesn't
354        let input = "\
355Prefix: 10.0.0.0 len: 24
356---
357Prefix: 192.168.1.0
358---
359";
360        let results = parser.parse_text(input).unwrap();
361
362        assert_eq!(results.len(), 2);
363        // First record: both values captured
364        assert_eq!(results[0][0], Value::Single("10.0.0.0".into()));
365        assert_eq!(results[0][1], Value::Single("24".into()));
366        // Second record: PREFIX_LENGTH should be Empty (not stale "24" from Filldown)
367        assert_eq!(results[1][0], Value::Single("192.168.1.0".into()));
368        assert_eq!(results[1][1], Value::Empty);
369    }
370
371    #[test]
372    fn test_rule_with_escaped_angle_brackets() {
373        // Templates like fortinet_get_system_ha_status use \< in rules
374        let template_str = r#"Value DateTime (\S+)
375
376Start
377  ^\s*<${DateTime}> -> Record
378"#;
379
380        let template = Template::parse_str(template_str).unwrap();
381        let mut parser = template.parser();
382
383        let input = "  <2020/11/18> some text\n";
384        let results = parser.parse_text(input).unwrap();
385
386        assert_eq!(results.len(), 1);
387        assert_eq!(results[0][0], Value::Single("2020/11/18".into()));
388    }
389}