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