Skip to main content

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_embedded() -> Result<Self, String> {
21        let embedded = include_str!("../dictionaries/dictionary");
22        Self::parse_from_str(embedded)
23    }
24 pub fn parse_from_str(content: &str) -> Result<Self, String> {
25        let mut attributes = HashMap::new();
26        let mut vendors = HashMap::new();
27
28        for (lineno, line) in content.lines().enumerate() {
29            let line = line.trim();
30            if line.is_empty() || line.starts_with('#') {
31                continue;
32            }
33
34            if line.starts_with("ATTRIBUTE") {
35                let parts: Vec<&str> = line.split_whitespace().collect();
36                if parts.len() >= 4 {
37                    let name = parts[1].to_string();
38                    let code = parts[2].parse::<u32>()
39                        .map_err(|e| format!("Invalid code on line {}: {}", lineno + 1, e))?;
40                    let data_type = parts[3].to_string();
41
42                    attributes.insert(code, RadiusAttributeDef {
43                        name,
44                        code,
45                        vendor: None,
46                        data_type,
47                    });
48                }
49            } else if line.starts_with("VENDOR") {
50                let parts: Vec<&str> = line.split_whitespace().collect();
51                if parts.len() >= 3 {
52                    let name = parts[1].to_string();
53                    let id = parts[2].parse::<u32>()
54                        .map_err(|e| format!("Invalid vendor ID on line {}: {}", lineno + 1, e))?;
55                    vendors.insert(name, id);
56                }
57            }
58        }
59
60        Ok(Dictionary { attributes, vendors })
61    }
62    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
63        let mut attributes = HashMap::new();
64        let mut vendors = HashMap::new();
65        let mut visited = HashSet::new();
66
67        fn parse_number(s: &str) -> Result<u32, String> {
68            let s = s.trim();
69            if s.starts_with("0x") || s.starts_with("0X") {
70                u32::from_str_radix(&s[2..], 16)
71                    .map_err(|e| format!("Invalid hex '{}': {}", s, e))
72            } else {
73                s.parse::<u32>()
74                    .map_err(|e| format!("Invalid number '{}': {}", s, e))
75            }
76        }
77
78        fn parse_file(
79            path: PathBuf,
80            attributes: &mut HashMap<u32, RadiusAttributeDef>,
81            vendors: &mut HashMap<String, u32>,
82            visited: &mut HashSet<PathBuf>,
83        ) -> Result<(), String> {
84            if !visited.insert(path.clone()) {
85                return Ok(()); // Prevent cyclic includes
86            }
87
88            let content = fs::read_to_string(&path)
89                .map_err(|e| format!("Failed to read {:?}: {}", path, e))?;
90
91            for (lineno, line) in content.lines().enumerate() {
92                let line = line.trim();
93                if line.is_empty() || line.starts_with('#') {
94                    continue;
95                }
96
97                if line.starts_with("$INCLUDE") {
98                    let parts: Vec<&str> = line.split_whitespace().collect();
99                    if parts.len() == 2 {
100                        let include_path = path.parent().unwrap().join(parts[1]);
101                        parse_file(include_path, attributes, vendors, visited)?;
102                    }
103                    continue;
104                }
105
106                if line.starts_with("ATTRIBUTE") {
107                    let parts: Vec<&str> = line.split_whitespace().collect();
108                    if parts.len() >= 4 {
109                        if parts[2].contains('.') {
110                            // eprintln!(
111                            //     "⚠️ Skipping unsupported dotted ATTRIBUTE code '{}' in file {:?} at line {}",
112                            //     parts[2], path, lineno + 1
113                            // );
114                            continue;
115                        }
116                        let name = parts[1].to_string();
117                        let code = match parse_number(parts[2]) {
118                            Ok(code) => code,
119                            Err(e) => {
120                                eprintln!(
121                                    "❌ Error: {} in file {:?} at line {}",
122                                    e, path, lineno + 1
123                                );
124                                continue;
125                            }
126                        };
127                        let data_type = parts[3].to_string();
128                        attributes.insert(
129                            code,
130                            RadiusAttributeDef {
131                                name,
132                                code,
133                                vendor: None,
134                                data_type,
135                            },
136                        );
137                    }
138                    continue;
139                }
140
141                if line.starts_with("VENDOR") {
142                    let parts: Vec<&str> = line.split_whitespace().collect();
143                    if parts.len() >= 3 {
144                        if parts[2].contains('.') {
145                            // eprintln!(
146                            //     "⚠️ Skipping unsupported dotted VENDOR ID '{}' in file {:?} at line {}",
147                            //     parts[2], path, lineno + 1
148                            // );
149                            continue;
150                        }
151                        let name = parts[1].to_string();
152                        let id = match parse_number(parts[2]) {
153                            Ok(id) => id,
154                            Err(e) => {
155                                eprintln!(
156                                    "❌ Error: {} in file {:?} at line {}",
157                                    e, path, lineno + 1
158                                );
159                                continue;
160                            }
161                        };
162                        vendors.insert(name, id);
163                    }
164                    continue;
165                }
166
167                // Support other directives like BEGIN-VENDOR, VALUE, etc., as needed.
168            }
169
170            Ok(())
171        }
172
173        parse_file(path.as_ref().to_path_buf(), &mut attributes, &mut vendors, &mut visited)?;
174        Ok(Dictionary { attributes, vendors })
175    }
176}