Skip to main content

sim_codec/implementation/
strings.rs

1//! String-literal encoding and decoding.
2//!
3//! Renders a string to a quoted, escaped literal and parses such a literal
4//! back, failing closed on malformed input.
5
6use sim_kernel::{CodecId, Error, Result};
7
8/// Render `value` as a double-quoted, backslash-escaped string literal.
9///
10/// Backslash, double quote, newline, carriage return, and tab use short
11/// escapes; other control characters use the `\u{..}` form. Round-trips through
12/// [`decode_string_literal`].
13///
14/// # Examples
15///
16/// ```
17/// use sim_codec::{decode_string_literal, encode_string_literal};
18/// use sim_kernel::CodecId;
19///
20/// let literal = encode_string_literal("a\tb\"c");
21/// assert_eq!(literal, "\"a\\tb\\\"c\"");
22/// assert_eq!(decode_string_literal(CodecId(0), &literal).unwrap(), "a\tb\"c");
23/// ```
24pub fn encode_string_literal(value: &str) -> String {
25    let mut out = String::from("\"");
26    for ch in value.chars() {
27        match ch {
28            '\\' => out.push_str("\\\\"),
29            '"' => out.push_str("\\\""),
30            '\n' => out.push_str("\\n"),
31            '\r' => out.push_str("\\r"),
32            '\t' => out.push_str("\\t"),
33            ch if ch.is_control() => {
34                use std::fmt::Write;
35                write!(&mut out, "\\u{{{:x}}}", ch as u32).expect("string writes do not fail");
36            }
37            ch => out.push(ch),
38        }
39    }
40    out.push('"');
41    out
42}
43
44/// Parse a double-quoted, escaped string literal back to its value.
45///
46/// Accepts the escapes produced by [`encode_string_literal`] and fails closed
47/// with a codec error tagged `codec` on any malformed literal (missing quotes,
48/// unterminated or unsupported escape, invalid Unicode scalar).
49pub fn decode_string_literal(codec: CodecId, raw: &str) -> Result<String> {
50    let inner = raw
51        .strip_prefix('"')
52        .and_then(|rest| rest.strip_suffix('"'))
53        .ok_or_else(|| Error::CodecError {
54            codec,
55            message: format!("invalid string literal {raw}"),
56        })?;
57    let chars = inner.chars().collect::<Vec<_>>();
58    let mut index = 0;
59    let mut out = String::new();
60    while index < chars.len() {
61        let ch = chars[index];
62        if ch != '\\' {
63            out.push(ch);
64            index += 1;
65            continue;
66        }
67
68        index += 1;
69        let escaped = *chars.get(index).ok_or_else(|| Error::CodecError {
70            codec,
71            message: format!("unterminated string escape in {raw}"),
72        })?;
73        match escaped {
74            '\\' => out.push('\\'),
75            '"' => out.push('"'),
76            'n' => out.push('\n'),
77            'r' => out.push('\r'),
78            't' => out.push('\t'),
79            'u' => {
80                index += 1;
81                if chars.get(index) != Some(&'{') {
82                    return Err(Error::CodecError {
83                        codec,
84                        message: format!("invalid unicode escape in {raw}"),
85                    });
86                }
87                let mut hex = String::new();
88                index += 1;
89                while let Some(ch) = chars.get(index) {
90                    if *ch == '}' {
91                        break;
92                    }
93                    hex.push(*ch);
94                    index += 1;
95                }
96                if chars.get(index) != Some(&'}') || hex.is_empty() {
97                    return Err(Error::CodecError {
98                        codec,
99                        message: format!("invalid unicode escape in {raw}"),
100                    });
101                }
102                let scalar = u32::from_str_radix(&hex, 16).map_err(|_| Error::CodecError {
103                    codec,
104                    message: format!("invalid unicode escape in {raw}"),
105                })?;
106                let decoded = char::from_u32(scalar).ok_or_else(|| Error::CodecError {
107                    codec,
108                    message: format!("invalid unicode scalar in {raw}"),
109                })?;
110                out.push(decoded);
111            }
112            other => {
113                return Err(Error::CodecError {
114                    codec,
115                    message: format!("unsupported string escape \\{other} in {raw}"),
116                });
117            }
118        }
119        index += 1;
120    }
121    Ok(out)
122}