1pub mod dictionary;
2
3use std::{
4 collections::HashSet,
5 fs::File,
6 io::{BufRead, BufReader},
7 path::{Path, PathBuf},
8};
9
10use anyhow::{Context, Result, bail, ensure};
11
12use crate::dictionary::{
13 AttributeType, DictionaryAttribute, DictionaryValue, DictionaryVendor, Oid, SizeFlag,
14};
15
16pub struct AttributeFlags {
17 pub encrypt: Option<u8>,
18}
19
20#[derive(Debug, Clone)]
21pub struct FileOpener {
22 root: PathBuf,
23}
24
25impl FileOpener {
26 pub fn new(root: impl Into<PathBuf>) -> Self {
27 let root_path = root.into();
28 let canonical_root = root_path.canonicalize().unwrap_or(root_path);
31 Self {
32 root: canonical_root,
33 }
34 }
35
36 pub fn get_root(&self) -> &Path {
37 &self.root
38 }
39
40 pub fn open_file(&self, relative_path: impl AsRef<Path>) -> Result<File> {
41 let relative_path = relative_path.as_ref();
42 let full_path = if relative_path.is_absolute() {
43 relative_path.to_path_buf()
44 } else {
45 self.root.join(relative_path)
46 };
47 let abs_path = full_path
48 .canonicalize()
49 .with_context(|| format!("failed to resolve absolute path for {:?}", full_path))?;
50 ensure!(
51 abs_path.starts_with(&self.root),
52 "attempted to open file {:?} outside of root {:?}",
53 abs_path,
54 self.root
55 );
56 let file =
57 File::open(&abs_path).with_context(|| format!("failed to open file {:?}", abs_path))?;
58 println!("Opened file {:?}", abs_path);
59 Ok(file)
60 }
61}
62
63pub struct Parser {
64 pub file_opener: FileOpener,
65 pub ignore_identical_attributes: bool,
66}
67
68impl Parser {
69 pub fn new(file_opener: FileOpener, ignore_identical_attributes: bool) -> Self {
70 Self {
71 file_opener,
72 ignore_identical_attributes,
73 }
74 }
75
76 pub fn parse_dictionary(&self, file_path: impl AsRef<Path>) -> Result<dictionary::Dictionary> {
77 let mut dict = dictionary::Dictionary {
79 attributes: Vec::new(),
80 values: Vec::new(),
81 vendors: Vec::new(),
82 };
83
84 let file = self.file_opener.open_file(&file_path)?;
85 let mut parsed = HashSet::new();
86 let p = file_path.as_ref();
87 let full_path = if p.is_absolute() {
88 p.to_path_buf()
89 } else {
90 self.file_opener.get_root().join(p) };
92 parsed.insert(full_path);
94 self.parse(&mut dict, &mut parsed, file)?;
95 Ok(dict)
96 }
97
98 fn parse(
99 &self,
100 dict: &mut dictionary::Dictionary,
101 parsed_files: &mut HashSet<PathBuf>,
102 file: File,
103 ) -> Result<()> {
104 let reader = BufReader::new(file);
105 let mut vendor_block: Option<DictionaryVendor> = None;
107
108 for (idx, raw_line) in reader.lines().enumerate() {
109 let line_no = idx + 1;
110 let mut line = raw_line.with_context(|| format!("reading line {}", line_no))?;
111 if let Some(comment_start) = line.find('#') {
112 line.truncate(comment_start);
113 }
114 if line.trim().is_empty() {
115 continue;
116 }
117 let fields: Vec<&str> = line.split_whitespace().collect();
118
119 match () {
120 _ if (fields.len() == 4 || fields.len() == 5) && fields[0] == "ATTRIBUTE" => {
122 let attr = self
123 .parse_attribute(&fields)
124 .map_err(|e| anyhow::anyhow!("line {}: {}", line_no, e))?;
125
126 let existing = if vendor_block.is_none() {
127 attribute_by_name(&dict.attributes, &attr.name)
128 } else {
129 vendor_block
130 .as_ref()
131 .and_then(|v| attribute_by_name(&v.attributes, &attr.name))
132 };
133
134 if let Some(existing_attr) = existing {
135 if self.ignore_identical_attributes && attr == *existing_attr {
136 continue;
138 }
139 bail!("line {}: duplicate attribute '{}'", line_no, attr.name);
140 }
141
142 if let Some(vb) = vendor_block.as_mut() {
143 vb.attributes.push(attr);
144 } else {
145 dict.attributes.push(attr);
146 }
147 }
148
149 _ if fields.len() == 4 && fields[0] == "VALUE" => {
151 let value = self
152 .parse_value(&fields)
153 .map_err(|e| anyhow::anyhow!("line {}: {}", line_no, e))?;
154 if let Some(vb) = vendor_block.as_mut() {
155 vb.values.push(value);
156 } else {
157 dict.values.push(value);
158 }
159 }
160
161 _ if (fields.len() == 3 || fields.len() == 4) && fields[0] == "VENDOR" => {
163 let vendor = self
164 .parse_vendor(&fields)
165 .map_err(|e| anyhow::anyhow!("line {}: {}", line_no, e))?;
166 if vendor_by_name_or_number(&dict.vendors, &vendor.name, vendor.code).is_some()
167 {
168 bail!("line {}: duplicate vendor '{}'", line_no, vendor.name);
169 }
170 dict.vendors.push(vendor);
171 }
172
173 _ if fields.len() == 2 && fields[0] == "BEGIN-VENDOR" => {
175 if vendor_block.is_some() {
176 bail!("line {}: nested vendor block not allowed", line_no);
177 }
178 let vendor = vendor_by_name(&dict.vendors, fields[1])
179 .ok_or_else(|| {
180 anyhow::anyhow!("line {}: unknown vendor '{}'", line_no, fields[1])
181 })?
182 .clone();
183 vendor_block = Some(vendor);
184 }
185
186 _ if fields.len() == 2 && fields[0] == "END-VENDOR" => {
188 if vendor_block.is_none() {
189 bail!("line {}: unmatched END-VENDOR", line_no);
190 }
191 if vendor_block.as_ref().unwrap().name != fields[1] {
192 bail!(
193 "line {}: invalid END-VENDOR '{}', expected '{}'",
194 line_no,
195 fields[1],
196 vendor_block.as_ref().unwrap().name
197 );
198 }
199 let vb = vendor_block.take().unwrap();
201 if let Some(pos) = dict.vendors.iter().position(|v| v.name == vb.name) {
202 dict.vendors[pos] = vb;
203 } else {
204 dict.vendors.push(vb);
205 }
206 }
207
208 _ if fields.len() == 2 && fields[0] == "$INCLUDE" => {
210 if vendor_block.is_some() {
211 bail!("line {}: $INCLUDE not allowed inside vendor block", line_no);
212 }
213 let include_path = fields[1];
214 let include_path = PathBuf::from(include_path);
215 let inc_file =
216 self.file_opener.open_file(&include_path).with_context(|| {
217 format!(
218 "line {}: failed to open include {}",
219 line_no,
220 include_path.display()
221 )
222 })?;
223 let inc_canonical = Self::canonical_path(&include_path)?;
224 if parsed_files.contains(&inc_canonical) {
225 bail!(
226 "line {}: recursive include {}",
227 line_no,
228 include_path.display()
229 );
230 }
231 parsed_files.insert(inc_canonical.clone());
232 self.parse(dict, parsed_files, inc_file)?;
233 }
234
235 _ => {
236 bail!("line {}: unknown line: {}", line_no, line);
237 }
238 }
239 }
240
241 if vendor_block.is_some() {
242 bail!("unclosed vendor block at EOF");
243 }
244
245 Ok(())
246 }
247
248 fn parse_attribute(&self, fields: &[&str]) -> std::result::Result<DictionaryAttribute, String> {
249 if fields.len() < 4 {
250 return Err("ATTRIBUTE line too short".into());
251 }
252
253 let name = fields[1].to_string();
254 let oid = parse_oid(fields[2])?;
255
256 let raw_type = fields[3];
258 let (attr_type, size) = if let Some(start) = raw_type.find('[') {
259 let end = raw_type
260 .find(']')
261 .ok_or("Missing closing bracket in type")?;
262 let base_type_str = &raw_type[..start];
263 let size_content = &raw_type[start + 1..end];
264
265 let base_type = Self::parse_attribute_type(base_type_str)?;
266
267 let size_flag = if let Some((min_s, max_s)) = size_content.split_once('-') {
268 let min = min_s
269 .trim()
270 .parse::<u32>()
271 .map_err(|_| "Invalid min range")?;
272 let max = max_s
273 .trim()
274 .parse::<u32>()
275 .map_err(|_| "Invalid max range")?;
276 SizeFlag::Range(min, max)
277 } else {
278 let exact = size_content
279 .trim()
280 .parse::<u32>()
281 .map_err(|_| "Invalid exact size")?;
282 SizeFlag::Exact(exact)
283 };
284
285 (base_type, size_flag)
286 } else {
287 (Self::parse_attribute_type(raw_type)?, SizeFlag::Any)
288 };
289 let mut encrypt = None;
292 let mut concat = None;
293
294 for &flag in &fields[4..] {
295 let flag_lc = flag.to_lowercase();
296 if flag_lc.starts_with("encrypt=") {
297 let (_, value) = flag
298 .split_once('=')
299 .ok_or_else(|| "invalid encrypt format".to_string())?;
300 encrypt = Some(
301 value
302 .parse::<u8>()
303 .map_err(|e| format!("invalid encrypt: {}", e))?,
304 );
305 } else if flag_lc == "concat" {
306 concat = Some(true);
307 }
308 }
309
310 Ok(DictionaryAttribute {
311 name,
312 oid,
313 attr_type,
314 size,
315 encrypt,
316 has_tag: None,
317 concat,
318 })
319 }
320
321 fn parse_value(&self, fields: &[&str]) -> std::result::Result<DictionaryValue, String> {
322 if fields.len() != 4 {
324 return Err("VALUE line must have 4 fields".into());
325 }
326 let attribute_name = fields[1].to_string();
327 let name = fields[2].to_string();
328 let value = fields[3]
329 .parse::<u64>()
330 .map_err(|e| format!("invalid value: {}", e))?;
331 Ok(DictionaryValue {
332 attribute_name,
333 name,
334 value,
335 })
336 }
337
338 fn parse_vendor(&self, fields: &[&str]) -> std::result::Result<DictionaryVendor, String> {
339 if fields.len() < 3 {
341 return Err("VENDOR line too short".into());
342 }
343 let name = fields[1].to_string();
344 let code = fields[2]
345 .parse::<u32>()
346 .map_err(|e| format!("invalid vendor code: {}", e))?;
347 Ok(DictionaryVendor {
348 name,
349 code,
350 attributes: Vec::new(),
351 values: Vec::new(),
352 })
353 }
354
355 fn canonical_path(p: &impl AsRef<Path>) -> Result<PathBuf> {
356 let path = p.as_ref();
357
358 let abs_path = path
359 .canonicalize()
360 .with_context(|| format!("failed to resolve absolute path for {:?}", path))?;
361
362 Ok(abs_path)
363 }
364
365 fn parse_attribute_type(s: &str) -> std::result::Result<AttributeType, String> {
366 Ok(match s.to_lowercase().as_str() {
367 "string" => AttributeType::String,
368 "integer" => AttributeType::Integer,
369 "ipaddr" => AttributeType::IpAddr,
370 "octets" => AttributeType::Octets,
371 "date" => AttributeType::Date,
372 "tlv" => AttributeType::Tlv,
373 "byte" => AttributeType::Byte,
374 "short" => AttributeType::Short,
375 "signed" => AttributeType::Signed,
376 "ipv4prefix" => AttributeType::Ipv4Prefix,
377 "vsa" => AttributeType::Vsa,
378 "ifid" => AttributeType::Ifid,
379 "ipv6addr" => AttributeType::Ipv6Addr,
380 "ipv6prefix" => AttributeType::Ipv6Prefix,
381 "interface-id" => AttributeType::InterfaceId,
382 other => AttributeType::Unknown(other.to_string()),
383 })
384 }
385}
386
387fn parse_oid(s: &str) -> std::result::Result<Oid, String> {
390 if let Some(idx) = s.find(':') {
392 let vendor = s[..idx]
393 .parse::<u32>()
394 .map_err(|e| format!("invalid vendor id: {}", e))?;
395 let code = s[idx + 1..]
396 .parse::<u32>()
397 .map_err(|e| format!("invalid code: {}", e))?;
398 Ok(Oid {
399 vendor: Some(vendor),
400 code,
401 })
402 } else {
403 let code = s
404 .parse::<u32>()
405 .map_err(|e| format!("invalid code: {}", e))?;
406 Ok(Oid { vendor: None, code })
407 }
408}
409
410fn attribute_by_name<'a>(
411 attrs: &'a [DictionaryAttribute],
412 name: &str,
413) -> Option<&'a DictionaryAttribute> {
414 attrs.iter().find(|a| a.name == name)
415}
416
417fn vendor_by_name<'a>(vendors: &'a [DictionaryVendor], name: &str) -> Option<&'a DictionaryVendor> {
418 vendors.iter().find(|v| v.name == name)
419}
420
421fn vendor_by_name_or_number<'a>(
422 vendors: &'a [DictionaryVendor],
423 name: &str,
424 code: u32,
425) -> Option<&'a DictionaryVendor> {
426 vendors.iter().find(|v| v.name == name || v.code == code)
427}