1use 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
23pub type HkConfig = HashMap<String, HkValue>;
27
28#[derive(Debug, Clone, PartialEq)]
30pub enum HkValue {
31 String(String),
32 Map(HashMap<String, HkValue>),
33}
34
35#[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
44pub 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 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
95pub 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
107pub 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
134pub 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
140fn 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 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
181fn 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 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}