⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
toml_parser.rs

1//! Minimal TOML Parser for Portfile.toml
2//!
3//! A no_std-compatible TOML parser supporting key-value pairs, sections,
4//! arrays, and inline tables. Designed specifically for parsing port
5//! definition files in the VeridianOS ports system.
6
7#[cfg(feature = "alloc")]
8use alloc::{collections::BTreeMap, string::String, vec::Vec};
9
10#[cfg(feature = "alloc")]
11use crate::error::KernelError;
12
13/// A parsed TOML value.
14#[cfg(feature = "alloc")]
15#[derive(Debug, Clone, PartialEq)]
16pub enum TomlValue {
17    /// A string value (e.g., `"hello"`)
18    String(String),
19    /// A 64-bit signed integer (e.g., `42`)
20    Integer(i64),
21    /// A boolean value (e.g., `true`)
22    Boolean(bool),
23    /// An array of values (e.g., `["a", "b"]`)
24    Array(Vec<TomlValue>),
25    /// A table / map of key-value pairs
26    Table(BTreeMap<String, TomlValue>),
27}
28
29#[cfg(feature = "alloc")]
30impl TomlValue {
31    /// Try to interpret this value as a string reference.
32    pub fn as_str(&self) -> Option<&str> {
33        match self {
34            TomlValue::String(s) => Some(s.as_str()),
35            _ => None,
36        }
37    }
38
39    /// Try to interpret this value as an integer.
40    pub fn as_integer(&self) -> Option<i64> {
41        match self {
42            TomlValue::Integer(n) => Some(*n),
43            _ => None,
44        }
45    }
46
47    /// Try to interpret this value as a boolean.
48    pub fn as_bool(&self) -> Option<bool> {
49        match self {
50            TomlValue::Boolean(b) => Some(*b),
51            _ => None,
52        }
53    }
54
55    /// Try to interpret this value as an array.
56    pub fn as_array(&self) -> Option<&[TomlValue]> {
57        match self {
58            TomlValue::Array(a) => Some(a.as_slice()),
59            _ => None,
60        }
61    }
62
63    /// Try to interpret this value as a table.
64    pub fn as_table(&self) -> Option<&BTreeMap<String, TomlValue>> {
65        match self {
66            TomlValue::Table(t) => Some(t),
67            _ => None,
68        }
69    }
70}
71
72/// Parse a TOML string into a nested `BTreeMap<String, TomlValue>`.
73///
74/// Supports:
75/// - Key-value pairs: `key = "value"`, `key = 42`, `key = true`
76/// - Sections: `[section]`
77/// - Arrays: `["a", "b", "c"]`
78/// - Inline tables: `{ key = "value", key2 = 42 }`
79#[cfg(feature = "alloc")]
80pub fn parse_toml(input: &str) -> Result<BTreeMap<String, TomlValue>, KernelError> {
81    let mut root = BTreeMap::new();
82    let mut current_section: Option<String> = None;
83
84    for raw_line in input.lines() {
85        let line = strip_comment(raw_line).trim();
86        if line.is_empty() {
87            continue;
88        }
89
90        // Section header: [section]
91        if line.starts_with('[') && line.ends_with(']') && !line.starts_with("[[") {
92            let section_name = line[1..line.len() - 1].trim();
93            if section_name.is_empty() {
94                return Err(KernelError::InvalidArgument {
95                    name: "toml_section",
96                    value: "empty_section_name",
97                });
98            }
99            current_section = Some(String::from(section_name));
100            // Ensure the section table exists
101            root.entry(String::from(section_name))
102                .or_insert_with(|| TomlValue::Table(BTreeMap::new()));
103            continue;
104        }
105
106        // Key-value pair: key = value
107        if let Some((key, value)) = split_key_value(line) {
108            let key = key.trim();
109            let value = value.trim();
110
111            let parsed_value = parse_value(value)?;
112
113            if let Some(ref section) = current_section {
114                // Insert into current section table
115                if let Some(TomlValue::Table(table)) = root.get_mut(section) {
116                    table.insert(String::from(key), parsed_value);
117                }
118            } else {
119                // Insert into root
120                root.insert(String::from(key), parsed_value);
121            }
122        }
123    }
124
125    Ok(root)
126}
127
128/// Strip inline comments (everything after an unquoted `#`).
129#[cfg(feature = "alloc")]
130fn strip_comment(line: &str) -> &str {
131    let mut in_string = false;
132    for (i, c) in line.char_indices() {
133        match c {
134            '"' => in_string = !in_string,
135            '#' if !in_string => return &line[..i],
136            _ => {}
137        }
138    }
139    line
140}
141
142/// Split `key = value` at the first unquoted `=`.
143#[cfg(feature = "alloc")]
144fn split_key_value(line: &str) -> Option<(&str, &str)> {
145    let mut in_string = false;
146    for (i, c) in line.char_indices() {
147        match c {
148            '"' => in_string = !in_string,
149            '=' if !in_string => {
150                return Some((&line[..i], &line[i + 1..]));
151            }
152            _ => {}
153        }
154    }
155    None
156}
157
158/// Parse a single TOML value from its string representation.
159#[cfg(feature = "alloc")]
160fn parse_value(s: &str) -> Result<TomlValue, KernelError> {
161    let s = s.trim();
162
163    // Quoted string
164    if s.starts_with('"') {
165        return parse_string(s);
166    }
167
168    // Boolean
169    if s == "true" {
170        return Ok(TomlValue::Boolean(true));
171    }
172    if s == "false" {
173        return Ok(TomlValue::Boolean(false));
174    }
175
176    // Array
177    if s.starts_with('[') {
178        return parse_array(s);
179    }
180
181    // Inline table
182    if s.starts_with('{') {
183        return parse_inline_table(s);
184    }
185
186    // Integer (try last to avoid misinterpreting other tokens)
187    if let Some(n) = try_parse_integer(s) {
188        return Ok(TomlValue::Integer(n));
189    }
190
191    Err(KernelError::InvalidArgument {
192        name: "toml_value",
193        value: "unrecognised_value",
194    })
195}
196
197/// Parse a quoted string value. Handles basic escape sequences.
198#[cfg(feature = "alloc")]
199fn parse_string(s: &str) -> Result<TomlValue, KernelError> {
200    if !s.starts_with('"') {
201        return Err(KernelError::InvalidArgument {
202            name: "toml_string",
203            value: "missing_opening_quote",
204        });
205    }
206
207    let mut result = String::new();
208    let mut chars = s[1..].chars();
209    let mut closed = false;
210
211    while let Some(c) = chars.next() {
212        match c {
213            '"' => {
214                closed = true;
215                break;
216            }
217            '\\' => {
218                let escaped = chars.next().ok_or(KernelError::InvalidArgument {
219                    name: "toml_string",
220                    value: "unterminated_escape",
221                })?;
222                match escaped {
223                    'n' => result.push('\n'),
224                    't' => result.push('\t'),
225                    'r' => result.push('\r'),
226                    '\\' => result.push('\\'),
227                    '"' => result.push('"'),
228                    _ => {
229                        result.push('\\');
230                        result.push(escaped);
231                    }
232                }
233            }
234            _ => result.push(c),
235        }
236    }
237
238    if !closed {
239        return Err(KernelError::InvalidArgument {
240            name: "toml_string",
241            value: "unterminated_string",
242        });
243    }
244
245    Ok(TomlValue::String(result))
246}
247
248/// Parse an array value: `[val1, val2, ...]`
249#[cfg(feature = "alloc")]
250fn parse_array(s: &str) -> Result<TomlValue, KernelError> {
251    if !s.starts_with('[') || !s.ends_with(']') {
252        return Err(KernelError::InvalidArgument {
253            name: "toml_array",
254            value: "malformed_array",
255        });
256    }
257
258    let inner = s[1..s.len() - 1].trim();
259    if inner.is_empty() {
260        return Ok(TomlValue::Array(Vec::new()));
261    }
262
263    let elements = split_top_level(inner, ',');
264    let mut values = Vec::new();
265    for elem in elements {
266        let elem = elem.trim();
267        if !elem.is_empty() {
268            values.push(parse_value(elem)?);
269        }
270    }
271
272    Ok(TomlValue::Array(values))
273}
274
275/// Parse an inline table: `{ key = val, key2 = val2 }`
276#[cfg(feature = "alloc")]
277fn parse_inline_table(s: &str) -> Result<TomlValue, KernelError> {
278    if !s.starts_with('{') || !s.ends_with('}') {
279        return Err(KernelError::InvalidArgument {
280            name: "toml_table",
281            value: "malformed_inline_table",
282        });
283    }
284
285    let inner = s[1..s.len() - 1].trim();
286    if inner.is_empty() {
287        return Ok(TomlValue::Table(BTreeMap::new()));
288    }
289
290    let pairs = split_top_level(inner, ',');
291    let mut table = BTreeMap::new();
292    for pair in pairs {
293        let pair = pair.trim();
294        if pair.is_empty() {
295            continue;
296        }
297        if let Some((key, value)) = split_key_value(pair) {
298            table.insert(String::from(key.trim()), parse_value(value)?);
299        } else {
300            return Err(KernelError::InvalidArgument {
301                name: "toml_table",
302                value: "missing_equals_in_inline_table",
303            });
304        }
305    }
306
307    Ok(TomlValue::Table(table))
308}
309
310/// Split a string by `delimiter`, respecting quoted strings and nested
311/// brackets / braces.
312#[cfg(feature = "alloc")]
313fn split_top_level(s: &str, delimiter: char) -> Vec<&str> {
314    let mut parts = Vec::new();
315    let mut depth = 0i32;
316    let mut in_string = false;
317    let mut start = 0;
318
319    for (i, c) in s.char_indices() {
320        match c {
321            '"' => in_string = !in_string,
322            '[' | '{' if !in_string => depth += 1,
323            ']' | '}' if !in_string => depth -= 1,
324            c if c == delimiter && !in_string && depth == 0 => {
325                parts.push(&s[start..i]);
326                start = i + 1;
327            }
328            _ => {}
329        }
330    }
331    // Push the final segment
332    if start <= s.len() {
333        parts.push(&s[start..]);
334    }
335
336    parts
337}
338
339/// Try to parse a string as a signed 64-bit integer.
340#[cfg(feature = "alloc")]
341fn try_parse_integer(s: &str) -> Option<i64> {
342    let s = s.trim();
343    if s.is_empty() {
344        return None;
345    }
346
347    // Handle hex, octal, binary prefixes
348    if s.starts_with("0x") || s.starts_with("0X") {
349        return i64::from_str_radix(&s[2..].replace('_', ""), 16).ok();
350    }
351    if s.starts_with("0o") || s.starts_with("0O") {
352        return i64::from_str_radix(&s[2..].replace('_', ""), 8).ok();
353    }
354    if s.starts_with("0b") || s.starts_with("0B") {
355        return i64::from_str_radix(&s[2..].replace('_', ""), 2).ok();
356    }
357
358    // Decimal -- allow underscores as visual separators
359    let cleaned: String = s.chars().filter(|&c| c != '_').collect();
360    cleaned.parse::<i64>().ok()
361}