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::parse(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::parse(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                && let Some(&next) = chars.peek()
198                && (next == '<' || next == '>')
199            {
200                // Convert \< to < and \> to >
201                result.push(chars.next().unwrap());
202                continue;
203            }
204            result.push(c);
205        }
206
207        result
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_parse_simple_value() {
217        let v = ValueDef::parse("Value Interface (\\S+)", 1).unwrap();
218        assert_eq!(v.name, "Interface");
219        assert_eq!(v.pattern, "(\\S+)");
220        assert!(v.options.is_empty());
221        assert_eq!(v.template_pattern, "(?P<Interface>\\S+)");
222    }
223
224    #[test]
225    fn test_parse_value_with_options() {
226        let v = ValueDef::parse("Value Required,Filldown Hostname (\\S+)", 1).unwrap();
227        assert_eq!(v.name, "Hostname");
228        assert!(v.has_option(ValueOption::Required));
229        assert!(v.has_option(ValueOption::Filldown));
230        assert!(!v.has_option(ValueOption::List));
231    }
232
233    #[test]
234    fn test_parse_value_with_spaces_in_regex() {
235        let v = ValueDef::parse("Value Status (up|down|administratively down)", 1).unwrap();
236        assert_eq!(v.name, "Status");
237        assert_eq!(v.pattern, "(up|down|administratively down)");
238    }
239
240    #[test]
241    fn test_invalid_regex() {
242        let result = ValueDef::parse("Value Bad ([invalid)", 1);
243        assert!(matches!(result, Err(TemplateError::InvalidRegex { .. })));
244    }
245
246    #[test]
247    fn test_missing_parens() {
248        let result = ValueDef::parse("Value Name \\S+", 1);
249        assert!(matches!(result, Err(TemplateError::InvalidValue { .. })));
250    }
251
252    #[test]
253    fn test_normalize_angle_brackets() {
254        // \< and \> should be converted to < and >
255        let v = ValueDef::parse(r"Value DateTime (\S+\s+\d+\s+\d+|\<no date\>)", 1).unwrap();
256        // The pattern should have literal < and > after normalization
257        assert!(v.pattern.contains("<no date>"));
258        assert!(!v.pattern.contains(r"\<"));
259    }
260}