Skip to main content

textfsm_core/template/
value.rs

1//! Value definition parsing and representation.
2
3use fancy_regex::Regex;
4use std::collections::HashSet;
5
6use crate::error::TemplateError;
7use crate::types::{ValueOption, ValueOptions};
8
9/// A Value definition from the template header.
10#[derive(Debug, Clone)]
11pub struct ValueDef {
12    /// Name of the value (used as column header).
13    pub name: String,
14
15    /// Original regex pattern from template.
16    pub pattern: String,
17
18    /// Options applied to this value.
19    pub options: ValueOptions,
20
21    /// Regex pattern transformed for named capture: `(...)` -> `(?P<name>...)`.
22    pub(crate) template_pattern: String,
23
24    /// Compiled regex for List values with nested groups.
25    pub(crate) compiled_regex: Option<Regex>,
26}
27
28impl ValueDef {
29    /// Maximum allowed length for a value name.
30    pub const MAX_NAME_LEN: usize = 48;
31
32    /// Parse a Value line: `Value [Options] Name (regex)`
33    pub fn parse(line: &str, line_num: usize) -> Result<Self, TemplateError> {
34        let trimmed = line.trim();
35
36        if !trimmed.starts_with("Value ") {
37            return Err(TemplateError::InvalidValue {
38                line: line_num,
39                message: "line must start with 'Value '".into(),
40            });
41        }
42
43        // Remove "Value " prefix
44        let rest = &trimmed[6..];
45
46        // Find where the regex starts (first '(')
47        let regex_start = rest.find('(').ok_or_else(|| TemplateError::InvalidValue {
48            line: line_num,
49            message: "regex pattern must be wrapped in parentheses".into(),
50        })?;
51
52        let before_regex = rest[..regex_start].trim();
53        let pattern = rest[regex_start..].trim();
54
55        // Parse the part before regex for options and name
56        let before_parts: Vec<&str> = before_regex.split_whitespace().collect();
57
58        let (options, name) = match before_parts.len() {
59            0 => {
60                return Err(TemplateError::InvalidValue {
61                    line: line_num,
62                    message: "missing value name".into(),
63                })
64            }
65            1 => {
66                // Just a name, no options
67                (HashSet::new(), before_parts[0].to_string())
68            }
69            2 => {
70                // Could be "Options Name" - check if first part looks like options
71                // Options contain commas or are valid option names
72                if before_parts[0].contains(',')
73                    || ValueOption::from_str(before_parts[0]).is_some()
74                {
75                    let opts = Self::parse_options(before_parts[0], line_num)?;
76                    (opts, before_parts[1].to_string())
77                } else {
78                    // First part is not a valid option, error
79                    return Err(TemplateError::InvalidValue {
80                        line: line_num,
81                        message: format!(
82                            "invalid format - expected 'Value [Options] Name (regex)', got unknown token '{}'",
83                            before_parts[0]
84                        ),
85                    });
86                }
87            }
88            _ => {
89                return Err(TemplateError::InvalidValue {
90                    line: line_num,
91                    message: "too many tokens before regex pattern".into(),
92                })
93            }
94        };
95
96        // Validate name length
97        if name.len() > Self::MAX_NAME_LEN {
98            return Err(TemplateError::InvalidValue {
99                line: line_num,
100                message: format!("name '{}' exceeds maximum length of {}", name, Self::MAX_NAME_LEN),
101            });
102        }
103
104        // Validate name is alphanumeric/underscore
105        if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
106            return Err(TemplateError::InvalidValue {
107                line: line_num,
108                message: format!("name '{}' contains invalid characters", name),
109            });
110        }
111
112        // Validate regex is wrapped in parentheses
113        if !pattern.starts_with('(') || !pattern.ends_with(')') {
114            return Err(TemplateError::InvalidValue {
115                line: line_num,
116                message: "regex must be wrapped in parentheses".into(),
117            });
118        }
119
120        // Check for escaped closing paren (invalid)
121        if pattern.ends_with("\\)") {
122            return Err(TemplateError::InvalidValue {
123                line: line_num,
124                message: "regex cannot end with escaped parenthesis".into(),
125            });
126        }
127
128        // Normalize pattern: convert Python-style \< and \> to literal < and >
129        // In Python's re, \< is not a valid escape so it's treated as literal <
130        // In Rust's regex/fancy-regex, \< is a word boundary, so we need to convert
131        let pattern = Self::normalize_pattern(pattern);
132
133        // Validate the regex compiles
134        Regex::new(&pattern).map_err(|e| TemplateError::InvalidRegex {
135            pattern: pattern.to_string(),
136            message: e.to_string(),
137        })?;
138
139        // Create the named capture group version: (pattern) -> (?P<name>pattern)
140        let inner_pattern = &pattern[1..pattern.len() - 1];
141        let template_pattern = format!("(?P<{}>{})", name, inner_pattern);
142
143        // For List values with nested groups, compile the regex
144        let compiled_regex = if options.contains(&ValueOption::List) {
145            let re = Regex::new(&pattern).ok();
146            // Only store if there are nested groups
147            re.filter(|r| r.captures_len() > 1)
148        } else {
149            None
150        };
151
152        Ok(Self {
153            name,
154            pattern,
155            options,
156            template_pattern,
157            compiled_regex,
158        })
159    }
160
161    fn parse_options(opts_str: &str, _line_num: usize) -> Result<ValueOptions, TemplateError> {
162        let mut options = HashSet::new();
163
164        for opt_name in opts_str.split(',') {
165            let opt_name = opt_name.trim();
166            if opt_name.is_empty() {
167                continue;
168            }
169
170            let opt = ValueOption::from_str(opt_name)
171                .ok_or_else(|| TemplateError::UnknownOption(opt_name.into()))?;
172
173            if !options.insert(opt) {
174                return Err(TemplateError::DuplicateOption(opt_name.into()));
175            }
176        }
177
178        Ok(options)
179    }
180
181    /// Check if this value has a specific option.
182    pub fn has_option(&self, opt: ValueOption) -> bool {
183        self.options.contains(&opt)
184    }
185
186    /// Normalize a regex pattern for Rust compatibility.
187    ///
188    /// Python's re module treats `\<` and `\>` as literal `<` and `>` since they're
189    /// not valid escape sequences. In Rust's regex/fancy-regex, `\<` and `\>` are
190    /// word boundary assertions. This function converts them to literal characters.
191    fn normalize_pattern(pattern: &str) -> String {
192        let mut result = String::with_capacity(pattern.len());
193        let mut chars = pattern.chars().peekable();
194
195        while let Some(c) = chars.next() {
196            if c == '\\' {
197                if let Some(&next) = chars.peek() {
198                    if next == '<' || next == '>' {
199                        // Convert \< to < and \> to >
200                        result.push(chars.next().unwrap());
201                        continue;
202                    }
203                }
204            }
205            result.push(c);
206        }
207
208        result
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_parse_simple_value() {
218        let v = ValueDef::parse("Value Interface (\\S+)", 1).unwrap();
219        assert_eq!(v.name, "Interface");
220        assert_eq!(v.pattern, "(\\S+)");
221        assert!(v.options.is_empty());
222        assert_eq!(v.template_pattern, "(?P<Interface>\\S+)");
223    }
224
225    #[test]
226    fn test_parse_value_with_options() {
227        let v = ValueDef::parse("Value Required,Filldown Hostname (\\S+)", 1).unwrap();
228        assert_eq!(v.name, "Hostname");
229        assert!(v.has_option(ValueOption::Required));
230        assert!(v.has_option(ValueOption::Filldown));
231        assert!(!v.has_option(ValueOption::List));
232    }
233
234    #[test]
235    fn test_parse_value_with_spaces_in_regex() {
236        let v = ValueDef::parse("Value Status (up|down|administratively down)", 1).unwrap();
237        assert_eq!(v.name, "Status");
238        assert_eq!(v.pattern, "(up|down|administratively down)");
239    }
240
241    #[test]
242    fn test_invalid_regex() {
243        let result = ValueDef::parse("Value Bad ([invalid)", 1);
244        assert!(matches!(result, Err(TemplateError::InvalidRegex { .. })));
245    }
246
247    #[test]
248    fn test_missing_parens() {
249        let result = ValueDef::parse("Value Name \\S+", 1);
250        assert!(matches!(result, Err(TemplateError::InvalidValue { .. })));
251    }
252
253    #[test]
254    fn test_normalize_angle_brackets() {
255        // \< and \> should be converted to < and >
256        let v = ValueDef::parse(r"Value DateTime (\S+\s+\d+\s+\d+|\<no date\>)", 1).unwrap();
257        // The pattern should have literal < and > after normalization
258        assert!(v.pattern.contains("<no date>"));
259        assert!(!v.pattern.contains(r"\<"));
260    }
261}