Skip to main content

bynk_project/
json.rs

1//! The one JSON string escaper for every manifest `bynk-emit` hand-renders.
2//!
3//! Several build outputs are JSON that `bynk-emit` writes by hand rather than
4//! via serde (`bynk-contracts.json`, `bynk-secrets.json`, `package.json`, the
5//! source map): the shapes are a handful of fields each, and staying
6//! serde-free keeps the dependency footprint honest. What they must share is
7//! the escaping — every one of them interpolates text that originated in
8//! Bynk source (a secret name from a `StrLit`, an adapter's npm package
9//! name, a file path), so as far as the renderer is concerned the input is
10//! arbitrary.
11//!
12//! This lived as four near-copies, one per rendering module. Three agreed;
13//! the `package.json` one escaped only `"` and `\`, so an adapter-declared
14//! package name or version range carrying a control character emitted a
15//! literal control character inside a JSON string — a parse error, i.e. an
16//! invalid `package.json` in the build output. One definition means a fix
17//! like that cannot land in three places and miss the fourth.
18//!
19//! P4.0 (#1113, [DECISION C]): moved here from `bynk-emit/src/json.rs` so
20//! `bynk-project`'s own `paths::render_package_json` (its sole in-crate use)
21//! doesn't need a dependency back on `bynk-emit`; `bynk-emit`'s four other
22//! call sites (`secrets.rs`, `contracts.rs`, `source_map.rs`, `emit.rs`) now
23//! depend on this copy instead of a crate-local one, one definition either
24//! way.
25
26use std::fmt::Write as _;
27
28/// Render `s` as a double-quoted JSON string literal, escaping the two
29/// structural characters (`"` and `\`) and the whole C0 control range.
30///
31/// The C0 range matters and is easy to forget: RFC 8259 forbids an unescaped
32/// character below U+0020 inside a string, so a stray newline or NUL is the
33/// difference between a manifest that parses and one that merely usually
34/// parses. Characters at or above U+0020 pass through as-is — the output is
35/// UTF-8 JSON, so there is no reason to `\u`-escape non-ASCII.
36pub fn json_string(s: &str) -> String {
37    let mut out = String::with_capacity(s.len() + 2);
38    out.push('"');
39    for c in s.chars() {
40        match c {
41            '"' => out.push_str("\\\""),
42            '\\' => out.push_str("\\\\"),
43            '\n' => out.push_str("\\n"),
44            '\r' => out.push_str("\\r"),
45            '\t' => out.push_str("\\t"),
46            // The rest of C0 has no short escape; `\u00xx` is the only form.
47            // `write!` into the buffer rather than `push_str(&format!(…))` (what
48            // two of the four copies this replaces did) — no `String` allocated
49            // per control character.
50            c if (c as u32) < 0x20 => {
51                let _ = write!(out, "\\u{:04x}", c as u32);
52            }
53            c => out.push(c),
54        }
55    }
56    out.push('"');
57    out
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn plain_text_is_merely_quoted() {
66        assert_eq!(json_string("AUTH_JWT_SECRET"), "\"AUTH_JWT_SECRET\"");
67        assert_eq!(json_string(""), "\"\"");
68    }
69
70    #[test]
71    fn structural_characters_are_escaped() {
72        assert_eq!(json_string("a\"b"), "\"a\\\"b\"");
73        assert_eq!(json_string("a\\b"), "\"a\\\\b\"");
74    }
75
76    #[test]
77    fn the_control_range_is_escaped() {
78        assert_eq!(json_string("a\nb"), "\"a\\nb\"");
79        assert_eq!(json_string("a\rb"), "\"a\\rb\"");
80        assert_eq!(json_string("a\tb"), "\"a\\tb\"");
81        assert_eq!(json_string("a\u{1}b"), "\"a\\u0001b\"");
82        assert_eq!(json_string("a\u{0}b"), "\"a\\u0000b\"");
83        assert_eq!(json_string("a\u{1f}b"), "\"a\\u001fb\"");
84    }
85
86    /// U+0020 is the first character JSON allows unescaped; the boundary is
87    /// exactly where the C0 arm stops.
88    #[test]
89    fn space_and_above_pass_through() {
90        assert_eq!(json_string("a b"), "\"a b\"");
91        assert_eq!(json_string("a\u{7f}b"), "\"a\u{7f}b\"");
92        assert_eq!(json_string("héllo ☃"), "\"héllo ☃\"");
93    }
94}