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