1use crate::debug::trace;
45use crate::{CodecError, LinoValue};
46use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
47
48pub const DEFAULT_INDENT: &str = " ";
50
51pub const BASE64_MARKER: &str = "base64";
53
54pub fn encode(value: &LinoValue, indent: &str) -> String {
56 let mut out = String::new();
57 write_value(value, indent, 0, &mut out);
58 out
59}
60
61pub fn decode(text: &str) -> Result<LinoValue, CodecError> {
63 let tokens = tokenize(text)?;
64 trace("readable.decode", || format!("{} tokens", tokens.len()));
65 let mut cursor = Cursor { tokens, pos: 0 };
66 let rows = cursor.parse_rows(true)?;
67
68 if cursor.pos < cursor.tokens.len() {
69 return Err(CodecError::ParseError(
70 "unexpected ')' in readable notation".to_string(),
71 ));
72 }
73
74 if rows.len() == 1 && rows[0].len() == 1 {
76 return node_to_value(&rows[0][0]);
77 }
78
79 rows_to_value(&rows, true)
80}
81
82fn write_value(value: &LinoValue, indent: &str, level: usize, out: &mut String) {
85 match value {
86 LinoValue::Object(pairs) => {
87 if pairs.is_empty() {
88 out.push_str("(\n");
90 push_indent(indent, level, out);
91 out.push(')');
92 return;
93 }
94
95 out.push('(');
96 for (key, child) in pairs {
97 out.push('\n');
98 push_indent(indent, level + 1, out);
99 out.push_str(&format_key(key));
100 out.push(' ');
101 write_value(child, indent, level + 1, out);
102 }
103 out.push('\n');
104 push_indent(indent, level, out);
105 out.push(')');
106 }
107
108 LinoValue::Array(items) => {
109 if items.is_empty() {
110 out.push_str("()");
111 return;
112 }
113
114 out.push('(');
115 for item in items {
116 out.push('\n');
117 push_indent(indent, level + 1, out);
118 write_value(item, indent, level + 1, out);
119 }
120 out.push('\n');
121 push_indent(indent, level, out);
122 out.push(')');
123 }
124
125 scalar => out.push_str(&format_scalar(scalar)),
126 }
127}
128
129fn push_indent(indent: &str, level: usize, out: &mut String) {
130 for _ in 0..level {
131 out.push_str(indent);
132 }
133}
134
135fn format_scalar(value: &LinoValue) -> String {
138 match value {
139 LinoValue::Null => "null".to_string(),
140 LinoValue::Bool(b) => b.to_string(),
141 LinoValue::Int(i) => i.to_string(),
142 LinoValue::Float(f) => format_float(*f),
143 LinoValue::String(s) => format_string(s),
144 LinoValue::Array(_) | LinoValue::Object(_) => String::new(),
146 }
147}
148
149fn format_float(f: f64) -> String {
150 if f.is_nan() {
151 "NaN".to_string()
152 } else if f.is_infinite() {
153 if f.is_sign_positive() {
154 "Infinity".to_string()
155 } else {
156 "-Infinity".to_string()
157 }
158 } else {
159 format!("{:?}", f)
162 }
163}
164
165fn format_string(value: &str) -> String {
168 if needs_encoding(value) {
169 return format!(
170 "({} {})",
171 BASE64_MARKER,
172 quote(&BASE64.encode(value.as_bytes()))
173 );
174 }
175 quote(value)
176}
177
178fn needs_encoding(value: &str) -> bool {
181 value.chars().any(char::is_control)
182}
183
184fn quote(value: &str) -> String {
185 let has_double = value.contains('"');
186 let has_single = value.contains('\'');
187
188 if !has_double {
189 return format!("\"{}\"", value);
190 }
191 if !has_single {
192 return format!("'{}'", value);
193 }
194 format!("\"{}\"", value.replace('"', "\"\""))
196}
197
198fn format_key(key: &str) -> String {
200 let plain = !key.is_empty()
201 && key != BASE64_MARKER
202 && !needs_encoding(key)
203 && !key
204 .chars()
205 .any(|c| c.is_whitespace() || matches!(c, '(' | ')' | '\'' | '"' | ':' | '`'));
206
207 if plain {
208 key.to_string()
209 } else {
210 format_string(key)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
217enum Token {
218 Open,
219 Close,
220 Newline,
221 Ref { value: String, quoted: bool },
222}
223
224#[derive(Debug, Clone)]
227enum Node {
228 Ref {
229 value: String,
230 quoted: bool,
231 },
232 Link {
233 rows: Vec<Vec<Node>>,
234 multiline: bool,
235 },
236}
237
238fn tokenize(text: &str) -> Result<Vec<Token>, CodecError> {
239 let chars: Vec<char> = text.chars().collect();
240 let mut tokens = Vec::new();
241 let mut i = 0;
242
243 while i < chars.len() {
244 let c = chars[i];
245
246 if c == '\n' {
247 tokens.push(Token::Newline);
248 i += 1;
249 } else if c.is_whitespace() {
250 i += 1;
251 } else if c == '(' {
252 tokens.push(Token::Open);
253 i += 1;
254 } else if c == ')' {
255 tokens.push(Token::Close);
256 i += 1;
257 } else if matches!(c, '"' | '\'' | '`') {
258 let (value, next) = read_quoted(&chars, i, c)?;
259 tokens.push(Token::Ref {
260 value,
261 quoted: true,
262 });
263 i = next;
264 } else {
265 let start = i;
266 while i < chars.len()
267 && !chars[i].is_whitespace()
268 && !matches!(chars[i], '(' | ')' | '"' | '\'' | '`')
269 {
270 i += 1;
271 }
272 tokens.push(Token::Ref {
273 value: chars[start..i].iter().collect(),
274 quoted: false,
275 });
276 }
277 }
278
279 Ok(tokens)
280}
281
282fn read_quoted(
284 chars: &[char],
285 start: usize,
286 quote_char: char,
287) -> Result<(String, usize), CodecError> {
288 let mut value = String::new();
289 let mut i = start + 1;
290
291 while i < chars.len() {
292 if chars[i] == quote_char {
293 if chars.get(i + 1) == Some("e_char) {
294 value.push(quote_char);
295 i += 2;
296 continue;
297 }
298 return Ok((value, i + 1));
299 }
300 value.push(chars[i]);
301 i += 1;
302 }
303
304 Err(CodecError::ParseError(format!(
305 "unterminated quoted value starting at character {}",
306 start
307 )))
308}
309
310struct Cursor {
311 tokens: Vec<Token>,
312 pos: usize,
313}
314
315impl Cursor {
316 fn parse_rows(&mut self, top_level: bool) -> Result<Vec<Vec<Node>>, CodecError> {
319 let mut rows: Vec<Vec<Node>> = Vec::new();
320 let mut row: Vec<Node> = Vec::new();
321
322 while self.pos < self.tokens.len() {
323 match &self.tokens[self.pos] {
324 Token::Close => {
325 if top_level {
326 break;
327 }
328 self.pos += 1;
329 if !row.is_empty() {
330 rows.push(row);
331 }
332 return Ok(rows);
333 }
334 Token::Newline => {
335 self.pos += 1;
336 if !row.is_empty() {
337 rows.push(std::mem::take(&mut row));
338 }
339 }
340 _ => row.push(self.parse_node()?),
341 }
342 }
343
344 if !top_level {
345 return Err(CodecError::ParseError(
346 "unterminated '(' in readable notation".to_string(),
347 ));
348 }
349
350 if !row.is_empty() {
351 rows.push(row);
352 }
353 Ok(rows)
354 }
355
356 fn parse_node(&mut self) -> Result<Node, CodecError> {
357 match self.tokens[self.pos].clone() {
358 Token::Ref { value, quoted } => {
359 self.pos += 1;
360 Ok(Node::Ref { value, quoted })
361 }
362 Token::Open => {
363 self.pos += 1;
364 let multiline = self.link_is_multiline();
365 let rows = self.parse_rows(false)?;
366 Ok(Node::Link { rows, multiline })
367 }
368 Token::Close | Token::Newline => Err(CodecError::ParseError(
369 "unexpected token in readable notation".to_string(),
370 )),
371 }
372 }
373
374 fn link_is_multiline(&self) -> bool {
377 self.tokens[self.pos..]
378 .iter()
379 .take_while(|t| **t != Token::Close)
380 .any(|t| *t == Token::Newline)
381 }
382}
383
384fn node_to_value(node: &Node) -> Result<LinoValue, CodecError> {
385 match node {
386 Node::Ref { value, quoted } => Ok(ref_to_value(value, *quoted)),
387 Node::Link { rows, multiline } => rows_to_value(rows, *multiline),
388 }
389}
390
391fn rows_to_value(rows: &[Vec<Node>], multiline: bool) -> Result<LinoValue, CodecError> {
392 if rows.is_empty() {
393 return Ok(if multiline {
394 LinoValue::Object(vec![])
395 } else {
396 LinoValue::Array(vec![])
397 });
398 }
399
400 if let Some(marked) = decode_marked_value(rows) {
401 return marked;
402 }
403
404 let is_object = rows
406 .iter()
407 .all(|row| row.len() == 2 && matches!(row[0], Node::Ref { .. }));
408
409 if is_object {
410 let mut pairs = Vec::with_capacity(rows.len());
411 for row in rows {
412 let Node::Ref { value: key, .. } = &row[0] else {
413 unreachable!("checked by is_object")
414 };
415 pairs.push((key.clone(), node_to_value(&row[1])?));
416 }
417 return Ok(LinoValue::Object(pairs));
418 }
419
420 let mut items = Vec::new();
421 for row in rows {
422 for node in row {
423 items.push(node_to_value(node)?);
424 }
425 }
426 Ok(LinoValue::Array(items))
427}
428
429fn decode_marked_value(rows: &[Vec<Node>]) -> Option<Result<LinoValue, CodecError>> {
432 if rows.len() != 1 || rows[0].len() != 2 {
433 return None;
434 }
435
436 let Node::Ref {
437 value: marker,
438 quoted: false,
439 } = &rows[0][0]
440 else {
441 return None;
442 };
443 if marker != BASE64_MARKER {
444 return None;
445 }
446
447 let Node::Ref {
448 value: payload,
449 quoted: true,
450 } = &rows[0][1]
451 else {
452 return None;
453 };
454
455 Some(
456 BASE64
457 .decode(payload)
458 .map_err(|e| CodecError::DecodeError(format!("invalid base64 value: {}", e)))
459 .and_then(|bytes| {
460 String::from_utf8(bytes)
461 .map(LinoValue::String)
462 .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 value: {}", e)))
463 }),
464 )
465}
466
467fn ref_to_value(value: &str, quoted: bool) -> LinoValue {
470 if quoted {
471 return LinoValue::String(value.to_string());
472 }
473
474 match value {
475 "null" => return LinoValue::Null,
476 "true" => return LinoValue::Bool(true),
477 "false" => return LinoValue::Bool(false),
478 "NaN" => return LinoValue::Float(f64::NAN),
479 "Infinity" => return LinoValue::Float(f64::INFINITY),
480 "-Infinity" => return LinoValue::Float(f64::NEG_INFINITY),
481 _ => {}
482 }
483
484 if let Ok(i) = value.parse::<i64>() {
485 return LinoValue::Int(i);
486 }
487 if value.contains(['.', 'e', 'E']) {
488 if let Ok(f) = value.parse::<f64>() {
489 return LinoValue::Float(f);
490 }
491 }
492
493 LinoValue::String(value.to_string())
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 fn roundtrip(value: &LinoValue) -> LinoValue {
501 let text = encode(value, DEFAULT_INDENT);
502 decode(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
503 }
504
505 #[test]
506 fn empty_containers_keep_their_type() {
507 assert_eq!(encode(&LinoValue::Array(vec![]), DEFAULT_INDENT), "()");
508 assert_eq!(encode(&LinoValue::Object(vec![]), DEFAULT_INDENT), "(\n)");
509 assert_eq!(
510 roundtrip(&LinoValue::Array(vec![])),
511 LinoValue::Array(vec![])
512 );
513 assert_eq!(
514 roundtrip(&LinoValue::Object(vec![])),
515 LinoValue::Object(vec![])
516 );
517 }
518
519 #[test]
520 fn single_pair_object_is_not_an_array() {
521 let value = LinoValue::object([("a", LinoValue::Int(1))]);
522 assert_eq!(encode(&value, DEFAULT_INDENT), "(\n a 1\n)");
523 assert_eq!(roundtrip(&value), value);
524 }
525
526 #[test]
527 fn numeric_looking_strings_stay_strings() {
528 let value = LinoValue::object([
529 ("count", LinoValue::Int(42)),
530 ("zip", LinoValue::String("10001".to_string())),
531 ("flag", LinoValue::String("true".to_string())),
532 ]);
533 assert_eq!(roundtrip(&value), value);
534 }
535
536 #[test]
537 fn whole_floats_stay_floats() {
538 let value = LinoValue::Float(1.0);
539 assert_eq!(encode(&value, DEFAULT_INDENT), "1.0");
540 assert!(matches!(roundtrip(&value), LinoValue::Float(f) if (f - 1.0).abs() < f64::EPSILON));
541 }
542
543 #[test]
544 fn base64_key_is_quoted_so_it_is_not_a_marker() {
545 let value = LinoValue::object([("base64", LinoValue::String("plain".to_string()))]);
546 assert_eq!(
547 encode(&value, DEFAULT_INDENT),
548 "(\n \"base64\" \"plain\"\n)"
549 );
550 assert_eq!(roundtrip(&value), value);
551 }
552
553 #[test]
554 fn control_characters_are_marked_individually() {
555 let value = LinoValue::object([
556 ("plain", LinoValue::String("visible".to_string())),
557 ("raw", LinoValue::String("line1\nline2".to_string())),
558 ]);
559 let text = encode(&value, DEFAULT_INDENT);
560 assert!(text.contains("plain \"visible\""), "{}", text);
561 assert!(
562 text.contains("raw (base64 \"bGluZTEKbGluZTI=\")"),
563 "{}",
564 text
565 );
566 assert_eq!(roundtrip(&value), value);
567 }
568
569 #[test]
570 fn custom_indent_is_used() {
571 let value = LinoValue::object([("a", LinoValue::Int(1))]);
572 assert_eq!(encode(&value, " "), "(\n a 1\n)");
573 }
574
575 #[test]
576 fn handwritten_document_without_root_parentheses_decodes() {
577 let decoded = decode("a 1\nb \"two\"").unwrap();
578 assert_eq!(
579 decoded,
580 LinoValue::object([
581 ("a", LinoValue::Int(1)),
582 ("b", LinoValue::String("two".into()))
583 ])
584 );
585 }
586
587 #[test]
588 fn unterminated_input_is_an_error() {
589 assert!(decode("(\n a 1\n").is_err());
590 assert!(decode("(\n a \"unterminated\n").is_err());
591 assert!(decode("a 1)").is_err());
592 }
593}