strat9_kernel/boot/
toml.rs1use alloc::{borrow::Cow, vec::Vec};
26
27#[derive(Debug, Clone)]
29pub enum ConfigValue<'a> {
30 Integer(i64),
31 Boolean(bool),
32 String(Cow<'a, str>),
35}
36
37#[derive(Debug, Clone)]
40pub struct TomlConfig<'a> {
41 entries: Vec<(&'a str, &'a str, ConfigValue<'a>)>,
42}
43
44impl<'a> TomlConfig<'a> {
45 fn lookup(&self, section: &str, key: &str) -> Option<&ConfigValue<'a>> {
46 let idx = self
47 .entries
48 .binary_search_by(|(s, k, _)| (*s, *k).cmp(&(section, key)))
49 .ok()?;
50 Some(&self.entries[idx].2)
51 }
52
53 pub fn get_int(&self, section: &str, key: &str) -> Option<i64> {
54 match self.lookup(section, key)? {
55 ConfigValue::Integer(v) => Some(*v),
56 _ => None,
57 }
58 }
59
60 pub fn get_bool(&self, section: &str, key: &str) -> Option<bool> {
61 match self.lookup(section, key)? {
62 ConfigValue::Boolean(v) => Some(*v),
63 _ => None,
64 }
65 }
66
67 pub fn get_str(&self, section: &str, key: &str) -> Option<&str> {
68 match self.lookup(section, key)? {
69 ConfigValue::String(v) => Some(v.as_ref()),
72 _ => None,
73 }
74 }
75
76 pub fn len(&self) -> usize {
77 self.entries.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.entries.is_empty()
82 }
83}
84
85pub fn parse_toml<'a>(data: &'a [u8]) -> Result<TomlConfig<'a>, &'static str> {
89 let text = core::str::from_utf8(data).map_err(|_| "invalid UTF-8 in TOML")?;
90
91 let approx_lines = text.bytes().filter(|&b| b == b'\n').count();
93 let mut entries: Vec<(&'a str, &'a str, ConfigValue<'a>)> = Vec::with_capacity(approx_lines);
94
95 let mut sec: &'a str = "";
96 let mut seen_section = false;
97
98 for raw_line in text.lines() {
99 let line = strip_comment(raw_line).trim();
100 if line.is_empty() || line.starts_with('#') {
101 continue;
102 }
103
104 if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
105 if name.contains('.') {
106 return Err("nested tables not supported");
107 }
108 if name.trim().is_empty() {
109 return Err("empty section name");
110 }
111 sec = name.trim();
112 seen_section = true;
113 continue;
114 }
115
116 if !seen_section {
117 return Err("key-value pair before any [section] header");
118 }
119
120 let (key, val) = line.split_once('=').ok_or("invalid TOML syntax")?;
123 let key = key.trim();
124 let val = val.trim();
125
126 let v = if val == "true" {
127 ConfigValue::Boolean(true)
128 } else if val == "false" {
129 ConfigValue::Boolean(false)
130 } else if let Some(hex) = val.strip_prefix("0x").or_else(|| val.strip_prefix("0X")) {
131 ConfigValue::Integer(i64::from_str_radix(hex, 16).map_err(|_| "invalid hex")?)
132 } else if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
133 let inner = &val[1..val.len() - 1];
134 ConfigValue::String(unescape_cow(inner))
135 } else {
136 ConfigValue::Integer(val.parse().map_err(|_| {
137 "unsupported value type (expected bool, int, hex, or quoted string)"
138 })?)
139 };
140
141 entries.push((sec, key, v));
142 }
143
144 entries.sort_unstable_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
147 for w in entries.windows(2) {
148 if w[0].0 == w[1].0 && w[0].1 == w[1].1 {
149 return Err("duplicate key in TOML section");
150 }
151 }
152
153 Ok(TomlConfig { entries })
154}
155
156fn strip_comment(line: &str) -> &str {
158 let mut in_str = false;
159 for (i, c) in line.char_indices() {
160 match c {
161 '"' => in_str = !in_str,
162 '#' if !in_str => return &line[..i],
163 _ => {}
164 }
165 }
166 line
167}
168
169fn unescape_cow(s: &str) -> Cow<'_, str> {
171 if !s.contains('\\') {
172 return Cow::Borrowed(s);
173 }
174 let mut out = alloc::string::String::with_capacity(s.len());
175 let mut chars = s.chars();
176 while let Some(c) = chars.next() {
177 if c == '\\' {
178 match chars.next() {
179 Some('n') => out.push('\n'),
180 Some('t') => out.push('\t'),
181 Some('r') => out.push('\r'),
182 Some('"') => out.push('"'),
183 Some('\\') => out.push('\\'),
184 Some(other) => out.push(other),
186 None => {}
187 }
188 } else {
189 out.push(c);
190 }
191 }
192 Cow::Owned(out)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_parse_simple() {
201 let toml = b"
202# Comment
203[buddy]
204compaction_threshold = 35
205";
206 let config = parse_toml(toml).unwrap();
207 assert_eq!(config.get_int("buddy", "compaction_threshold"), Some(35));
208 }
209
210 #[test]
211 fn test_parse_multiple_sections() {
212 let toml = b"
213[section1]
214key1 = 100
215key2 = true
216
217[section2]
218key3 = \"hello\"
219";
220 let config = parse_toml(toml).unwrap();
221 assert_eq!(config.get_int("section1", "key1"), Some(100));
222 assert_eq!(config.get_bool("section1", "key2"), Some(true));
223 assert_eq!(config.get_str("section2", "key3"), Some("hello"));
224 }
225
226 #[test]
227 fn test_parse_hex() {
228 let toml = b"
229[test]
230value = 0xFF
231";
232 let config = parse_toml(toml).unwrap();
233 assert_eq!(config.get_int("test", "value"), Some(255));
234 }
235
236 #[test]
237 fn test_trailing_comment_outside_string() {
238 let toml = b"
239[s]
240threshold = 35 # annotation
241name = \"a#b\" # hash inside string stays
242";
243 let config = parse_toml(toml).unwrap();
244 assert_eq!(config.get_int("s", "threshold"), Some(35));
245 assert_eq!(config.get_str("s", "name"), Some("a#b"));
246 }
247
248 #[test]
249 fn test_escape_sequences() {
250 let toml = b"
251[s]
252path = \"C:\\\\boot\\\\kernel\"
253msg = \"line\\\\nnext\"
254plain = \"no\\\\escapes\"
255";
256 let config = parse_toml(toml).unwrap();
257 assert_eq!(config.get_str("s", "path"), Some("C:\\boot\\kernel"));
258 assert_eq!(config.get_str("s", "msg"), Some("line\nnext"));
259 assert_eq!(config.get_str("s", "plain"), Some("no\\escapes"));
260 }
261
262 #[test]
263 fn test_value_containing_equals() {
264 let toml = b"
265[s]
266pair = \"key=value\"
267";
268 let config = parse_toml(toml).unwrap();
269 assert_eq!(config.get_str("s", "pair"), Some("key=value"));
270 }
271
272 #[test]
273 fn test_reject_key_before_section() {
274 let toml = b"orphan = 1\n[s]\nx = 1\n";
275 assert_eq!(
276 parse_toml(toml).err(),
277 Some("key-value pair before any [section] header")
278 );
279 }
280
281 #[test]
282 fn test_reject_duplicate_key() {
283 let toml = b"
284[s]
285k = 1
286k = 2
287";
288 assert_eq!(
289 parse_toml(toml).err(),
290 Some("duplicate key in TOML section")
291 );
292 }
293
294 #[test]
295 fn test_reject_empty_section_name() {
296 let toml = b"
297[]
298x = 1
299";
300 assert_eq!(parse_toml(toml).err(), Some("empty section name"));
301 }
302
303 #[test]
304 fn test_reject_nested_table() {
305 let toml = b"
306[a.b]
307x = 1
308";
309 assert_eq!(parse_toml(toml).err(), Some("nested tables not supported"));
310 }
311
312 #[test]
313 fn test_float_error_message() {
314 let toml = b"
315[s]
316ratio = 0.5
317";
318 assert_eq!(
319 parse_toml(toml).err(),
320 Some("unsupported value type (expected bool, int, hex, or quoted string)")
321 );
322 }
323
324 #[test]
325 fn test_malformed_quote_is_not_a_string() {
326 let toml = b"
329[s]
330val = \"not-closed
331";
332 assert!(parse_toml(toml)
333 .err()
334 .unwrap()
335 .starts_with("unsupported value type"));
336 }
337}