1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatReader, FormatWriter};
6use crate::value::Value;
7
8pub struct PropertiesReader;
19
20impl PropertiesReader {
21 fn unescape(s: &str) -> String {
23 let mut result = String::with_capacity(s.len());
24 let mut chars = s.chars();
25 while let Some(c) = chars.next() {
26 if c == '\\' {
27 match chars.next() {
28 Some('n') => result.push('\n'),
29 Some('t') => result.push('\t'),
30 Some('r') => result.push('\r'),
31 Some('\\') => result.push('\\'),
32 Some('=') => result.push('='),
33 Some(':') => result.push(':'),
34 Some(' ') => result.push(' '),
35 Some('#') => result.push('#'),
36 Some('!') => result.push('!'),
37 Some('u') => {
38 let hex: String = chars.by_ref().take(4).collect();
40 if hex.len() == 4 {
41 if let Ok(code) = u32::from_str_radix(&hex, 16) {
42 if let Some(ch) = char::from_u32(code) {
43 result.push(ch);
44 continue;
45 }
46 }
47 }
48 result.push_str("\\u");
50 result.push_str(&hex);
51 }
52 Some(other) => {
53 result.push(other);
55 }
56 None => {
57 }
59 }
60 } else {
61 result.push(c);
62 }
63 }
64 result
65 }
66
67 fn find_separator(line: &str) -> Option<(usize, usize)> {
71 let bytes = line.as_bytes();
72
73 let mut escaped = false;
75 for (i, &b) in bytes.iter().enumerate() {
76 if escaped {
77 escaped = false;
78 continue;
79 }
80 if b == b'\\' {
81 escaped = true;
82 continue;
83 }
84 if b == b'=' || b == b':' {
85 let value_start = line[i + 1..]
87 .find(|c: char| c != ' ' && c != '\t')
88 .map_or(line.len(), |pos| i + 1 + pos);
89 return Some((i, value_start));
90 }
91 }
92
93 escaped = false;
95 for (i, &b) in bytes.iter().enumerate() {
96 if escaped {
97 escaped = false;
98 continue;
99 }
100 if b == b'\\' {
101 escaped = true;
102 continue;
103 }
104 if b == b' ' || b == b'\t' {
105 let value_start = line[i..]
106 .find(|c: char| c != ' ' && c != '\t')
107 .map_or(line.len(), |pos| i + pos);
108 return Some((i, value_start));
109 }
110 }
111
112 None
113 }
114
115 fn join_logical_lines(input: &str) -> Vec<String> {
117 let mut logical_lines = Vec::new();
118 let mut current = String::new();
119 let mut continuation = false;
120
121 for line in input.lines() {
122 if continuation {
123 current.push_str(line.trim_start());
125 } else {
126 if !current.is_empty() {
127 logical_lines.push(std::mem::take(&mut current));
128 }
129 current = line.to_string();
130 }
131
132 let trailing_backslashes = current.bytes().rev().take_while(|&b| b == b'\\').count();
134 if trailing_backslashes % 2 == 1 {
135 current.truncate(current.len() - 1);
137 continuation = true;
138 } else {
139 continuation = false;
140 }
141 }
142
143 if !current.is_empty() {
144 logical_lines.push(current);
145 }
146
147 logical_lines
148 }
149}
150
151impl FormatReader for PropertiesReader {
152 fn read(&self, input: &str) -> anyhow::Result<Value> {
153 let mut map = IndexMap::new();
154 let logical_lines = Self::join_logical_lines(input);
155
156 for (line_num, line) in logical_lines.iter().enumerate() {
157 let trimmed = line.trim_start();
158
159 if trimmed.is_empty() {
161 continue;
162 }
163
164 if trimmed.starts_with('#') || trimmed.starts_with('!') {
166 continue;
167 }
168
169 match Self::find_separator(trimmed) {
171 Some((key_end, value_start)) => {
172 let raw_key = &trimmed[..key_end];
173 let raw_value = if value_start <= trimmed.len() {
174 &trimmed[value_start..]
175 } else {
176 ""
177 };
178 let key = Self::unescape(raw_key.trim_end());
179 let value = Self::unescape(raw_value);
180
181 if key.is_empty() {
182 return Err(crate::error::DkitError::ParseErrorAt {
183 format: "Properties".to_string(),
184 source: "empty key".to_string().into(),
185 line: line_num + 1,
186 column: 1,
187 line_text: line.to_string(),
188 }
189 .into());
190 }
191
192 map.insert(key, Value::String(value));
193 }
194 None => {
195 let key = Self::unescape(trimmed);
197 if !key.is_empty() {
198 map.insert(key, Value::String(String::new()));
199 }
200 }
201 }
202 }
203
204 Ok(Value::Object(map))
205 }
206
207 fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
208 let mut input = String::new();
209 reader
210 .read_to_string(&mut input)
211 .map_err(|e| crate::error::DkitError::ParseError {
212 format: "Properties".to_string(),
213 source: Box::new(e),
214 })?;
215 self.read(&input)
216 }
217}
218
219pub struct PropertiesWriter;
223
224impl PropertiesWriter {
225 fn escape_key(key: &str) -> String {
227 let mut result = String::with_capacity(key.len());
228 for (i, c) in key.chars().enumerate() {
229 match c {
230 ' ' => {
231 if i == 0 {
232 result.push_str("\\ ");
233 } else {
234 result.push(' ');
235 }
236 }
237 '=' => result.push_str("\\="),
238 ':' => result.push_str("\\:"),
239 '\\' => result.push_str("\\\\"),
240 '#' => result.push_str("\\#"),
241 '!' => result.push_str("\\!"),
242 '\t' => result.push_str("\\t"),
243 '\n' => result.push_str("\\n"),
244 '\r' => result.push_str("\\r"),
245 c if !(' '..='~').contains(&c) => {
246 for unit in c.encode_utf16(&mut [0u16; 2]) {
248 result.push_str(&format!("\\u{:04X}", unit));
249 }
250 }
251 _ => result.push(c),
252 }
253 }
254 result
255 }
256
257 fn escape_value(value: &str) -> String {
259 let mut result = String::with_capacity(value.len());
260 for (i, c) in value.chars().enumerate() {
261 match c {
262 ' ' if i == 0 => result.push_str("\\ "),
263 '\\' => result.push_str("\\\\"),
264 '\t' => result.push_str("\\t"),
265 '\n' => result.push_str("\\n"),
266 '\r' => result.push_str("\\r"),
267 c if !(' '..='~').contains(&c) => {
268 for unit in c.encode_utf16(&mut [0u16; 2]) {
269 result.push_str(&format!("\\u{:04X}", unit));
270 }
271 }
272 _ => result.push(c),
273 }
274 }
275 result
276 }
277
278 fn value_to_string(value: &Value) -> String {
280 match value {
281 Value::String(s) => s.clone(),
282 Value::Null => String::new(),
283 Value::Bool(b) => b.to_string(),
284 Value::Integer(n) => n.to_string(),
285 Value::Float(f) => f.to_string(),
286 Value::Array(_) | Value::Object(_) => serde_json::to_string(value).unwrap_or_default(),
287 }
288 }
289}
290
291impl FormatWriter for PropertiesWriter {
292 fn write(&self, value: &Value) -> anyhow::Result<String> {
293 match value {
294 Value::Object(map) => {
295 let mut output = String::new();
296 for (key, val) in map {
297 let escaped_key = Self::escape_key(key);
298 let raw_value = Self::value_to_string(val);
299 let escaped_value = Self::escape_value(&raw_value);
300 output.push_str(&format!("{}={}\n", escaped_key, escaped_value));
301 }
302 Ok(output)
303 }
304 _ => {
305 anyhow::bail!(
306 "Properties format requires an Object value (flat key-value pairs). Got: {}",
307 match value {
308 Value::Null => "null",
309 Value::Bool(_) => "boolean",
310 Value::Integer(_) => "integer",
311 Value::Float(_) => "float",
312 Value::String(_) => "string",
313 Value::Array(_) => "array",
314 _ => "unknown",
315 }
316 );
317 }
318 }
319 }
320
321 fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
322 let output = self.write(value)?;
323 writer
324 .write_all(output.as_bytes())
325 .map_err(|e| crate::error::DkitError::WriteError {
326 format: "Properties".to_string(),
327 source: Box::new(e),
328 })?;
329 Ok(())
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
340 fn test_reader_simple_key_value() {
341 let reader = PropertiesReader;
342 let input = "name=Alice\nage=30\n";
343 let v = reader.read(input).unwrap();
344 let obj = v.as_object().unwrap();
345 assert_eq!(obj.get("name"), Some(&Value::String("Alice".to_string())));
346 assert_eq!(obj.get("age"), Some(&Value::String("30".to_string())));
347 }
348
349 #[test]
350 fn test_reader_colon_separator() {
351 let reader = PropertiesReader;
352 let input = "name: Alice\nage: 30\n";
353 let v = reader.read(input).unwrap();
354 let obj = v.as_object().unwrap();
355 assert_eq!(obj.get("name"), Some(&Value::String("Alice".to_string())));
356 assert_eq!(obj.get("age"), Some(&Value::String("30".to_string())));
357 }
358
359 #[test]
360 fn test_reader_space_separator() {
361 let reader = PropertiesReader;
362 let input = "name Alice\nage 30\n";
363 let v = reader.read(input).unwrap();
364 let obj = v.as_object().unwrap();
365 assert_eq!(obj.get("name"), Some(&Value::String("Alice".to_string())));
366 assert_eq!(obj.get("age"), Some(&Value::String("30".to_string())));
367 }
368
369 #[test]
370 fn test_reader_comments_hash() {
371 let reader = PropertiesReader;
372 let input = "# This is a comment\nkey=value\n";
373 let v = reader.read(input).unwrap();
374 let obj = v.as_object().unwrap();
375 assert_eq!(obj.len(), 1);
376 assert_eq!(obj.get("key"), Some(&Value::String("value".to_string())));
377 }
378
379 #[test]
380 fn test_reader_comments_exclamation() {
381 let reader = PropertiesReader;
382 let input = "! This is a comment\nkey=value\n";
383 let v = reader.read(input).unwrap();
384 let obj = v.as_object().unwrap();
385 assert_eq!(obj.len(), 1);
386 }
387
388 #[test]
389 fn test_reader_empty_lines() {
390 let reader = PropertiesReader;
391 let input = "key1=val1\n\n\nkey2=val2\n";
392 let v = reader.read(input).unwrap();
393 let obj = v.as_object().unwrap();
394 assert_eq!(obj.len(), 2);
395 }
396
397 #[test]
398 fn test_reader_empty_value() {
399 let reader = PropertiesReader;
400 let input = "key=\n";
401 let v = reader.read(input).unwrap();
402 let obj = v.as_object().unwrap();
403 assert_eq!(obj.get("key"), Some(&Value::String(String::new())));
404 }
405
406 #[test]
407 fn test_reader_key_without_separator() {
408 let reader = PropertiesReader;
409 let input = "lonely_key\n";
411 let v = reader.read(input).unwrap();
412 let obj = v.as_object().unwrap();
413 assert_eq!(obj.get("lonely_key"), Some(&Value::String(String::new())));
414 }
415
416 #[test]
417 fn test_reader_multiline_value() {
418 let reader = PropertiesReader;
419 let input = "message=Hello \\\n World\n";
420 let v = reader.read(input).unwrap();
421 let obj = v.as_object().unwrap();
422 assert_eq!(
423 obj.get("message"),
424 Some(&Value::String("Hello World".to_string()))
425 );
426 }
427
428 #[test]
429 fn test_reader_multiline_multiple_continuations() {
430 let reader = PropertiesReader;
431 let input = "long=line1 \\\nline2 \\\nline3\n";
432 let v = reader.read(input).unwrap();
433 let obj = v.as_object().unwrap();
434 assert_eq!(
435 obj.get("long"),
436 Some(&Value::String("line1 line2 line3".to_string()))
437 );
438 }
439
440 #[test]
441 fn test_reader_unicode_escape() {
442 let reader = PropertiesReader;
443 let input = "greeting=\\u0048\\u0065\\u006C\\u006C\\u006F\n";
444 let v = reader.read(input).unwrap();
445 let obj = v.as_object().unwrap();
446 assert_eq!(
447 obj.get("greeting"),
448 Some(&Value::String("Hello".to_string()))
449 );
450 }
451
452 #[test]
453 fn test_reader_escaped_separator_in_key() {
454 let reader = PropertiesReader;
455 let input = "key\\=with\\=equals=value\n";
456 let v = reader.read(input).unwrap();
457 let obj = v.as_object().unwrap();
458 assert_eq!(
459 obj.get("key=with=equals"),
460 Some(&Value::String("value".to_string()))
461 );
462 }
463
464 #[test]
465 fn test_reader_escaped_special_chars() {
466 let reader = PropertiesReader;
467 let input = "path=C\\:\\\\Users\\\\test\n";
468 let v = reader.read(input).unwrap();
469 let obj = v.as_object().unwrap();
470 assert_eq!(
471 obj.get("path"),
472 Some(&Value::String("C:\\Users\\test".to_string()))
473 );
474 }
475
476 #[test]
477 fn test_reader_dotted_keys() {
478 let reader = PropertiesReader;
479 let input = "app.db.host=localhost\napp.db.port=5432\n";
480 let v = reader.read(input).unwrap();
481 let obj = v.as_object().unwrap();
482 assert_eq!(
483 obj.get("app.db.host"),
484 Some(&Value::String("localhost".to_string()))
485 );
486 assert_eq!(
487 obj.get("app.db.port"),
488 Some(&Value::String("5432".to_string()))
489 );
490 }
491
492 #[test]
493 fn test_reader_whitespace_around_separator() {
494 let reader = PropertiesReader;
495 let input = "key = value\n";
496 let v = reader.read(input).unwrap();
497 let obj = v.as_object().unwrap();
498 assert_eq!(obj.get("key"), Some(&Value::String("value".to_string())));
499 }
500
501 #[test]
502 fn test_reader_duplicate_keys_last_wins() {
503 let reader = PropertiesReader;
504 let input = "key=first\nkey=second\n";
505 let v = reader.read(input).unwrap();
506 let obj = v.as_object().unwrap();
507 assert_eq!(obj.get("key"), Some(&Value::String("second".to_string())));
508 }
509
510 #[test]
511 fn test_reader_empty_input() {
512 let reader = PropertiesReader;
513 let v = reader.read("").unwrap();
514 assert!(v.as_object().unwrap().is_empty());
515 }
516
517 #[test]
518 fn test_reader_only_comments() {
519 let reader = PropertiesReader;
520 let input = "# comment 1\n! comment 2\n";
521 let v = reader.read(input).unwrap();
522 assert!(v.as_object().unwrap().is_empty());
523 }
524
525 #[test]
526 fn test_reader_from_reader() {
527 let reader = PropertiesReader;
528 let input = b"key=value" as &[u8];
529 let v = reader.read_from_reader(input).unwrap();
530 let obj = v.as_object().unwrap();
531 assert_eq!(obj.get("key"), Some(&Value::String("value".to_string())));
532 }
533
534 #[test]
535 fn test_reader_newline_escape_in_value() {
536 let reader = PropertiesReader;
537 let input = "msg=line1\\nline2\n";
538 let v = reader.read(input).unwrap();
539 let obj = v.as_object().unwrap();
540 assert_eq!(
541 obj.get("msg"),
542 Some(&Value::String("line1\nline2".to_string()))
543 );
544 }
545
546 #[test]
547 fn test_reader_tab_escape_in_value() {
548 let reader = PropertiesReader;
549 let input = "data=col1\\tcol2\n";
550 let v = reader.read(input).unwrap();
551 let obj = v.as_object().unwrap();
552 assert_eq!(
553 obj.get("data"),
554 Some(&Value::String("col1\tcol2".to_string()))
555 );
556 }
557
558 #[test]
559 fn test_reader_realistic_i18n() {
560 let reader = PropertiesReader;
561 let input = r#"# Application messages
562app.title=My Application
563app.greeting=Welcome, {0}!
564app.error.notfound=Page not found
565app.error.server=Internal server error
566"#;
567 let v = reader.read(input).unwrap();
568 let obj = v.as_object().unwrap();
569 assert_eq!(obj.len(), 4);
570 assert_eq!(
571 obj.get("app.title"),
572 Some(&Value::String("My Application".to_string()))
573 );
574 assert_eq!(
575 obj.get("app.greeting"),
576 Some(&Value::String("Welcome, {0}!".to_string()))
577 );
578 }
579
580 #[test]
583 fn test_writer_simple() {
584 let writer = PropertiesWriter;
585 let v = Value::Object({
586 let mut m = IndexMap::new();
587 m.insert("name".to_string(), Value::String("Alice".to_string()));
588 m.insert("age".to_string(), Value::String("30".to_string()));
589 m
590 });
591 let output = writer.write(&v).unwrap();
592 assert!(output.contains("name=Alice\n"));
593 assert!(output.contains("age=30\n"));
594 }
595
596 #[test]
597 fn test_writer_special_chars_in_key() {
598 let writer = PropertiesWriter;
599 let v = Value::Object({
600 let mut m = IndexMap::new();
601 m.insert("key=with".to_string(), Value::String("value".to_string()));
602 m
603 });
604 let output = writer.write(&v).unwrap();
605 assert!(output.contains("key\\=with=value\n"));
606 }
607
608 #[test]
609 fn test_writer_non_ascii() {
610 let writer = PropertiesWriter;
611 let v = Value::Object({
612 let mut m = IndexMap::new();
613 m.insert(
614 "greeting".to_string(),
615 Value::String("こんにちは".to_string()),
616 );
617 m
618 });
619 let output = writer.write(&v).unwrap();
620 assert!(output.contains("greeting="));
621 assert!(output.contains("\\u"));
622 }
623
624 #[test]
625 fn test_writer_newline_in_value() {
626 let writer = PropertiesWriter;
627 let v = Value::Object({
628 let mut m = IndexMap::new();
629 m.insert("msg".to_string(), Value::String("line1\nline2".to_string()));
630 m
631 });
632 let output = writer.write(&v).unwrap();
633 assert!(output.contains("msg=line1\\nline2\n"));
634 }
635
636 #[test]
637 fn test_writer_non_string_values() {
638 let writer = PropertiesWriter;
639 let v = Value::Object({
640 let mut m = IndexMap::new();
641 m.insert("count".to_string(), Value::Integer(42));
642 m.insert("rate".to_string(), Value::Float(3.14));
643 m.insert("enabled".to_string(), Value::Bool(true));
644 m.insert("empty".to_string(), Value::Null);
645 m
646 });
647 let output = writer.write(&v).unwrap();
648 assert!(output.contains("count=42\n"));
649 assert!(output.contains("rate=3.14\n"));
650 assert!(output.contains("enabled=true\n"));
651 assert!(output.contains("empty=\n"));
652 }
653
654 #[test]
655 fn test_writer_non_object_error() {
656 let writer = PropertiesWriter;
657 let result = writer.write(&Value::String("hello".to_string()));
658 assert!(result.is_err());
659 }
660
661 #[test]
662 fn test_writer_to_writer() {
663 let writer = PropertiesWriter;
664 let v = Value::Object({
665 let mut m = IndexMap::new();
666 m.insert("key".to_string(), Value::String("value".to_string()));
667 m
668 });
669 let mut buf = Vec::new();
670 writer.write_to_writer(&v, &mut buf).unwrap();
671 let output = String::from_utf8(buf).unwrap();
672 assert_eq!(output, "key=value\n");
673 }
674
675 #[test]
676 fn test_writer_leading_space_in_value() {
677 let writer = PropertiesWriter;
678 let v = Value::Object({
679 let mut m = IndexMap::new();
680 m.insert("key".to_string(), Value::String(" leading".to_string()));
681 m
682 });
683 let output = writer.write(&v).unwrap();
684 assert!(output.contains("key=\\ leading\n"));
685 }
686
687 #[test]
690 fn test_roundtrip_simple() {
691 let input = "name=Alice\nage=30\n";
692 let reader = PropertiesReader;
693 let writer = PropertiesWriter;
694
695 let value = reader.read(input).unwrap();
696 let output = writer.write(&value).unwrap();
697 let value2 = reader.read(&output).unwrap();
698
699 assert_eq!(value, value2);
700 }
701
702 #[test]
703 fn test_roundtrip_dotted_keys() {
704 let input = "app.db.host=localhost\napp.db.port=5432\n";
705 let reader = PropertiesReader;
706 let writer = PropertiesWriter;
707
708 let value = reader.read(input).unwrap();
709 let output = writer.write(&value).unwrap();
710 let value2 = reader.read(&output).unwrap();
711
712 assert_eq!(value, value2);
713 }
714
715 #[test]
716 fn test_roundtrip_special_chars() {
717 let reader = PropertiesReader;
718 let writer = PropertiesWriter;
719
720 let v = Value::Object({
721 let mut m = IndexMap::new();
722 m.insert("key=eq".to_string(), Value::String("val:colon".to_string()));
723 m.insert("path".to_string(), Value::String("C:\\Users".to_string()));
724 m
725 });
726
727 let output = writer.write(&v).unwrap();
728 let value2 = reader.read(&output).unwrap();
729 assert_eq!(v, value2);
730 }
731
732 #[test]
733 fn test_roundtrip_unicode() {
734 let reader = PropertiesReader;
735 let writer = PropertiesWriter;
736
737 let v = Value::Object({
738 let mut m = IndexMap::new();
739 m.insert("msg".to_string(), Value::String("Hello 세계".to_string()));
740 m
741 });
742
743 let output = writer.write(&v).unwrap();
744 let value2 = reader.read(&output).unwrap();
745 assert_eq!(v, value2);
746 }
747
748 #[test]
749 fn test_roundtrip_newlines() {
750 let reader = PropertiesReader;
751 let writer = PropertiesWriter;
752
753 let v = Value::Object({
754 let mut m = IndexMap::new();
755 m.insert(
756 "multiline".to_string(),
757 Value::String("line1\nline2\nline3".to_string()),
758 );
759 m
760 });
761
762 let output = writer.write(&v).unwrap();
763 let value2 = reader.read(&output).unwrap();
764 assert_eq!(v, value2);
765 }
766}