1use crate::{Result, TextFsmError};
2use fancy_regex::Regex;
3use log::{debug, trace};
4use std::collections::HashMap;
5use std::path::Path;
6
7#[derive(Debug, Clone)]
9pub struct ParsedCliTable {
10 pub fname: String,
12 pub rows: Vec<CliTableRow>,
14}
15
16#[derive(Debug, Clone)]
18pub struct CliTable {
19 pub tables: Vec<ParsedCliTable>,
21 pub platform_regex_rules: HashMap<String, Vec<CliTableRegexRule>>,
23}
24
25#[derive(Debug, Clone)]
27pub struct CliTableRegexRule {
28 pub table_index: usize,
30 pub row_index: usize,
32 pub command_regex: Regex,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct CliTableRow {
39 pub templates: Vec<String>,
41 pub hostname: Option<String>,
43 pub platform: Option<String>,
45 pub command: String,
47}
48
49impl ParsedCliTable {
50 fn parse(fname: &Path) -> Result<Vec<CliTableRow>> {
51 use std::io::BufReader;
52 let file = std::fs::File::open(fname)?;
53 let reader = BufReader::new(file);
54 let mut rows: Vec<CliTableRow> = vec![];
55 let mut rdr = csv::ReaderBuilder::new()
56 .comment(Some(b'#'))
57 .has_headers(true)
58 .delimiter(b',')
59 .trim(csv::Trim::All)
60 .from_reader(reader);
61 trace!("Reader");
62
63 let headers: Vec<&str> = rdr.headers()?.into_iter().collect();
64 trace!("Headers: {:?}", &headers);
65
66 if !headers.contains(&"Template") {
67 return Err(TextFsmError::ParseError(
68 "No 'Template' column in index file".into(),
69 ));
70 }
71 if !headers.contains(&"Command") {
72 return Err(TextFsmError::ParseError(
73 "No 'Command' column in index file".into(),
74 ));
75 }
76
77 let template_position = headers.iter().position(|x| *x == "Template").unwrap();
78 let command_position = headers.iter().position(|x| *x == "Command").unwrap();
79 let maybe_platform_position = headers
80 .iter()
81 .position(|x| *x == "Platform")
82 .or_else(|| headers.iter().position(|x| *x == "Vendor"));
83 let maybe_hostname_position = headers.iter().position(|x| *x == "Hostname");
84
85 for result in rdr.records() {
86 let record = result?;
87 let platform: Option<String> =
88 maybe_platform_position.map(|ppos| record[ppos].to_string());
89 let hostname: Option<String> =
90 maybe_hostname_position.map(|hpos| record[hpos].to_string());
91 let templates: Vec<String> = record[template_position]
92 .split(':')
93 .map(|x| x.to_string())
94 .collect();
95 let command = record[command_position].to_string();
96
97 let row = CliTableRow {
98 templates,
99 hostname,
100 platform,
101 command,
102 };
103 rows.push(row);
104 }
105 Ok(rows)
106 }
107
108 pub fn from_file<P: AsRef<Path>>(fname: P) -> Result<Self> {
110 let path = fname.as_ref();
111 debug!("Loading cli table from {}", path.display());
112 let rows = Self::parse(path)?;
113 Ok(ParsedCliTable {
114 fname: path.to_string_lossy().into_owned(),
115 rows,
116 })
117 }
118}
119
120impl CliTable {
121 fn expand_string(input: &str) -> String {
124 if input.is_empty() {
125 return String::new();
126 }
127
128 let count = input.chars().count();
129 let mut result = String::with_capacity(count * 4);
130
131 for c in input.chars() {
132 result.push('(');
133 result.push(c);
134 }
135
136 for _ in 0..count {
137 result.push_str(")?");
138 }
139
140 result
141 }
142
143 fn expand_brackets(input: &str) -> String {
145 let mut result = String::with_capacity(input.len());
146 let mut current_pos = 0;
147
148 while let Some(start) = input[current_pos..].find("[[") {
149 result.push_str(&input[current_pos..current_pos + start]);
151
152 let content_start = current_pos + start + 2;
154
155 if let Some(end) = input[content_start..].find("]]") {
157 let content = &input[content_start..content_start + end];
158 let expanded = Self::expand_string(content);
159 result.push_str(&expanded);
160 current_pos = content_start + end + 2;
161 } else {
162 result.push_str("[[");
164 current_pos = content_start;
165 }
166 }
167
168 result.push_str(&input[current_pos..]);
170 result
171 }
172
173 fn get_directory(filename: &str) -> Option<String> {
174 let path = Path::new(filename);
175 path.parent().map(|p| p.to_string_lossy().into_owned())
176 }
177
178 pub fn get_template_for_command(
180 &self,
181 platform: &str,
182 cmd: &str,
183 ) -> Option<(String, CliTableRow)> {
184 let plat_regex_list = self.platform_regex_rules.get(platform)?;
185 for rule in plat_regex_list {
186 if rule.command_regex.is_match(cmd).expect("Fancy regex ok?") {
187 let row = self.tables[rule.table_index].rows[rule.row_index].clone();
188 let fname = &self.tables[rule.table_index].fname;
189 if let Some(fdir) = Self::get_directory(fname) {
190 return Some((fdir, row));
191 }
192 }
193 }
194 None
195 }
196
197 pub fn from_file<P: AsRef<Path>>(fname: P) -> Result<Self> {
199 let parsed_cli_table = ParsedCliTable::from_file(fname)?;
200 let tables = vec![parsed_cli_table];
201 let mut platform_regex_rules: HashMap<String, Vec<CliTableRegexRule>> = Default::default();
202
203 for (table_index, table) in tables.iter().enumerate() {
204 for (row_index, row) in table.rows.iter().enumerate() {
205 let expanded_command = Self::expand_brackets(&row.command);
206 let anchored_command = format!("^{}$", expanded_command);
207 let command_regex = Regex::new(&anchored_command)
208 .map_err(|e| TextFsmError::ParseError(e.to_string()))?;
209
210 let rule = CliTableRegexRule {
211 table_index,
212 row_index,
213 command_regex,
214 };
215 let no_platform = "no-platform".to_string();
216 let platform_name: &str = row.platform.as_ref().unwrap_or(&no_platform);
217 platform_regex_rules
218 .entry(platform_name.into())
219 .or_default()
220 .push(rule);
221 }
222 }
223 Ok(CliTable {
224 platform_regex_rules,
225 tables,
226 })
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_expand_string() {
236 assert_eq!(CliTable::expand_string(""), "");
237 assert_eq!(CliTable::expand_string("w"), "(w)?");
238 assert_eq!(CliTable::expand_string("sh"), "(s(h)?)?");
239 assert_eq!(CliTable::expand_string("sho"), "(s(h(o)?)?)?");
240 assert_eq!(CliTable::expand_string("show"), "(s(h(o(w)?)?)?)?");
241 }
242
243 #[test]
244 fn test_expand_brackets() {
245 assert_eq!(CliTable::expand_brackets("show"), "show");
246 assert_eq!(CliTable::expand_brackets("sh[[ow]]"), "sh(o(w)?)?");
247 assert_eq!(CliTable::expand_brackets("[[show]]"), "(s(h(o(w)?)?)?)?");
248 assert_eq!(
249 CliTable::expand_brackets("sh[[ow]] ip bgp"),
250 "sh(o(w)?)? ip bgp"
251 );
252 assert_eq!(
253 CliTable::expand_brackets("sh[[ow]] ip bgp su[[mmary]]"),
254 "sh(o(w)?)? ip bgp su(m(m(a(r(y)?)?)?)?)?"
255 );
256 }
257}