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::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 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::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 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 if let Some(&next) = chars.peek() {
198 if next == '<' || next == '>' {
199 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 let v = ValueDef::parse(r"Value DateTime (\S+\s+\d+\s+\d+|\<no date\>)", 1).unwrap();
257 assert!(v.pattern.contains("<no date>"));
259 assert!(!v.pattern.contains(r"\<"));
260 }
261}