textfsm_core/template/
value.rs1use fancy_regex::Regex;
4use std::collections::HashSet;
5
6use crate::error::TemplateError;
7use crate::types::{ValueOption, ValueOptions};
8
9#[derive(Debug, Clone)]
11pub struct ValueDef {
12 pub name: String,
14
15 pub pattern: String,
17
18 pub options: ValueOptions,
20
21 pub(crate) template_pattern: String,
23
24 pub(crate) compiled_regex: Option<Regex>,
26}
27
28impl ValueDef {
29 pub const MAX_NAME_LEN: usize = 48;
31
32 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 let rest = &trimmed[6..];
45
46 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 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 (HashSet::new(), before_parts[0].to_string())
68 }
69 2 => {
70 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 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 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 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 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 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 let pattern = Self::normalize_pattern(pattern);
132
133 Regex::new(&pattern).map_err(|e| TemplateError::InvalidRegex {
135 pattern: pattern.to_string(),
136 message: e.to_string(),
137 })?;
138
139 let inner_pattern = &pattern[1..pattern.len() - 1];
141 let template_pattern = format!("(?P<{}>{})", name, inner_pattern);
142
143 let compiled_regex = if options.contains(&ValueOption::List) {
145 let re = Regex::new(&pattern).ok();
146 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 pub fn has_option(&self, opt: ValueOption) -> bool {
183 self.options.contains(&opt)
184 }
185
186 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 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 let v = ValueDef::parse(r"Value DateTime (\S+\s+\d+\s+\d+|\<no date\>)", 1).unwrap();
256 assert!(v.pattern.contains("<no date>"));
258 assert!(!v.pattern.contains(r"\<"));
259 }
260}