Skip to main content

hk_parser/
lib.rs

1// src/lib.rs
2//! Hacker Lang Configuration Parser
3//!
4//! This crate provides a robust parser and serializer for .hk files used in Hacker Lang,
5//! the programming language for HackerOS. It supports nested structures, comments, and
6//! error handling.
7use nom::{
8    branch::alt,
9    bytes::complete::{tag, take_until, take_while, take_while1},
10    character::complete::{multispace0, multispace1},
11    combinator::{eof, map, opt, peek},
12    error::context,
13    multi::{many0, many1},
14    sequence::{delimited, preceded, terminated, tuple},
15    IResult,
16};
17use std::collections::HashMap;
18use std::fs::File;
19use std::io::{self, BufRead, BufReader, Write};
20use std::path::Path;
21use thiserror::Error;
22
23/// Represents the structure of a .hk file.
24/// Sections are top-level keys in the outer HashMap.
25/// Values can be simple strings or nested HashMaps for subsections.
26pub type HkConfig = HashMap<String, HkValue>;
27
28/// Enum for values in the .hk config: either a simple string or a nested map.
29#[derive(Debug, Clone, PartialEq)]
30pub enum HkValue {
31    String(String),
32    Map(HashMap<String, HkValue>),
33}
34
35/// Custom error type for parsing .hk files.
36#[derive(Error, Debug)]
37pub enum HkError {
38    #[error("IO error: {0}")]
39    Io(#[from] io::Error),
40    #[error("Parse error at line {line}: {message}")]
41    Parse { line: usize, message: String },
42}
43
44/// Parses a .hk file from a string input.
45pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
46    let mut remaining = input.as_bytes();
47    let mut config = HashMap::new();
48    while !remaining.is_empty() {
49        let (rest, _) = match multispace0::<&[u8], nom::error::Error<&[u8]>>(remaining) {
50            Ok(v) => v,
51            Err(e) => return Err(HkError::Parse {
52                line: 1,
53                message: format!("Parse error: {}", e),
54            }),
55        };
56        remaining = rest;
57        if remaining.is_empty() {
58            break;
59        }
60        if remaining.starts_with(b"!") {
61            // Skip comment line
62            let (rest, _) = match comment(remaining) {
63                Ok(v) => v,
64                Err(e) => return Err(HkError::Parse {
65                    line: 1,
66                    message: format!("Parse error: {}", e),
67                }),
68            };
69            remaining = rest;
70            continue;
71        }
72        match section(remaining) {
73            Ok((rest, (name, values))) => {
74                config.insert(name, HkValue::Map(values));
75                remaining = rest;
76            }
77            Err(e) => {
78                let remaining_input = match &e {
79                    nom::Err::Error(err) | nom::Err::Failure(err) => err.input,
80                    nom::Err::Incomplete(_) => input.as_bytes(),
81                };
82                let consumed_len = input.as_bytes().len() - remaining_input.len();
83                let consumed_bytes = &input.as_bytes()[0..consumed_len];
84                let line = consumed_bytes.iter().filter(|&&b| b == b'\n').count() + 1;
85                return Err(HkError::Parse {
86                    line,
87                    message: format!("Parse error: {}", e),
88                });
89            }
90        }
91    }
92    Ok(config)
93}
94
95/// Loads and parses a .hk file from the given path.
96pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
97    let file = File::open(path)?;
98    let reader = BufReader::new(file);
99    let mut contents = String::new();
100    for line in reader.lines() {
101        contents.push_str(&line?);
102        contents.push('\n');
103    }
104    parse_hk(&contents)
105}
106
107/// Serializes a HkConfig back to a .hk string.
108pub fn serialize_hk(config: &HkConfig) -> String {
109    let mut output = String::new();
110    for (section, value) in config {
111        output.push_str(&format!("[{}]\n", section));
112        if let HkValue::Map(map) = value {
113            serialize_map(map, 0, &mut output);
114        }
115        output.push('\n');
116    }
117    output.trim_end().to_string()
118}
119
120fn serialize_map(map: &HashMap<String, HkValue>, indent: usize, output: &mut String) {
121    for (key, value) in map {
122        match value {
123            HkValue::String(s) => {
124                output.push_str(&format!("{}-> {} => {}\n", " ".repeat(indent), key, s));
125            }
126            HkValue::Map(submap) => {
127                output.push_str(&format!("{}-> {}\n", " ".repeat(indent), key));
128                serialize_map(submap, indent + 1, output);
129            }
130        }
131    }
132}
133
134/// Writes a HkConfig to a file.
135pub fn write_hk_file<P: AsRef<Path>>(path: P, config: &HkConfig) -> io::Result<()> {
136    let mut file = File::create(path)?;
137    file.write_all(serialize_hk(config).as_bytes())
138}
139
140// Parser combinators
141fn comment<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8], nom::error::Error<&'a [u8]>> {
142    context(
143        "comment",
144        delimited(tag(b"!"), take_while(|c| c != b'\r' && c != b'\n'), opt(tag(b"\n"))),
145    )(input)
146}
147
148fn section<'a>(input: &'a [u8]) -> IResult<&'a [u8], (String, HashMap<String, HkValue>), nom::error::Error<&'a [u8]>> {
149    context(
150        "section",
151        map(
152            tuple((
153                delimited(tag(b"["), take_until(&b"]"[..]), tag(b"]")),
154                   multispace0,
155                   terminated(
156                       many0(alt((
157                           map(comment, |_| None),
158                                  map(key_value, Some),
159                                  map(nested_key_value, Some),
160                       ))),
161                       // Fix: consume potential whitespace before peeking for [ or EOF
162                       tuple((
163                           multispace0,
164                           peek(alt((tag(b"["), map(eof, |_| &[] as &[u8]))))
165                       )),
166                   ),
167            )),
168            |(name, _, opt_pairs)| {
169                let mut map = HashMap::new();
170                for pair_opt in opt_pairs {
171                    if let Some((key, value)) = pair_opt {
172                        insert_nested(&mut map, key.split('.').collect::<Vec<_>>(), value);
173                    }
174                }
175                (std::str::from_utf8(name).unwrap().trim().to_string(), map)
176            },
177        ),
178    )(input)
179}
180
181/// Inserts a value into a nested map using dot-separated keys.
182fn insert_nested(map: &mut HashMap<String, HkValue>, keys: Vec<&str>, value: HkValue) {
183    let mut current = map;
184    for key in &keys[0..keys.len().saturating_sub(1)] {
185        let entry = current.entry(key.to_string()).or_insert(HkValue::Map(HashMap::new()));
186        if let HkValue::Map(submap) = entry {
187            current = submap;
188        } else {
189            // Error if trying to nest under a string
190            panic!("Invalid nesting");
191        }
192    }
193    if let Some(last_key) = keys.last() {
194        current.insert((*last_key).to_string(), value);
195    }
196}
197
198fn key_value<'a>(input: &'a [u8]) -> IResult<&'a [u8], (String, HkValue), nom::error::Error<&'a [u8]>> {
199    context(
200        "key_value",
201        map(
202            tuple((
203                preceded(tuple((multispace0, tag(b"->"), multispace1)), take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_')),
204                   multispace0,
205                   tag(b"=>"),
206                   multispace0,
207                   terminated(take_while(|c| c != b'\r' && c != b'\n'), opt(tag(b"\n"))),
208            )),
209            |(key, _, _, _, value)| (
210                std::str::from_utf8(key).unwrap().trim().to_string(),
211                                     HkValue::String(std::str::from_utf8(value).unwrap().trim().to_string()),
212            ),
213        ),
214    )(input)
215}
216
217fn nested_key_value<'a>(input: &'a [u8]) -> IResult<&'a [u8], (String, HkValue), nom::error::Error<&'a [u8]>> {
218    context(
219        "nested_key_value",
220        map(
221            tuple((
222                preceded(tuple((multispace0, tag(b"->"), multispace1)), take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_')),
223                   many1(sub_key_value),
224            )),
225            |(key, sub_pairs)| {
226                let mut sub_map = HashMap::new();
227                for (sub_key, sub_value) in sub_pairs {
228                    sub_map.insert(sub_key, sub_value);
229                }
230                (std::str::from_utf8(key).unwrap().trim().to_string(), HkValue::Map(sub_map))
231            },
232        ),
233    )(input)
234}
235
236fn sub_key_value<'a>(input: &'a [u8]) -> IResult<&'a [u8], (String, HkValue), nom::error::Error<&'a [u8]>> {
237    context(
238        "sub_key_value",
239        map(
240            tuple((
241                preceded(tuple((multispace1, tag(b"-->"), multispace1)), take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_')),
242                   multispace0,
243                   tag(b"=>"),
244                   multispace0,
245                   terminated(take_while(|c| c != b'\r' && c != b'\n'), opt(tag(b"\n"))),
246            )),
247            |(sub_key, _, _, _, sub_value)| (
248                std::str::from_utf8(sub_key).unwrap().trim().to_string(),
249                                             HkValue::String(std::str::from_utf8(sub_value).unwrap().trim().to_string()),
250            ),
251        ),
252    )(input)
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use pretty_assertions::assert_eq;
259
260    #[test]
261    fn test_parse_hk_with_comments() {
262        let input = r#"
263        ! Globalne informacje o projekcie
264        [metadata]
265        -> name => Hacker Lang
266        -> version => 1.5
267        -> authors => HackerOS Team <hackeros068@gmail.com>
268        -> license => MIT
269        [description]
270        -> summary => Programing language for HackerOS.
271        -> long => Język programowania Hacker Lang z plikami konfiguracyjnymi .hk lub .hacker lub skryptami itd. .hl.
272        [specs]
273        -> rust => >= 1.92.0
274        -> dependencies
275        --> odin => >= 2026-01
276        --> c => C23
277        --> crystal => 1.19.0
278        --> python => 3.13
279        "#;
280        let result = parse_hk(input).unwrap();
281        assert_eq!(result.len(), 3);
282
283        if let Some(HkValue::Map(metadata)) = result.get("metadata") {
284            assert_eq!(metadata.len(), 4);
285            assert_eq!(metadata.get("name"), Some(&HkValue::String("Hacker Lang".to_string())));
286            assert_eq!(metadata.get("version"), Some(&HkValue::String("1.5".to_string())));
287            assert_eq!(metadata.get("authors"), Some(&HkValue::String("HackerOS Team <hackeros068@gmail.com>".to_string())));
288            assert_eq!(metadata.get("license"), Some(&HkValue::String("MIT".to_string())));
289        }
290
291        if let Some(HkValue::Map(description)) = result.get("description") {
292            assert_eq!(description.len(), 2);
293            assert_eq!(description.get("summary"), Some(&HkValue::String("Programing language for HackerOS.".to_string())));
294            assert_eq!(description.get("long"), Some(&HkValue::String("Język programowania Hacker Lang z plikami konfiguracyjnymi .hk lub .hacker lub skryptami itd. .hl.".to_string())));
295        }
296
297        if let Some(HkValue::Map(specs)) = result.get("specs") {
298            assert_eq!(specs.len(), 2);
299            assert_eq!(specs.get("rust"), Some(&HkValue::String(">= 1.92.0".to_string())));
300
301            if let Some(HkValue::Map(deps)) = specs.get("dependencies") {
302                assert_eq!(deps.len(), 4);
303                assert_eq!(deps.get("odin"), Some(&HkValue::String(">= 2026-01".to_string())));
304                assert_eq!(deps.get("c"), Some(&HkValue::String("C23".to_string())));
305                assert_eq!(deps.get("crystal"), Some(&HkValue::String("1.19.0".to_string())));
306                assert_eq!(deps.get("python"), Some(&HkValue::String("3.13".to_string())));
307            }
308        }
309    }
310
311    #[test]
312    fn test_serialize_hk() {
313        let mut config = HashMap::new();
314        let mut metadata = HashMap::new();
315        metadata.insert("name".to_string(), HkValue::String("Hacker Lang".to_string()));
316        metadata.insert("version".to_string(), HkValue::String("1.5".to_string()));
317        config.insert("metadata".to_string(), HkValue::Map(metadata));
318        let serialized = serialize_hk(&config);
319        assert!(serialized.contains("[metadata]"));
320        assert!(serialized.contains("-> name => Hacker Lang"));
321        assert!(serialized.contains("-> version => 1.5"));
322    }
323
324    #[test]
325    fn test_error_handling() {
326        let invalid_input = r#"
327        [metadata]
328        -> name = Hacker Lang # Missing =>
329        "#;
330        let err = parse_hk(invalid_input).unwrap_err();
331        match err {
332            HkError::Parse { line, message } => {
333                assert_eq!(line, 3);
334                assert!(message.contains("Parse error"));
335            }
336            _ => panic!("Unexpected error"),
337        }
338    }
339}