Skip to main content

lino_objects_codec/
readable.rs

1//! Readable, indented Links Notation representation.
2//!
3//! This module implements the default output of [`crate::encode`]: a plain-text,
4//! indented projection where keys and values are written as they are, so the file
5//! can be read, grepped and reviewed without decoding anything.
6//!
7//! # Shape
8//!
9//! One construct — `( )` — is used for both objects and arrays, at every level
10//! including the root. What distinguishes them is the content of the lines:
11//! `key value` pairs make an object, bare values make an array.
12//!
13//! ```text
14//! (
15//!   type "RouterState"
16//!   server (
17//!     host "127.0.0.1"
18//!     port 18878
19//!   )
20//!   models (
21//!     "claude-haiku"
22//!     "claude-opus"
23//!   )
24//! )
25//! ```
26//!
27//! # Value mapping
28//!
29//! | `LinoValue`                | Readable form                                  |
30//! |----------------------------|------------------------------------------------|
31//! | `Object`                   | `( )` with one `key value` pair per line        |
32//! | `Array`                    | `( )` with one value per line                   |
33//! | `String`                   | quoted, never encoded                           |
34//! | `Int` / `Float` / `Bool` / `Null` | bare, so the type survives the round trip |
35//!
36//! Empty containers keep their type: an empty array is `()` on one line, while an
37//! empty object is written as `(` and `)` on two lines.
38//!
39//! Text is written as text. A string keeps every character a reader would grep
40//! for, including newlines and tabs, and is quoted with a run of delimiters —
41//! `"""say "hi""""` — when it holds the delimiter itself. Only the characters a
42//! form cannot carry are escaped, and only they: the value is then written as
43//! `(escaped "…")`, where `%XX` stands for one escaped byte. The indented form
44//! escapes the carriage return, which CRLF normalisation would otherwise rewrite,
45//! and the other control characters; the single-line form escapes the newline as
46//! well, because there a record ends at the end of the line. Nothing else is
47//! encoded: base64 lives in [`crate::encode_compact`], which a caller asks for by
48//! name.
49//!
50//! # Single-line form
51//!
52//! [`encode_line`] writes the same document on one line, so one record is one
53//! line and an append-only log stays greppable, tailable and countable by
54//! `wc -l`. Rows can no longer be told apart by line breaks there, so an object
55//! names itself with the `o` link id the notation already has, and its pairs are
56//! written as their own links:
57//!
58//! ```text
59//! (o: (type "RouterState") (server (o: (host "127.0.0.1") (port 18878))) (models ("claude-haiku" "claude-opus")))
60//! ```
61//!
62//! | Value            | Single-line form              |
63//! |------------------|-------------------------------|
64//! | `Object`         | `(o: (key value) …)`          |
65//! | empty `Object`   | `(o:)`                        |
66//! | `Array`          | `(value …)`                   |
67//! | empty `Array`    | `()`                          |
68//! | scalars          | exactly as in the indented form |
69//!
70//! The marker is what answers the ambiguity a flat layout otherwise has: without
71//! it `((key value))` reads both as the one-pair object and as the array holding
72//! the two-element array, and an empty key makes it worse. With it, a bare `( )`
73//! is always an array and a marked one is always an object, so every value —
74//! empty key included — survives the round trip. Consequently a *hand-written*
75//! one-line link such as `(a 1)` is the two-element array, not the one-pair
76//! object: on one line, objects say so.
77
78use crate::debug::trace;
79use crate::{CodecError, LinoValue};
80use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
81
82/// Default indentation used by [`encode`].
83pub const DEFAULT_INDENT: &str = "  ";
84
85/// Marker of a base64 payload. Written by [`crate::encode_compact`] and by
86/// versions up to 0.6.0 of the readable form, which is why it is still read.
87pub const BASE64_MARKER: &str = "base64";
88
89/// Marker of a string whose unwritable characters are percent-escaped.
90///
91/// It reads as `(escaped "line one%0Aline two")`. Only those characters change;
92/// the rest of the text is written as it is, so the value stays readable and
93/// greppable.
94pub const ESCAPED_MARKER: &str = "escaped";
95
96/// Link id naming an object in the single-line form, written as `(o: …)`.
97pub const OBJECT_MARKER: &str = "o";
98
99/// Encode a value into the readable, indented Links Notation form.
100pub fn encode(value: &LinoValue, indent: &str) -> String {
101    let mut out = String::new();
102    write_value(value, indent, 0, &mut out);
103    out
104}
105
106/// Encode a value into the readable, single-line Links Notation form.
107///
108/// The result never contains a newline, so one value is one line of an
109/// append-only log. See the module documentation for the shape.
110pub fn encode_line(value: &LinoValue) -> String {
111    let mut out = String::new();
112    write_line_value(value, &mut out);
113    out
114}
115
116/// Decode the readable, single-line Links Notation form back into a value.
117///
118/// This is the exact inverse of [`encode_line`]. Input spanning more than one
119/// line is rejected: a line-based reader hands over one record at a time, and
120/// silently accepting several would merge two records into one value.
121pub fn decode_line(text: &str) -> Result<LinoValue, CodecError> {
122    let line = text.trim_matches(|c: char| c == '\n' || c == '\r');
123    if line.contains('\n') || line.contains('\r') {
124        return Err(CodecError::ParseError(
125            "a single-line document cannot contain a line break".to_string(),
126        ));
127    }
128    decode(line)
129}
130
131/// Decode the readable, indented Links Notation form back into a value.
132pub fn decode(text: &str) -> Result<LinoValue, CodecError> {
133    let tokens = tokenize(text)?;
134    trace("readable.decode", || format!("{} tokens", tokens.len()));
135    let mut cursor = Cursor { tokens, pos: 0 };
136    let rows = cursor.parse_rows(true)?;
137
138    if cursor.pos < cursor.tokens.len() {
139        return Err(CodecError::ParseError(
140            "unexpected ')' in readable notation".to_string(),
141        ));
142    }
143
144    // A document holding a single value (for example `42`) is that value.
145    if rows.len() == 1 && rows[0].len() == 1 {
146        return node_to_value(&rows[0][0]);
147    }
148
149    rows_to_value(&rows, true, false)
150}
151
152/// Quote and escape a value for one physical line. Decode with [`decode_line`].
153pub fn format_value_single_line(value: &str) -> String {
154    format_string(value, Form::Line)
155}
156
157/// Quote a value retaining literal newlines and tabs. Characters that text-file
158/// normalization could change are escaped; decode with [`decode`].
159pub fn format_value_verbatim(value: &str) -> String {
160    format_string(value, Form::Indented)
161}
162
163// === Encoding ===
164
165/// The line structure of the text being written, which is what decides whether a
166/// newline can be written literally: in the indented form a value ends at its
167/// closing quote, so it may span lines; in the single-line form a record ends at
168/// the end of the line, so a newline inside a value would end the record.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170enum Form {
171    Indented,
172    Line,
173}
174
175fn write_value(value: &LinoValue, indent: &str, level: usize, out: &mut String) {
176    match value {
177        LinoValue::Object(pairs) => {
178            if pairs.is_empty() {
179                // An empty object spans two lines; `()` on one line is an empty array.
180                out.push_str("(\n");
181                push_indent(indent, level, out);
182                out.push(')');
183                return;
184            }
185
186            out.push('(');
187            for (key, child) in pairs {
188                out.push('\n');
189                push_indent(indent, level + 1, out);
190                out.push_str(&format_key(key, Form::Indented));
191                out.push(' ');
192                write_value(child, indent, level + 1, out);
193            }
194            out.push('\n');
195            push_indent(indent, level, out);
196            out.push(')');
197        }
198
199        LinoValue::Array(items) => {
200            if items.is_empty() {
201                out.push_str("()");
202                return;
203            }
204
205            out.push('(');
206            for item in items {
207                out.push('\n');
208                push_indent(indent, level + 1, out);
209                write_value(item, indent, level + 1, out);
210            }
211            out.push('\n');
212            push_indent(indent, level, out);
213            out.push(')');
214        }
215
216        scalar => out.push_str(&format_scalar(scalar, Form::Indented)),
217    }
218}
219
220/// Write a value on one line. Objects name themselves with the `o` link id and
221/// write each pair as its own link, so nothing depends on where lines break.
222fn write_line_value(value: &LinoValue, out: &mut String) {
223    match value {
224        LinoValue::Object(pairs) => {
225            if pairs.is_empty() {
226                // `()` is the empty array, so the empty object keeps its marker.
227                out.push('(');
228                out.push_str(OBJECT_MARKER);
229                out.push_str(":)");
230                return;
231            }
232
233            out.push('(');
234            out.push_str(OBJECT_MARKER);
235            out.push(':');
236            for (key, child) in pairs {
237                out.push_str(" (");
238                out.push_str(&format_key(key, Form::Line));
239                out.push(' ');
240                write_line_value(child, out);
241                out.push(')');
242            }
243            out.push(')');
244        }
245
246        LinoValue::Array(items) => {
247            out.push('(');
248            // `(null)` is a compact null; this array needs an explicit marker.
249            if matches!(items.as_slice(), [LinoValue::Null]) {
250                out.push_str("a: ");
251            }
252            for (index, item) in items.iter().enumerate() {
253                if index > 0 {
254                    out.push(' ');
255                }
256                write_line_value(item, out);
257            }
258            out.push(')');
259        }
260
261        scalar => out.push_str(&format_scalar(scalar, Form::Line)),
262    }
263}
264
265fn push_indent(indent: &str, level: usize, out: &mut String) {
266    for _ in 0..level {
267        out.push_str(indent);
268    }
269}
270
271/// Format a scalar value. Strings are quoted, everything else stays bare so that
272/// its type is recoverable when reading the document back.
273fn format_scalar(value: &LinoValue, form: Form) -> String {
274    match value {
275        LinoValue::Null => "null".to_string(),
276        LinoValue::Bool(b) => b.to_string(),
277        LinoValue::Int(i) => i.to_string(),
278        LinoValue::Float(f) => format_float(*f),
279        #[cfg(feature = "serde_json")]
280        LinoValue::JsonNumber(n) => format!("(json-number {})", quote(&n.to_string())),
281        LinoValue::String(s) => format_string(s, form),
282        // Containers are handled by write_value.
283        LinoValue::Array(_) | LinoValue::Object(_) => String::new(),
284    }
285}
286
287fn format_float(f: f64) -> String {
288    if f.is_nan() {
289        "NaN".to_string()
290    } else if f.is_infinite() {
291        if f.is_sign_positive() {
292            "Infinity".to_string()
293        } else {
294            "-Infinity".to_string()
295        }
296    } else {
297        // `{:?}` keeps the decimal point for whole floats (`1.0`), which is what
298        // tells a float apart from an integer when reading the document back.
299        format!("{:?}", f)
300    }
301}
302
303/// Format a string value. The text is written as text; when it holds characters
304/// this form cannot carry, those characters — and only those — are percent-escaped
305/// and the value is marked, so the rest of it stays readable and greppable.
306fn format_string(value: &str, form: Form) -> String {
307    match escape_unwritable(value, form) {
308        Some(escaped) => format!("({} {})", ESCAPED_MARKER, quote(&escaped)),
309        None => quote(value),
310    }
311}
312
313/// Percent-escape the characters this form cannot carry, or `None` when the text
314/// can be written as it is. `%` is escaped too, so escaping is reversible.
315fn escape_unwritable(value: &str, form: Form) -> Option<String> {
316    if !value.chars().any(|c| is_unwritable(c, form)) {
317        return None;
318    }
319
320    let mut out = String::with_capacity(value.len());
321    let mut buffer = [0u8; 4];
322    for c in value.chars() {
323        if c == '%' || is_unwritable(c, form) {
324            for byte in c.encode_utf8(&mut buffer).as_bytes() {
325                out.push('%');
326                out.push(hex_digit(byte >> 4));
327                out.push(hex_digit(byte & 0xf));
328            }
329        } else {
330            out.push(c);
331        }
332    }
333    Some(out)
334}
335
336/// One upper-case hexadecimal digit of a percent escape.
337fn hex_digit(value: u8) -> char {
338    char::from_digit(u32::from(value), 16).map_or('0', |digit| digit.to_ascii_uppercase())
339}
340
341/// Whether a character has to be escaped in this form. A tab is text a reader can
342/// see, and so is a newline in the indented form, where a value may span lines.
343/// A carriage return is escaped because CRLF normalisation rewrites it, and the
344/// remaining control characters because they are not text at all.
345fn is_unwritable(c: char, form: Form) -> bool {
346    if !c.is_control() {
347        return false;
348    }
349    match c {
350        '\t' => false,
351        '\n' => form == Form::Line,
352        _ => true,
353    }
354}
355
356/// Quote a value so that both this reader and the notation's own parser read it
357/// back unchanged.
358///
359/// One delimiter is enough while the text holds none of that
360/// kind; when it holds both kinds, a run of at least three opens the notation's
361/// n-quote form, where the text is literal and only a run at least as long closes
362/// it. A value starting with the delimiter would lengthen the opening run, so the
363/// other delimiter is used for it.
364pub fn quote(value: &str) -> String {
365    if !value.contains('"') {
366        return format!("\"{}\"", value);
367    }
368    if !value.contains('\'') {
369        return format!("'{}'", value);
370    }
371
372    let delimiter = if value.starts_with('"') { '\'' } else { '"' };
373    // A run of two delimiters is the empty value, so the n-quote form starts at
374    // three; beyond that the run only has to outrun the longest one inside.
375    let count = (longest_run(value, delimiter) + 1).max(3);
376    let run: String = std::iter::repeat_n(delimiter, count).collect();
377    format!("{run}{value}{run}")
378}
379
380/// The length of the longest run of `c` in `value`.
381fn longest_run(value: &str, c: char) -> usize {
382    let mut longest = 0;
383    let mut current = 0;
384    for candidate in value.chars() {
385        if candidate == c {
386            current += 1;
387            longest = longest.max(current);
388        } else {
389            current = 0;
390        }
391    }
392    longest
393}
394
395/// Format an object key. Keys are bare when they read as plain identifiers.
396fn format_key(key: &str, form: Form) -> String {
397    let plain = !key.is_empty()
398        && key != BASE64_MARKER
399        && key != ESCAPED_MARKER
400        && key != "json-number"
401        && !key.chars().any(|c| {
402            c.is_whitespace() || c.is_control() || matches!(c, '(' | ')' | '\'' | '"' | ':' | '`')
403        });
404
405    if plain {
406        key.to_string()
407    } else {
408        format_string(key, form)
409    }
410}
411
412// === Decoding ===
413
414#[derive(Debug, Clone, PartialEq, Eq)]
415enum Token {
416    Open,
417    Close,
418    Newline,
419    Ref { value: String, quoted: bool },
420}
421
422/// A parsed element of the readable form: either a reference (remembering whether
423/// it was quoted, which is what distinguishes a string from a number) or a link.
424#[derive(Debug, Clone)]
425enum Node {
426    Ref {
427        value: String,
428        quoted: bool,
429    },
430    Link {
431        rows: Vec<Vec<Node>>,
432        multiline: bool,
433        /// Whether the link named itself an object with the `o:` marker.
434        object: bool,
435    },
436}
437
438fn tokenize(text: &str) -> Result<Vec<Token>, CodecError> {
439    let mut tokens = Vec::new();
440    let mut i = 0;
441
442    // Keep byte offsets into the original UTF-8 text. Advance by whole code
443    // points for whitespace/references; ASCII quote boundaries are safe slices.
444    while i < text.len() {
445        let c = text[i..].chars().next().expect("in bounds");
446
447        if c == '\n' {
448            tokens.push(Token::Newline);
449            i += 1;
450        } else if c.is_whitespace() {
451            i += c.len_utf8();
452        } else if c == '(' {
453            tokens.push(Token::Open);
454            i += 1;
455        } else if c == ')' {
456            tokens.push(Token::Close);
457            i += 1;
458        } else if matches!(c, '"' | '\'' | '`') {
459            let (value, next) = read_quoted(text, i, c)?;
460            tokens.push(Token::Ref {
461                value,
462                quoted: true,
463            });
464            i = next;
465        } else {
466            let start = i;
467            for c in text[start..].chars() {
468                if c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\'' | '`') {
469                    break;
470                }
471                i += c.len_utf8();
472            }
473            tokens.push(Token::Ref {
474                value: text[start..i].to_owned(),
475                quoted: false,
476            });
477        }
478    }
479
480    Ok(tokens)
481}
482
483/// Read a quoted reference. The opening run of delimiters says how it is read,
484/// which is what the notation's own parser does:
485///
486/// * one delimiter — the text is literal and a doubled delimiter is one literal
487///   delimiter, which is how versions up to 0.6.0 wrote such values;
488/// * two — the empty value;
489/// * three or more — the n-quote form: the text is literal, and the value ends at
490///   the first run at least as long, whose last delimiters close it. A longer run
491///   therefore belongs to the text, so a value may end with a delimiter.
492fn read_quoted(text: &str, start: usize, quote_char: char) -> Result<(String, usize), CodecError> {
493    let quote_byte = text.as_bytes()[start];
494    let opening = run_length(text, start, quote_byte);
495
496    if opening == 2 {
497        return Ok((String::new(), start + 2));
498    }
499
500    if opening == 1 {
501        let mut i = start + 1;
502
503        while let Some(next) = text[i..].find(quote_char) {
504            let next = i + next;
505            if text.as_bytes().get(next + 1) == Some(&quote_byte) {
506                i = next + 2;
507                continue;
508            }
509            let doubled = format!("{quote_char}{quote_char}");
510            return Ok((
511                text[start + 1..next].replace(&doubled, &quote_char.to_string()),
512                next + 1,
513            ));
514        }
515
516        return Err(unterminated_quote(text[..start].chars().count()));
517    }
518
519    let mut i = start + opening;
520    while let Some(next) = text[i..].find(quote_char) {
521        i += next;
522        let run = run_length(text, i, quote_byte);
523        if run >= opening {
524            let value = text[start + opening..i + run - opening].to_owned();
525            return Ok((value, i + run));
526        }
527        i += run;
528    }
529
530    Err(unterminated_quote(text[..start].chars().count()))
531}
532
533/// The length of the run of `c` that starts at `start`.
534fn run_length(text: &str, start: usize, c: u8) -> usize {
535    text.as_bytes()[start..]
536        .iter()
537        .take_while(|&&x| x == c)
538        .count()
539}
540
541fn unterminated_quote(start: usize) -> CodecError {
542    CodecError::ParseError(format!(
543        "unterminated quoted value starting at character {}",
544        start
545    ))
546}
547
548struct Cursor {
549    tokens: Vec<Token>,
550    pos: usize,
551}
552
553impl Cursor {
554    /// Parse rows until the matching `)` (or the end of input at the top level).
555    /// A row is one line: the values written between two newlines.
556    fn parse_rows(&mut self, top_level: bool) -> Result<Vec<Vec<Node>>, CodecError> {
557        let mut rows: Vec<Vec<Node>> = Vec::new();
558        let mut row: Vec<Node> = Vec::new();
559
560        while self.pos < self.tokens.len() {
561            match &self.tokens[self.pos] {
562                Token::Close => {
563                    if top_level {
564                        break;
565                    }
566                    self.pos += 1;
567                    if !row.is_empty() {
568                        rows.push(row);
569                    }
570                    return Ok(rows);
571                }
572                Token::Newline => {
573                    self.pos += 1;
574                    if !row.is_empty() {
575                        rows.push(std::mem::take(&mut row));
576                    }
577                }
578                _ => row.push(self.parse_node()?),
579            }
580        }
581
582        if !top_level {
583            return Err(CodecError::ParseError(
584                "unterminated '(' in readable notation".to_string(),
585            ));
586        }
587
588        if !row.is_empty() {
589            rows.push(row);
590        }
591        Ok(rows)
592    }
593
594    fn parse_node(&mut self) -> Result<Node, CodecError> {
595        match self.tokens[self.pos].clone() {
596            Token::Ref { value, quoted } => {
597                self.pos += 1;
598                Ok(Node::Ref { value, quoted })
599            }
600            Token::Open => {
601                self.pos += 1;
602                let object = self.take_object_marker();
603                let multiline = self.link_is_multiline();
604                let rows = self.parse_rows(false)?;
605                Ok(Node::Link {
606                    rows,
607                    multiline,
608                    object,
609                })
610            }
611            Token::Close | Token::Newline => Err(CodecError::ParseError(
612                "unexpected token in readable notation".to_string(),
613            )),
614        }
615    }
616
617    /// Consume the `o:` marker if the link that just opened carries one, which is
618    /// how the single-line form says "this link is an object, not an array".
619    fn take_object_marker(&mut self) -> bool {
620        let marker = format!("{}:", OBJECT_MARKER);
621        let is_marker = matches!(
622            self.tokens.get(self.pos),
623            Some(Token::Ref { value, quoted: false }) if *value == marker
624        );
625        let array_marker = matches!(self.tokens.get(self.pos),
626            Some(Token::Ref { value, quoted: false }) if value == "a:");
627        if is_marker || array_marker {
628            self.pos += 1;
629        }
630        is_marker
631    }
632
633    /// Whether the link that just opened spans more than one line, which is what
634    /// tells an empty object (`(\n)`) from an empty array (`()`).
635    fn link_is_multiline(&self) -> bool {
636        self.tokens[self.pos..]
637            .iter()
638            .take_while(|t| **t != Token::Close)
639            .any(|t| *t == Token::Newline)
640    }
641}
642
643fn node_to_value(node: &Node) -> Result<LinoValue, CodecError> {
644    match node {
645        Node::Ref { value, quoted } => Ok(ref_to_value(value, *quoted)),
646        Node::Link {
647            rows,
648            multiline,
649            object,
650        } => rows_to_value(rows, *multiline, *object),
651    }
652}
653
654fn rows_to_value(
655    rows: &[Vec<Node>],
656    multiline: bool,
657    object_marker: bool,
658) -> Result<LinoValue, CodecError> {
659    if object_marker {
660        return marked_object_to_value(rows);
661    }
662
663    if rows.is_empty() {
664        return Ok(if multiline {
665            LinoValue::Object(vec![])
666        } else {
667            LinoValue::Array(vec![])
668        });
669    }
670
671    if let Some(marked) = decode_marked_value(rows) {
672        return marked;
673    }
674
675    // Written on one line, a link is a list of values: an object on one line says
676    // so with the `o:` marker, which is what keeps `(key value)` unambiguous.
677    if !multiline {
678        let mut items = Vec::new();
679        for row in rows {
680            for node in row {
681                items.push(node_to_value(node)?);
682            }
683        }
684        return Ok(LinoValue::Array(items));
685    }
686
687    // `key value` on every line makes an object; anything else is a list of values.
688    let is_object = rows
689        .iter()
690        .all(|row| row.len() == 2 && node_to_key(&row[0]).is_some());
691
692    if is_object {
693        let mut pairs = Vec::with_capacity(rows.len());
694        for row in rows {
695            let Some(key) = node_to_key(&row[0]) else {
696                unreachable!("checked by is_object")
697            };
698            pairs.push((key, node_to_value(&row[1])?));
699        }
700        return Ok(LinoValue::Object(pairs));
701    }
702
703    let mut items = Vec::new();
704    for row in rows {
705        for node in row {
706            items.push(node_to_value(node)?);
707        }
708    }
709    Ok(LinoValue::Array(items))
710}
711
712/// Build the object a `(o: (key value) …)` link describes. Every value in it is
713/// a pair, so anything else is a malformed document rather than a silent array.
714fn marked_object_to_value(rows: &[Vec<Node>]) -> Result<LinoValue, CodecError> {
715    let mut pairs = Vec::new();
716
717    for node in rows.iter().flatten() {
718        let Node::Link {
719            rows: pair,
720            object: false,
721            ..
722        } = node
723        else {
724            return Err(CodecError::ParseError(format!(
725                "an object marked '{}:' holds (key value) pairs, found a value that is not a pair",
726                OBJECT_MARKER
727            )));
728        };
729
730        let [row] = pair.as_slice() else {
731            return Err(CodecError::ParseError(format!(
732                "an object marked '{}:' holds (key value) pairs, found a link of {} lines",
733                OBJECT_MARKER,
734                pair.len()
735            )));
736        };
737
738        let [key_node, value] = row.as_slice() else {
739            return Err(CodecError::ParseError(format!(
740                "an object marked '{}:' holds (key value) pairs, found a link of {} values",
741                OBJECT_MARKER,
742                row.len()
743            )));
744        };
745
746        let Some(key) = node_to_key(key_node) else {
747            return Err(CodecError::ParseError(format!(
748                "an object marked '{}:' holds (key value) pairs, found a pair whose key is not text",
749                OBJECT_MARKER
750            )));
751        };
752
753        pairs.push((key, node_to_value(value)?));
754    }
755
756    Ok(LinoValue::Object(pairs))
757}
758
759/// The key a node in key position spells: a reference is the key itself, and a
760/// marked link is the text its marker escapes, which is how a key holding a
761/// character the form cannot carry stays a key instead of turning its object into
762/// an array.
763fn node_to_key(node: &Node) -> Option<String> {
764    match node {
765        Node::Ref { value, .. } => Some(value.clone()),
766        Node::Link {
767            rows,
768            object: false,
769            ..
770        } => match decode_marked_value(rows)? {
771            Ok(LinoValue::String(key)) => Some(key),
772            _ => None,
773        },
774        Node::Link { .. } => None,
775    }
776}
777
778/// Recognise a marked value: `(escaped "…")`, whose text is written as it is
779/// except for the percent-escaped characters this form cannot carry, and
780/// `(base64 "…")`, which versions up to 0.6.0 wrote and which is still read. A
781/// quoted marker is an ordinary object key, not a marker.
782fn decode_marked_value(rows: &[Vec<Node>]) -> Option<Result<LinoValue, CodecError>> {
783    if rows.len() != 1 || rows[0].len() != 2 {
784        return None;
785    }
786
787    let Node::Ref {
788        value: marker,
789        quoted: false,
790    } = &rows[0][0]
791    else {
792        return None;
793    };
794
795    let Node::Ref {
796        value: payload,
797        quoted: true,
798    } = &rows[0][1]
799    else {
800        return None;
801    };
802
803    #[cfg(feature = "serde_json")]
804    if marker == "json-number" {
805        return Some(
806            payload
807                .parse::<serde_json::Number>()
808                .map(LinoValue::JsonNumber)
809                .map_err(|e| CodecError::DecodeError(format!("invalid JSON number: {e}"))),
810        );
811    }
812
813    if marker == ESCAPED_MARKER {
814        return Some(unescape(payload).map(LinoValue::String));
815    }
816
817    if marker == BASE64_MARKER {
818        return Some(
819            BASE64
820                .decode(payload)
821                .map_err(|e| CodecError::DecodeError(format!("invalid base64 value: {}", e)))
822                .and_then(|bytes| {
823                    String::from_utf8(bytes)
824                        .map(LinoValue::String)
825                        .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 value: {}", e)))
826                }),
827        );
828    }
829
830    None
831}
832
833/// Undo the percent-escaping of an `(escaped "…")` payload. Escapes stand for
834/// bytes, so a character outside ASCII is written as its UTF-8 bytes and read
835/// back from them.
836pub fn unescape(payload: &str) -> Result<String, CodecError> {
837    let bytes = payload.as_bytes();
838    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
839    let mut i = 0;
840
841    while i < bytes.len() {
842        if bytes[i] != b'%' {
843            out.push(bytes[i]);
844            i += 1;
845            continue;
846        }
847
848        let escape = payload.get(i + 1..i + 3).ok_or_else(|| {
849            CodecError::DecodeError(format!(
850                "truncated escape at character {} of an escaped value",
851                i
852            ))
853        })?;
854        let byte = u8::from_str_radix(escape, 16).map_err(|_| {
855            CodecError::DecodeError(format!("invalid escape '%{}' in an escaped value", escape))
856        })?;
857        out.push(byte);
858        i += 3;
859    }
860
861    String::from_utf8(out)
862        .map_err(|e| CodecError::DecodeError(format!("invalid UTF-8 escaped value: {}", e)))
863}
864
865/// Convert a reference to a value. Quoted references are always strings; bare
866/// references keep the type they were written with.
867fn ref_to_value(value: &str, quoted: bool) -> LinoValue {
868    if quoted {
869        return LinoValue::String(value.to_string());
870    }
871
872    match value {
873        "null" => return LinoValue::Null,
874        "true" => return LinoValue::Bool(true),
875        "false" => return LinoValue::Bool(false),
876        "NaN" => return LinoValue::Float(f64::NAN),
877        "Infinity" => return LinoValue::Float(f64::INFINITY),
878        "-Infinity" => return LinoValue::Float(f64::NEG_INFINITY),
879        _ => {}
880    }
881
882    if let Ok(i) = value.parse::<i64>() {
883        return LinoValue::Int(i);
884    }
885    if value.contains(['.', 'e', 'E']) {
886        if let Ok(f) = value.parse::<f64>() {
887            return LinoValue::Float(f);
888        }
889    }
890
891    LinoValue::String(value.to_string())
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    fn roundtrip(value: &LinoValue) -> LinoValue {
899        let text = encode(value, DEFAULT_INDENT);
900        decode(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
901    }
902
903    #[test]
904    fn empty_containers_keep_their_type() {
905        assert_eq!(encode(&LinoValue::Array(vec![]), DEFAULT_INDENT), "()");
906        assert_eq!(encode(&LinoValue::Object(vec![]), DEFAULT_INDENT), "(\n)");
907        assert_eq!(
908            roundtrip(&LinoValue::Array(vec![])),
909            LinoValue::Array(vec![])
910        );
911        assert_eq!(
912            roundtrip(&LinoValue::Object(vec![])),
913            LinoValue::Object(vec![])
914        );
915    }
916
917    #[test]
918    fn single_pair_object_is_not_an_array() {
919        let value = LinoValue::object([("a", LinoValue::Int(1))]);
920        assert_eq!(encode(&value, DEFAULT_INDENT), "(\n  a 1\n)");
921        assert_eq!(roundtrip(&value), value);
922    }
923
924    #[test]
925    fn numeric_looking_strings_stay_strings() {
926        let value = LinoValue::object([
927            ("count", LinoValue::Int(42)),
928            ("zip", LinoValue::String("10001".to_string())),
929            ("flag", LinoValue::String("true".to_string())),
930        ]);
931        assert_eq!(roundtrip(&value), value);
932    }
933
934    #[test]
935    fn whole_floats_stay_floats() {
936        let value = LinoValue::Float(1.0);
937        assert_eq!(encode(&value, DEFAULT_INDENT), "1.0");
938        assert!(matches!(roundtrip(&value), LinoValue::Float(f) if (f - 1.0).abs() < f64::EPSILON));
939    }
940
941    #[test]
942    fn base64_key_is_quoted_so_it_is_not_a_marker() {
943        let value = LinoValue::object([("base64", LinoValue::String("plain".to_string()))]);
944        assert_eq!(
945            encode(&value, DEFAULT_INDENT),
946            "(\n  \"base64\" \"plain\"\n)"
947        );
948        assert_eq!(roundtrip(&value), value);
949    }
950
951    #[test]
952    fn text_is_written_as_text_and_only_the_rest_is_escaped() {
953        let value = LinoValue::object([
954            ("plain", LinoValue::String("visible".to_string())),
955            ("raw", LinoValue::String("line1\nline2".to_string())),
956            ("returned", LinoValue::String("line1\r\nline2".to_string())),
957        ]);
958        let text = encode(&value, DEFAULT_INDENT);
959        assert!(text.contains("plain \"visible\""), "{}", text);
960        assert!(text.contains("raw \"line1\nline2\""), "{}", text);
961        assert!(
962            text.contains("returned (escaped \"line1%0D\nline2\")"),
963            "{}",
964            text
965        );
966        assert!(!text.contains("base64"), "{}", text);
967        assert_eq!(roundtrip(&value), value);
968    }
969
970    /// The readable form is Links Notation, so the notation's own parser has to
971    /// read every document it writes -- quotes, newlines and escapes included.
972    #[test]
973    fn every_written_document_parses_as_links_notation() {
974        let texts = [
975            "plain",
976            "it's",
977            "he said \"hello\"",
978            "both \"kinds\" of 'quotes'",
979            "a\"\"b'c",
980            "a\"\"\"b'c",
981            "trailing quote\"'",
982            "'\"",
983            "line one\nline two",
984            "a\tb",
985            "null\u{0}byte",
986            "unicode: 你好世界 🌍",
987        ];
988
989        for text in texts {
990            let value = LinoValue::object([
991                ("message", LinoValue::String(text.to_string())),
992                ("level", LinoValue::String("info".to_string())),
993            ]);
994
995            for document in [encode(&value, DEFAULT_INDENT), encode_line(&value)] {
996                assert!(
997                    links_notation::parse_lino(&document).is_ok(),
998                    "links-notation rejected {:?}",
999                    document
1000                );
1001            }
1002        }
1003    }
1004
1005    /// The value the notation's parser reads back has to be the value written,
1006    /// not merely something that parses.
1007    #[test]
1008    fn links_notation_reads_back_the_text_that_was_written() {
1009        for text in [
1010            "he said \"hello\"",
1011            "both \"kinds\" of 'quotes'",
1012            "a\"\"b'c",
1013            "line one\nline two",
1014        ] {
1015            let document = encode(
1016                &LinoValue::object([("message", LinoValue::String(text.to_string()))]),
1017                DEFAULT_INDENT,
1018            );
1019            let parsed = links_notation::parse_lino(&document)
1020                .unwrap_or_else(|e| panic!("links-notation rejected {:?}: {}", document, e));
1021
1022            let mut refs = Vec::new();
1023            collect_refs(&parsed, &mut refs);
1024            assert!(
1025                refs.contains(&text.to_string()),
1026                "links-notation read {:?} out of {:?}",
1027                refs,
1028                document
1029            );
1030        }
1031    }
1032
1033    fn collect_refs(node: &links_notation::LiNo<String>, out: &mut Vec<String>) {
1034        match node {
1035            links_notation::LiNo::Ref(value) => out.push(value.clone()),
1036            links_notation::LiNo::Link { values, .. } => {
1037                for value in values {
1038                    collect_refs(value, out);
1039                }
1040            }
1041        }
1042    }
1043
1044    #[test]
1045    fn custom_indent_is_used() {
1046        let value = LinoValue::object([("a", LinoValue::Int(1))]);
1047        assert_eq!(encode(&value, "    "), "(\n    a 1\n)");
1048    }
1049
1050    #[test]
1051    fn handwritten_document_without_root_parentheses_decodes() {
1052        let decoded = decode("a 1\nb \"two\"").unwrap();
1053        assert_eq!(
1054            decoded,
1055            LinoValue::object([
1056                ("a", LinoValue::Int(1)),
1057                ("b", LinoValue::String("two".into()))
1058            ])
1059        );
1060    }
1061
1062    fn line_roundtrip(value: &LinoValue) -> LinoValue {
1063        let text = encode_line(value);
1064        assert!(
1065            !text.contains('\n'),
1066            "line form must hold no newline: {:?}",
1067            text
1068        );
1069        decode_line(&text).unwrap_or_else(|e| panic!("failed to decode {:?}: {}", text, e))
1070    }
1071
1072    #[test]
1073    fn line_form_marks_objects_and_leaves_arrays_bare() {
1074        assert_eq!(encode_line(&LinoValue::Array(vec![])), "()");
1075        assert_eq!(encode_line(&LinoValue::Object(vec![])), "(o:)");
1076        assert_eq!(
1077            encode_line(&LinoValue::object([("a", LinoValue::Int(1))])),
1078            "(o: (a 1))"
1079        );
1080        assert_eq!(
1081            encode_line(&LinoValue::array([
1082                LinoValue::String("key".into()),
1083                LinoValue::String("value".into()),
1084            ])),
1085            "(\"key\" \"value\")"
1086        );
1087    }
1088
1089    #[test]
1090    fn line_form_tells_a_one_pair_object_from_a_two_element_array() {
1091        let object = LinoValue::object([("key", LinoValue::String("value".into()))]);
1092        let array = LinoValue::array([
1093            LinoValue::String("key".into()),
1094            LinoValue::String("value".into()),
1095        ]);
1096        assert_ne!(encode_line(&object), encode_line(&array));
1097        assert_eq!(line_roundtrip(&object), object);
1098        assert_eq!(line_roundtrip(&array), array);
1099    }
1100
1101    #[test]
1102    fn line_form_roundtrips_nested_and_empty_containers() {
1103        let value = LinoValue::object([
1104            ("empty_array", LinoValue::Array(vec![])),
1105            ("empty_object", LinoValue::Object(vec![])),
1106            (
1107                "records",
1108                LinoValue::array([
1109                    LinoValue::object([("id", LinoValue::Int(1))]),
1110                    LinoValue::object([("id", LinoValue::Int(2))]),
1111                ]),
1112            ),
1113        ]);
1114        assert_eq!(
1115            encode_line(&value),
1116            "(o: (empty_array ()) (empty_object (o:)) (records ((o: (id 1)) (o: (id 2)))))"
1117        );
1118        assert_eq!(line_roundtrip(&value), value);
1119    }
1120
1121    #[test]
1122    fn line_form_keeps_the_empty_key() {
1123        let value = LinoValue::object([("", LinoValue::Int(2))]);
1124        assert_eq!(encode_line(&value), "(o: (\"\" 2))");
1125        assert_eq!(line_roundtrip(&value), value);
1126    }
1127
1128    #[test]
1129    fn line_form_keeps_strings_on_one_line() {
1130        let value = LinoValue::object([
1131            (
1132                "quotes",
1133                LinoValue::String("both \"kinds\" of 'quotes'".into()),
1134            ),
1135            ("unicode", LinoValue::String("héllo 世界 🌍".into())),
1136            ("multiline", LinoValue::String("line1\nline2".into())),
1137        ]);
1138        let text = encode_line(&value);
1139        assert!(!text.contains('\n'), "{}", text);
1140        assert_eq!(line_roundtrip(&value), value);
1141    }
1142
1143    #[test]
1144    fn a_line_form_object_holds_pairs_only() {
1145        assert!(decode_line("(o: 1 2)").is_err());
1146        assert!(decode_line("(o: (a 1 2))").is_err());
1147        assert!(decode_line("(o: (a))").is_err());
1148    }
1149
1150    #[test]
1151    fn decode_line_rejects_a_document_of_several_lines() {
1152        assert!(decode_line("(o: (a 1))\n(o: (a 2))").is_err());
1153        // A trailing line break is what a line-based reader hands over, so it is
1154        // stripped rather than rejected.
1155        assert_eq!(
1156            decode_line("(o: (a 1))\n").unwrap(),
1157            LinoValue::object([("a", LinoValue::Int(1))])
1158        );
1159    }
1160
1161    #[test]
1162    fn unterminated_input_is_an_error() {
1163        assert!(decode("(\n  a 1\n").is_err());
1164        assert!(decode("(\n  a \"unterminated\n").is_err());
1165        assert!(decode("(\n  a \"\"\"unterminated\n").is_err());
1166        assert!(decode("a 1)").is_err());
1167    }
1168
1169    /// The three ways a run of delimiters reads, which is what keeps documents
1170    /// written by earlier versions decoding as they did.
1171    #[test]
1172    fn a_run_of_delimiters_says_how_the_value_is_read() {
1173        // One delimiter: a doubled delimiter is one literal delimiter.
1174        assert_eq!(
1175            decode("\"both \"\"kinds\"\" of 'quotes'\"").unwrap(),
1176            LinoValue::String("both \"kinds\" of 'quotes'".to_string())
1177        );
1178        // Two: the empty value.
1179        assert_eq!(decode("\"\"").unwrap(), LinoValue::String(String::new()));
1180        // Three or more: the text is literal, and the last delimiters close it.
1181        assert_eq!(
1182            decode("\"\"\"say \"hi\"\"\"\"").unwrap(),
1183            LinoValue::String("say \"hi\"".to_string())
1184        );
1185    }
1186}