abol_parser/
lib.rs

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        // Canonicalize the root immediately to turn relative paths (like ../)
29        // into absolute system paths for reliable security checks.
30        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        // initialize empty dictionary
78        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) // Access the absolute root from FileOpener
91        };
92        // let canonical = Self::canonical_path(&file_path)?;
93        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        // vendor_block holds a temporary mutable vendor while inside BEGIN-VENDOR..END-VENDOR
106        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                // ATTRIBUTE lines: "ATTRIBUTE <name> <type> <oid> [encrypt]"
121                _ 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                            // skip if identical and ignoring identical
137                            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                // VALUE lines: "VALUE <attribute_name> <name> <value>"
150                _ 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                // VENDOR lines: "VENDOR <name> <code>"
162                _ 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                // BEGIN-VENDOR <name>
174                _ 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                // END-VENDOR <name>
187                _ 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                    // commit vendor_block back into dict (replace existing vendor entry)
200                    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                // $INCLUDE <path>
209                _ 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        // --- START Size and Type Parsing ---
257        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        // --- END Size and Type Parsing ---
290
291        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        // Expected: VALUE <attribute_name> <name> <value>
323        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        // Expected: VENDOR <name> <code>
340        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
387// Helper functions (module-level)
388
389fn parse_oid(s: &str) -> std::result::Result<Oid, String> {
390    // Support formats: "<code>" or "<vendor>:<code>"
391    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}