radius_server/
dictionary.rs

1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone)]
6pub struct RadiusAttributeDef {
7    pub name: String,
8    pub code: u32,
9    pub vendor: Option<u32>,
10    pub data_type: String,
11}
12
13#[derive(Debug)]
14pub struct Dictionary {
15    pub attributes: HashMap<u32, RadiusAttributeDef>,
16    pub vendors: HashMap<String, u32>,
17}
18
19impl Dictionary {
20    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
21        let mut attributes = HashMap::new();
22        let mut vendors = HashMap::new();
23        let mut visited = HashSet::new();
24
25        fn parse_number(s: &str) -> Result<u32, String> {
26            let s = s.trim();
27            if s.starts_with("0x") || s.starts_with("0X") {
28                u32::from_str_radix(&s[2..], 16)
29                    .map_err(|e| format!("Invalid hex '{}': {}", s, e))
30            } else {
31                s.parse::<u32>()
32                    .map_err(|e| format!("Invalid number '{}': {}", s, e))
33            }
34        }
35
36        fn parse_file(
37            path: PathBuf,
38            attributes: &mut HashMap<u32, RadiusAttributeDef>,
39            vendors: &mut HashMap<String, u32>,
40            visited: &mut HashSet<PathBuf>,
41        ) -> Result<(), String> {
42            if !visited.insert(path.clone()) {
43                return Ok(()); // Prevent cyclic includes
44            }
45
46            let content = fs::read_to_string(&path)
47                .map_err(|e| format!("Failed to read {:?}: {}", path, e))?;
48
49            for (lineno, line) in content.lines().enumerate() {
50                let line = line.trim();
51                if line.is_empty() || line.starts_with('#') {
52                    continue;
53                }
54
55                if line.starts_with("$INCLUDE") {
56                    let parts: Vec<&str> = line.split_whitespace().collect();
57                    if parts.len() == 2 {
58                        let include_path = path.parent().unwrap().join(parts[1]);
59                        parse_file(include_path, attributes, vendors, visited)?;
60                    }
61                    continue;
62                }
63
64                if line.starts_with("ATTRIBUTE") {
65                    let parts: Vec<&str> = line.split_whitespace().collect();
66                    if parts.len() >= 4 {
67                        if parts[2].contains('.') {
68                            // eprintln!(
69                            //     "⚠️ Skipping unsupported dotted ATTRIBUTE code '{}' in file {:?} at line {}",
70                            //     parts[2], path, lineno + 1
71                            // );
72                            continue;
73                        }
74                        let name = parts[1].to_string();
75                        let code = match parse_number(parts[2]) {
76                            Ok(code) => code,
77                            Err(e) => {
78                                eprintln!(
79                                    "❌ Error: {} in file {:?} at line {}",
80                                    e, path, lineno + 1
81                                );
82                                continue;
83                            }
84                        };
85                        let data_type = parts[3].to_string();
86                        attributes.insert(
87                            code,
88                            RadiusAttributeDef {
89                                name,
90                                code,
91                                vendor: None,
92                                data_type,
93                            },
94                        );
95                    }
96                    continue;
97                }
98
99                if line.starts_with("VENDOR") {
100                    let parts: Vec<&str> = line.split_whitespace().collect();
101                    if parts.len() >= 3 {
102                        if parts[2].contains('.') {
103                            // eprintln!(
104                            //     "⚠️ Skipping unsupported dotted VENDOR ID '{}' in file {:?} at line {}",
105                            //     parts[2], path, lineno + 1
106                            // );
107                            continue;
108                        }
109                        let name = parts[1].to_string();
110                        let id = match parse_number(parts[2]) {
111                            Ok(id) => id,
112                            Err(e) => {
113                                eprintln!(
114                                    "❌ Error: {} in file {:?} at line {}",
115                                    e, path, lineno + 1
116                                );
117                                continue;
118                            }
119                        };
120                        vendors.insert(name, id);
121                    }
122                    continue;
123                }
124
125                // Support other directives like BEGIN-VENDOR, VALUE, etc., as needed.
126            }
127
128            Ok(())
129        }
130
131        parse_file(path.as_ref().to_path_buf(), &mut attributes, &mut vendors, &mut visited)?;
132        Ok(Dictionary { attributes, vendors })
133    }
134}