1use crate::debug::trace;
73use crate::{CodecError, LinoValue};
74use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
75
76pub const DEFAULT_INDENT: &str = " ";
78
79pub const BASE64_MARKER: &str = "base64";
81
82pub const OBJECT_MARKER: &str = "o";
84
85pub fn encode(value: &LinoValue, indent: &str) -> String {
87 let mut out = String::new();
88 write_value(value, indent, 0, &mut out);
89 out
90}
91
92pub fn encode_line(value: &LinoValue) -> String {
97 let mut out = String::new();
98 write_line_value(value, &mut out);
99 out
100}
101
102pub fn decode_line(text: &str) -> Result<LinoValue, CodecError> {
108 let line = text.trim_matches(|c: char| c == '\n' || c == '\r');
109 if line.contains('\n') || line.contains('\r') {
110 return Err(CodecError::ParseError(
111 "a single-line document cannot contain a line break".to_string(),
112 ));
113 }
114 decode(line)
115}
116
117pub fn decode(text: &str) -> Result<LinoValue, CodecError> {
119 let tokens = tokenize(text)?;
120 trace("readable.decode", || format!("{} tokens", tokens.len()));
121 let mut cursor = Cursor { tokens, pos: 0 };
122 let rows = cursor.parse_rows(true)?;
123
124 if cursor.pos < cursor.tokens.len() {
125 return Err(CodecError::ParseError(
126 "unexpected ')' in readable notation".to_string(),
127 ));
128 }
129
130 if rows.len() == 1 && rows[0].len() == 1 {
132 return node_to_value(&rows[0][0]);
133 }
134
135 rows_to_value(&rows, true, false)
136}
137
138fn write_value(value: &LinoValue, indent: &str, level: usize, out: &mut String) {
141 match value {
142 LinoValue::Object(pairs) => {
143 if pairs.is_empty() {
144 out.push_str("(\n");
146 push_indent(indent, level, out);
147 out.push(')');
148 return;
149 }
150
151 out.push('(');
152 for (key, child) in pairs {
153 out.push('\n');
154 push_indent(indent, level + 1, out);
155 out.push_str(&format_key(key));
156 out.push(' ');
157 write_value(child, indent, level + 1, out);
158 }
159 out.push('\n');
160 push_indent(indent, level, out);
161 out.push(')');
162 }
163
164 LinoValue::Array(items) => {
165 if items.is_empty() {
166 out.push_str("()");
167 return;
168 }
169
170 out.push('(');
171 for item in items {
172 out.push('\n');
173 push_indent(indent, level + 1, out);
174 write_value(item, indent, level + 1, out);
175 }
176 out.push('\n');
177 push_indent(indent, level, out);
178 out.push(')');
179 }
180
181 scalar => out.push_str(&format_scalar(scalar)),
182 }
183}
184
185fn write_line_value(value: &LinoValue, out: &mut String) {
188 match value {
189 LinoValue::Object(pairs) => {
190 if pairs.is_empty() {
191 out.push('(');
193 out.push_str(OBJECT_MARKER);
194 out.push_str(":)");
195 return;
196 }
197
198 out.push('(');
199 out.push_str(OBJECT_MARKER);
200 out.push(':');
201 for (key, child) in pairs {
202 out.push_str(" (");
203 out.push_str(&format_key(key));
204 out.push(' ');
205 write_line_value(child, out);
206 out.push(')');
207 }
208 out.push(')');
209 }
210
211 LinoValue::Array(items) => {
212 out.push('(');
213 for (index, item) in items.iter().enumerate() {
214 if index > 0 {
215 out.push(' ');
216 }
217 write_line_value(item, out);
218 }
219 out.push(')');
220 }
221
222 scalar => out.push_str(&format_scalar(scalar)),
223 }
224}
225
226fn push_indent(indent: &str, level: usize, out: &mut String) {
227 for _ in 0..level {
228 out.push_str(indent);
229 }
230}
231
232fn format_scalar(value: &LinoValue) -> String {
235 match value {
236 LinoValue::Null => "null".to_string(),
237 LinoValue::Bool(b) => b.to_string(),
238 LinoValue::Int(i) => i.to_string(),
239 LinoValue::Float(f) => format_float(*f),
240 LinoValue::String(s) => format_string(s),
241 LinoValue::Array(_) | LinoValue::Object(_) => String::new(),
243 }
244}
245
246fn format_float(f: f64) -> String {
247 if f.is_nan() {
248 "NaN".to_string()
249 } else if f.is_infinite() {
250 if f.is_sign_positive() {
251 "Infinity".to_string()
252 } else {
253 "-Infinity".to_string()
254 }
255 } else {
256 format!("{:?}", f)
259 }
260}
261
262fn format_string(value: &str) -> String {
265 if needs_encoding(value) {
266 return format!(
267 "({} {})",
268 BASE64_MARKER,
269 quote(&BASE64.encode(value.as_bytes()))
270 );
271 }
272 quote(value)
273}
274
275fn needs_encoding(value: &str) -> bool {
278 value.chars().any(char::is_control)
279}
280
281fn quote(value: &str) -> String {
282 let has_double = value.contains('"');
283 let has_single = value.contains('\'');
284
285 if !has_double {
286 return format!("\"{}\"", value);
287 }
288 if !has_single {
289 return format!("'{}'", value);
290 }
291 format!("\"{}\"", value.replace('"', "\"\""))
293}
294
295fn format_key(key: &str) -> String {
297 let plain = !key.is_empty()
298 && key != BASE64_MARKER
299 && !needs_encoding(key)
300 && !key
301 .chars()
302 .any(|c| c.is_whitespace() || matches!(c, '(' | ')' | '\'' | '"' | ':' | '`'));
303
304 if plain {
305 key.to_string()
306 } else {
307 format_string(key)
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
314enum Token {
315 Open,
316 Close,
317 Newline,
318 Ref { value: String, quoted: bool },
319}
320
321#[derive(Debug, Clone)]
324enum Node {
325 Ref {
326 value: String,
327 quoted: bool,
328 },
329 Link {
330 rows: Vec<Vec<Node>>,
331 multiline: bool,
332 object: bool,
334 },
335}
336
337fn tokenize(text: &str) -> Result<Vec<Token>, CodecError> {
338 let chars: Vec<char> = text.chars().collect();
339 let mut tokens = Vec::new();
340 let mut i = 0;
341
342 while i < chars.len() {
343 let c = chars[i];
344
345 if c == '\n' {
346 tokens.push(Token::Newline);
347 i += 1;
348 } else if c.is_whitespace() {
349 i += 1;
350 } else if c == '(' {
351 tokens.push(Token::Open);
352 i += 1;
353 } else if c == ')' {
354 tokens.push(Token::Close);
355 i += 1;
356 } else if matches!(c, '"' | '\'' | '`') {
357 let (value, next) = read_quoted(&chars, i, c)?;
358 tokens.push(Token::Ref {
359 value,
360 quoted: true,
361 });
362 i = next;
363 } else {
364 let start = i;
365 while i < chars.len()
366 && !chars[i].is_whitespace()
367 && !matches!(chars[i], '(' | ')' | '"' | '\'' | '`')
368 {
369 i += 1;
370 }
371 tokens.push(Token::Ref {
372 value: chars[start..i].iter().collect(),
373 quoted: false,
374 });
375 }
376 }
377
378 Ok(tokens)
379}
380
381fn read_quoted(
383 chars: &[char],
384 start: usize,
385 quote_char: char,
386) -> Result<(String, usize), CodecError> {
387 let mut value = String::new();
388 let mut i = start + 1;
389
390 while i < chars.len() {
391 if chars[i] == quote_char {
392 if chars.get(i + 1) == Some("e_char) {
393 value.push(quote_char);
394 i += 2;
395 continue;
396 }
397 return Ok((value, i + 1));
398 }
399 value.push(chars[i]);
400 i += 1;
401 }
402
403 Err(CodecError::ParseError(format!(
404 "unterminated quoted value starting at character {}",
405 start
406 )))
407}
408
409struct Cursor {
410 tokens: Vec<Token>,
411 pos: usize,
412}
413
414impl Cursor {
415 fn parse_rows(&mut self, top_level: bool) -> Result<Vec<Vec<Node>>, CodecError> {
418 let mut rows: Vec<Vec<Node>> = Vec::new();
419 let mut row: Vec<Node> = Vec::new();
420
421 while self.pos < self.tokens.len() {
422 match &self.tokens[self.pos] {
423 Token::Close => {
424 if top_level {
425 break;
426 }
427 self.pos += 1;
428 if !row.is_empty() {
429 rows.push(row);
430 }
431 return Ok(rows);
432 }
433 Token::Newline => {
434 self.pos += 1;
435 if !row.is_empty() {
436 rows.push(std::mem::take(&mut row));
437 }
438 }
439 _ => row.push(self.parse_node()?),
440 }
441 }
442
443 if !top_level {
444 return Err(CodecError::ParseError(
445 "unterminated '(' in readable notation".to_string(),
446 ));
447 }
448
449 if !row.is_empty() {
450 rows.push(row);
451 }
452 Ok(rows)
453 }
454
455 fn parse_node(&mut self) -> Result<Node, CodecError> {
456 match self.tokens[self.pos].clone() {
457 Token::Ref { value, quoted } => {
458 self.pos += 1;
459 Ok(Node::Ref { value, quoted })
460 }
461 Token::Open => {
462 self.pos += 1;
463 let object = self.take_object_marker();
464 let multiline = self.link_is_multiline();
465 let rows = self.parse_rows(false)?;
466 Ok(Node::Link {
467 rows,
468 multiline,
469 object,
470 })
471 }
472 Token::Close | Token::Newline => Err(CodecError::ParseError(
473 "unexpected token in readable notation".to_string(),
474 )),
475 }
476 }
477
478 fn take_object_marker(&mut self) -> bool {
481 let marker = format!("{}:", OBJECT_MARKER);
482 let is_marker = matches!(
483 self.tokens.get(self.pos),
484 Some(Token::Ref { value, quoted: false }) if *value == marker
485 );
486 if is_marker {
487 self.pos += 1;
488 }
489 is_marker
490 }
491
492 fn link_is_multiline(&self) -> bool {
495 self.tokens[self.pos..]
496 .iter()
497 .take_while(|t| **t != Token::Close)
498 .any(|t| *t == Token::Newline)
499 }
500}
501
502fn node_to_value(node: &Node) -> Result<LinoValue, CodecError> {
503 match node {
504 Node::Ref { value, quoted } => Ok(ref_to_value(value, *quoted)),
505 Node::Link {
506 rows,
507 multiline,
508 object,
509 } => rows_to_value(rows, *multiline, *object),
510 }
511}
512
513fn rows_to_value(
514 rows: &[Vec<Node>],
515 multiline: bool,
516 object_marker: bool,
517) -> Result<LinoValue, CodecError> {
518 if object_marker {
519 return marked_object_to_value(rows);
520 }
521
522 if rows.is_empty() {
523 return Ok(if multiline {
524 LinoValue::Object(vec![])
525 } else {
526 LinoValue::Array(vec![])
527 });
528 }
529
530 if let Some(marked) = decode_marked_value(rows) {
531 return marked;
532 }
533
534 if !multiline {
537 let mut items = Vec::new();
538 for row in rows {
539 for node in row {
540 items.push(node_to_value(node)?);
541 }
542 }
543 return Ok(LinoValue::Array(items));
544 }
545
546 let is_object = rows
548 .iter()
549 .all(|row| row.len() == 2 && matches!(row[0], Node::Ref { .. }));
550
551 if is_object {
552 let mut pairs = Vec::with_capacity(rows.len());
553 for row in rows {
554 let Node::Ref { value: key, .. } = &row[0] else {
555 unreachable!("checked by is_object")
556 };
557 pairs.push((key.clone(), node_to_value(&row[1])?));
558 }
559 return Ok(LinoValue::Object(pairs));
560 }
561
562 let mut items = Vec::new();
563 for row in rows {
564 for node in row {
565 items.push(node_to_value(node)?);
566 }
567 }
568 Ok(LinoValue::Array(items))
569}
570
571fn marked_object_to_value(rows: &[Vec<Node>]) -> Result<LinoValue, CodecError> {
574 let mut pairs = Vec::new();
575
576 for node in rows.iter().flatten() {
577 let Node::Link {
578 rows: pair,
579 object: false,
580 ..
581 } = node
582 else {
583 return Err(CodecError::ParseError(format!(
584 "an object marked '{}:' holds (key value) pairs, found a value that is not a pair",
585 OBJECT_MARKER
586 )));
587 };
588
589 let [row] = pair.as_slice() else {
590 return Err(CodecError::ParseError(format!(
591 "an object marked '{}:' holds (key value) pairs, found a link of {} lines",
592 OBJECT_MARKER,
593 pair.len()
594 )));
595 };
596
597 let [Node::Ref { value: key, .. }, value] = row.as_slice() else {
598 return Err(CodecError::ParseError(format!(
599 "an object marked '{}:' holds (key value) pairs, found a link of {} values",
600 OBJECT_MARKER,
601 row.len()
602 )));
603 };
604
605 pairs.push((key.clone(), node_to_value(value)?));
606 }
607
608 Ok(LinoValue::Object(pairs))
609}
610
611fn decode_marked_value(rows: &[Vec<Node>]) -> Option<Result<LinoValue, CodecError>> {
614 if rows.len() != 1 || rows[0].len() != 2 {
615 return None;
616 }
617
618 let Node::Ref {
619 value: marker,
620 quoted: false,
621 } = &rows[0][0]
622 else {
623 return None;
624 };
625 if marker != BASE64_MARKER {
626 return None;
627 }
628
629 let Node::Ref {
630 value: payload,
631 quoted: true,
632 } = &rows[0][1]
633 else {
634 return None;
635 };
636
637 Some(
638 BASE64
639 .decode(payload)
640 .map_err(|e| CodecError::DecodeError(format!("invalid base64 value: {}", e)))
641 .and_then(|bytes| {
642 String::from_utf8(bytes)
643 .map(LinoValue::String)
644 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 value: {}", e)))
645 }),
646 )
647}
648
649fn ref_to_value(value: &str, quoted: bool) -> LinoValue {
652 if quoted {
653 return LinoValue::String(value.to_string());
654 }
655
656 match value {
657 "null" => return LinoValue::Null,
658 "true" => return LinoValue::Bool(true),
659 "false" => return LinoValue::Bool(false),
660 "NaN" => return LinoValue::Float(f64::NAN),
661 "Infinity" => return LinoValue::Float(f64::INFINITY),
662 "-Infinity" => return LinoValue::Float(f64::NEG_INFINITY),
663 _ => {}
664 }
665
666 if let Ok(i) = value.parse::<i64>() {
667 return LinoValue::Int(i);
668 }
669 if value.contains(['.', 'e', 'E']) {
670 if let Ok(f) = value.parse::<f64>() {
671 return LinoValue::Float(f);
672 }
673 }
674
675 LinoValue::String(value.to_string())
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 fn roundtrip(value: &LinoValue) -> LinoValue {
683 let text = encode(value, DEFAULT_INDENT);
684 decode(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
685 }
686
687 #[test]
688 fn empty_containers_keep_their_type() {
689 assert_eq!(encode(&LinoValue::Array(vec![]), DEFAULT_INDENT), "()");
690 assert_eq!(encode(&LinoValue::Object(vec![]), DEFAULT_INDENT), "(\n)");
691 assert_eq!(
692 roundtrip(&LinoValue::Array(vec![])),
693 LinoValue::Array(vec![])
694 );
695 assert_eq!(
696 roundtrip(&LinoValue::Object(vec![])),
697 LinoValue::Object(vec![])
698 );
699 }
700
701 #[test]
702 fn single_pair_object_is_not_an_array() {
703 let value = LinoValue::object([("a", LinoValue::Int(1))]);
704 assert_eq!(encode(&value, DEFAULT_INDENT), "(\n a 1\n)");
705 assert_eq!(roundtrip(&value), value);
706 }
707
708 #[test]
709 fn numeric_looking_strings_stay_strings() {
710 let value = LinoValue::object([
711 ("count", LinoValue::Int(42)),
712 ("zip", LinoValue::String("10001".to_string())),
713 ("flag", LinoValue::String("true".to_string())),
714 ]);
715 assert_eq!(roundtrip(&value), value);
716 }
717
718 #[test]
719 fn whole_floats_stay_floats() {
720 let value = LinoValue::Float(1.0);
721 assert_eq!(encode(&value, DEFAULT_INDENT), "1.0");
722 assert!(matches!(roundtrip(&value), LinoValue::Float(f) if (f - 1.0).abs() < f64::EPSILON));
723 }
724
725 #[test]
726 fn base64_key_is_quoted_so_it_is_not_a_marker() {
727 let value = LinoValue::object([("base64", LinoValue::String("plain".to_string()))]);
728 assert_eq!(
729 encode(&value, DEFAULT_INDENT),
730 "(\n \"base64\" \"plain\"\n)"
731 );
732 assert_eq!(roundtrip(&value), value);
733 }
734
735 #[test]
736 fn control_characters_are_marked_individually() {
737 let value = LinoValue::object([
738 ("plain", LinoValue::String("visible".to_string())),
739 ("raw", LinoValue::String("line1\nline2".to_string())),
740 ]);
741 let text = encode(&value, DEFAULT_INDENT);
742 assert!(text.contains("plain \"visible\""), "{}", text);
743 assert!(
744 text.contains("raw (base64 \"bGluZTEKbGluZTI=\")"),
745 "{}",
746 text
747 );
748 assert_eq!(roundtrip(&value), value);
749 }
750
751 #[test]
752 fn custom_indent_is_used() {
753 let value = LinoValue::object([("a", LinoValue::Int(1))]);
754 assert_eq!(encode(&value, " "), "(\n a 1\n)");
755 }
756
757 #[test]
758 fn handwritten_document_without_root_parentheses_decodes() {
759 let decoded = decode("a 1\nb \"two\"").unwrap();
760 assert_eq!(
761 decoded,
762 LinoValue::object([
763 ("a", LinoValue::Int(1)),
764 ("b", LinoValue::String("two".into()))
765 ])
766 );
767 }
768
769 fn line_roundtrip(value: &LinoValue) -> LinoValue {
770 let text = encode_line(value);
771 assert!(
772 !text.contains('\n'),
773 "line form must hold no newline: {:?}",
774 text
775 );
776 decode_line(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
777 }
778
779 #[test]
780 fn line_form_marks_objects_and_leaves_arrays_bare() {
781 assert_eq!(encode_line(&LinoValue::Array(vec![])), "()");
782 assert_eq!(encode_line(&LinoValue::Object(vec![])), "(o:)");
783 assert_eq!(
784 encode_line(&LinoValue::object([("a", LinoValue::Int(1))])),
785 "(o: (a 1))"
786 );
787 assert_eq!(
788 encode_line(&LinoValue::array([
789 LinoValue::String("key".into()),
790 LinoValue::String("value".into()),
791 ])),
792 "(\"key\" \"value\")"
793 );
794 }
795
796 #[test]
797 fn line_form_tells_a_one_pair_object_from_a_two_element_array() {
798 let object = LinoValue::object([("key", LinoValue::String("value".into()))]);
799 let array = LinoValue::array([
800 LinoValue::String("key".into()),
801 LinoValue::String("value".into()),
802 ]);
803 assert_ne!(encode_line(&object), encode_line(&array));
804 assert_eq!(line_roundtrip(&object), object);
805 assert_eq!(line_roundtrip(&array), array);
806 }
807
808 #[test]
809 fn line_form_roundtrips_nested_and_empty_containers() {
810 let value = LinoValue::object([
811 ("empty_array", LinoValue::Array(vec![])),
812 ("empty_object", LinoValue::Object(vec![])),
813 (
814 "records",
815 LinoValue::array([
816 LinoValue::object([("id", LinoValue::Int(1))]),
817 LinoValue::object([("id", LinoValue::Int(2))]),
818 ]),
819 ),
820 ]);
821 assert_eq!(
822 encode_line(&value),
823 "(o: (empty_array ()) (empty_object (o:)) (records ((o: (id 1)) (o: (id 2)))))"
824 );
825 assert_eq!(line_roundtrip(&value), value);
826 }
827
828 #[test]
829 fn line_form_keeps_the_empty_key() {
830 let value = LinoValue::object([("", LinoValue::Int(2))]);
831 assert_eq!(encode_line(&value), "(o: (\"\" 2))");
832 assert_eq!(line_roundtrip(&value), value);
833 }
834
835 #[test]
836 fn line_form_keeps_strings_on_one_line() {
837 let value = LinoValue::object([
838 (
839 "quotes",
840 LinoValue::String("both \"kinds\" of 'quotes'".into()),
841 ),
842 ("unicode", LinoValue::String("héllo 世界 🌍".into())),
843 ("multiline", LinoValue::String("line1\nline2".into())),
844 ]);
845 let text = encode_line(&value);
846 assert!(!text.contains('\n'), "{}", text);
847 assert_eq!(line_roundtrip(&value), value);
848 }
849
850 #[test]
851 fn a_line_form_object_holds_pairs_only() {
852 assert!(decode_line("(o: 1 2)").is_err());
853 assert!(decode_line("(o: (a 1 2))").is_err());
854 assert!(decode_line("(o: (a))").is_err());
855 }
856
857 #[test]
858 fn decode_line_rejects_a_document_of_several_lines() {
859 assert!(decode_line("(o: (a 1))\n(o: (a 2))").is_err());
860 assert_eq!(
863 decode_line("(o: (a 1))\n").unwrap(),
864 LinoValue::object([("a", LinoValue::Int(1))])
865 );
866 }
867
868 #[test]
869 fn unterminated_input_is_an_error() {
870 assert!(decode("(\n a 1\n").is_err());
871 assert!(decode("(\n a \"unterminated\n").is_err());
872 assert!(decode("a 1)").is_err());
873 }
874}