Skip to main content

nodedb_types/json_msgpack/
transcoder.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Streaming msgpack → JSON string transcoder.
4//!
5//! Walks msgpack bytes and writes JSON text directly into a String.
6//! No intermediate `serde_json::Value` or `nodedb_types::Value`.
7//! Used ONLY at the outermost pgwire/HTTP layer for client compatibility.
8//!
9//! Instant ext values (fixext8 type 1 / 2) render as ISO 8601 strings.
10//! Every other ext type renders as `null`.
11
12use std::fmt::Write as _;
13
14use super::error::MsgpackResult;
15use super::instant_ext::instant_from_ext;
16use super::reader::{Cursor, base64_encode};
17use crate::datetime::NdbDateTime;
18use crate::value::float_text::non_finite_float_text;
19
20/// Transcode raw msgpack bytes to a JSON string without intermediate types.
21///
22/// The input must contain exactly one top-level value and nothing else;
23/// trailing bytes are rejected rather than silently dropped.
24pub fn msgpack_to_json_string(bytes: &[u8]) -> MsgpackResult<String> {
25    if bytes.is_empty() {
26        return Ok(String::new());
27    }
28    let mut c = Cursor::new(bytes);
29    let mut out = String::with_capacity(bytes.len() * 2);
30    transcode_value(&mut c, &mut out)?;
31    c.finish()?;
32    Ok(out)
33}
34
35fn transcode_value(c: &mut Cursor<'_>, out: &mut String) -> zerompk::Result<()> {
36    if c.depth > 500 {
37        return Err(zerompk::Error::DepthLimitExceeded { max: 500 });
38    }
39
40    let marker = c.take()?;
41    match marker {
42        0xC0 => out.push_str("null"),
43        0xC2 => out.push_str("false"),
44        0xC3 => out.push_str("true"),
45
46        0x00..=0x7F => {
47            write_int(out, marker as i64);
48        }
49        0xE0..=0xFF => {
50            write_int(out, marker as i8 as i64);
51        }
52
53        0xCC => {
54            write_uint(out, c.take()? as u64);
55        }
56        0xCD => {
57            write_uint(out, c.read_u16_be()? as u64);
58        }
59        0xCE => {
60            write_uint(out, c.read_u32_be()? as u64);
61        }
62        0xCF => {
63            let b = c.take_n(8)?;
64            write_uint(
65                out,
66                u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
67            );
68        }
69
70        0xD0 => {
71            write_int(out, c.take()? as i8 as i64);
72        }
73        0xD1 => {
74            let b = c.take_n(2)?;
75            write_int(out, i16::from_be_bytes([b[0], b[1]]) as i64);
76        }
77        0xD2 => {
78            let b = c.take_n(4)?;
79            write_int(out, i32::from_be_bytes([b[0], b[1], b[2], b[3]]) as i64);
80        }
81        0xD3 => {
82            let b = c.take_n(8)?;
83            write_int(
84                out,
85                i64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
86            );
87        }
88
89        0xCA => {
90            let b = c.take_n(4)?;
91            write_float(out, f32::from_be_bytes([b[0], b[1], b[2], b[3]]) as f64);
92        }
93        0xCB => {
94            let b = c.take_n(8)?;
95            write_float(
96                out,
97                f64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]),
98            );
99        }
100
101        m @ 0xA0..=0xBF => transcode_str(c, out, (m & 0x1F) as usize)?,
102        0xD9 => {
103            let l = c.take()? as usize;
104            transcode_str(c, out, l)?;
105        }
106        0xDA => {
107            let l = c.read_u16_be()? as usize;
108            transcode_str(c, out, l)?;
109        }
110        0xDB => {
111            let l = c.read_u32_be()? as usize;
112            transcode_str(c, out, l)?;
113        }
114
115        0xC4 => {
116            let l = c.take()? as usize;
117            transcode_bin(c, out, l)?;
118        }
119        0xC5 => {
120            let l = c.read_u16_be()? as usize;
121            transcode_bin(c, out, l)?;
122        }
123        0xC6 => {
124            let l = c.read_u32_be()? as usize;
125            transcode_bin(c, out, l)?;
126        }
127
128        m @ 0x90..=0x9F => transcode_array(c, out, (m & 0x0F) as usize)?,
129        0xDC => {
130            let l = c.read_u16_be()? as usize;
131            transcode_array(c, out, l)?;
132        }
133        0xDD => {
134            let l = c.read_u32_be()? as usize;
135            transcode_array(c, out, l)?;
136        }
137
138        m @ 0x80..=0x8F => transcode_map(c, out, (m & 0x0F) as usize)?,
139        0xDE => {
140            let l = c.read_u16_be()? as usize;
141            transcode_map(c, out, l)?;
142        }
143        0xDF => {
144            let l = c.read_u32_be()? as usize;
145            transcode_map(c, out, l)?;
146        }
147
148        // fixext8: instants render as ISO 8601, other types as null
149        0xD7 => {
150            let (ext_type, payload) = c.take_fixext8()?;
151            match instant_from_ext(ext_type, payload) {
152                Some((_, micros)) => {
153                    out.push('"');
154                    out.push_str(&NdbDateTime::from_micros(micros).to_iso8601());
155                    out.push('"');
156                }
157                None => out.push_str("null"),
158            }
159        }
160
161        // other ext types — render as null
162        0xD4 => {
163            c.take_n(2)?;
164            out.push_str("null");
165        }
166        0xD5 => {
167            c.take_n(3)?;
168            out.push_str("null");
169        }
170        0xD6 => {
171            c.take_n(5)?;
172            out.push_str("null");
173        }
174        0xD8 => {
175            c.take_n(17)?;
176            out.push_str("null");
177        }
178        0xC7 => {
179            let l = c.take()? as usize;
180            c.take_n(1 + l)?;
181            out.push_str("null");
182        }
183        0xC8 => {
184            let l = c.read_u16_be()? as usize;
185            c.take_n(1 + l)?;
186            out.push_str("null");
187        }
188        0xC9 => {
189            let l = c.read_u32_be()? as usize;
190            c.take_n(1 + l)?;
191            out.push_str("null");
192        }
193
194        _ => return Err(zerompk::Error::InvalidMarker(marker)),
195    }
196    Ok(())
197}
198
199fn write_int(out: &mut String, v: i64) {
200    let _ = write!(out, "{v}");
201}
202
203fn write_uint(out: &mut String, v: u64) {
204    let _ = write!(out, "{v}");
205}
206
207/// Write a float as JSON. A non-finite float is its PostgreSQL text in a
208/// JSON string, as PostgreSQL `to_json` renders it, never `null`.
209fn write_float(out: &mut String, v: f64) {
210    if let Some(text) = non_finite_float_text(v) {
211        out.push('"');
212        out.push_str(text);
213        out.push('"');
214    } else if v.fract() == 0.0 && v.abs() < (1i64 << 53) as f64 {
215        let _ = write!(out, "{v:.1}");
216    } else {
217        let _ = write!(out, "{v}");
218    }
219}
220
221fn transcode_str(c: &mut Cursor<'_>, out: &mut String, len: usize) -> zerompk::Result<()> {
222    let bytes = c.take_n(len)?;
223    let s = std::str::from_utf8(bytes).map_err(|_| zerompk::Error::InvalidMarker(0))?;
224    out.push('"');
225    for ch in s.chars() {
226        match ch {
227            '"' => out.push_str("\\\""),
228            '\\' => out.push_str("\\\\"),
229            '\n' => out.push_str("\\n"),
230            '\r' => out.push_str("\\r"),
231            '\t' => out.push_str("\\t"),
232            c if c < '\x20' => {
233                let _ = write!(out, "\\u{:04x}", c as u32);
234            }
235            c => out.push(c),
236        }
237    }
238    out.push('"');
239    Ok(())
240}
241
242fn transcode_bin(c: &mut Cursor<'_>, out: &mut String, len: usize) -> zerompk::Result<()> {
243    let bytes = c.take_n(len)?;
244    out.push('"');
245    out.push_str(&base64_encode(bytes));
246    out.push('"');
247    Ok(())
248}
249
250fn transcode_array(c: &mut Cursor<'_>, out: &mut String, len: usize) -> zerompk::Result<()> {
251    c.depth += 1;
252    out.push('[');
253    for i in 0..len {
254        if i > 0 {
255            out.push(',');
256        }
257        transcode_value(c, out)?;
258    }
259    out.push(']');
260    c.depth -= 1;
261    Ok(())
262}
263
264fn transcode_map(c: &mut Cursor<'_>, out: &mut String, len: usize) -> zerompk::Result<()> {
265    c.depth += 1;
266    out.push('{');
267    for i in 0..len {
268        if i > 0 {
269            out.push(',');
270        }
271        let key_marker = c.peek()?;
272        if (0xA0..=0xBF).contains(&key_marker)
273            || key_marker == 0xD9
274            || key_marker == 0xDA
275            || key_marker == 0xDB
276        {
277            transcode_value(c, out)?;
278        } else {
279            let mut tmp = String::new();
280            transcode_value(c, &mut tmp)?;
281            out.push('"');
282            out.push_str(&tmp);
283            out.push('"');
284        }
285        out.push(':');
286        transcode_value(c, out)?;
287    }
288    out.push('}');
289    c.depth -= 1;
290    Ok(())
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::json_msgpack::writer::json_to_msgpack;
297
298    #[test]
299    fn basic_map() {
300        let val = serde_json::json!({"name": "alice", "age": 30, "active": true});
301        let mp = json_to_msgpack(&val).unwrap();
302        let json_str = msgpack_to_json_string(&mp).unwrap();
303        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
304        assert_eq!(parsed["name"], "alice");
305        assert_eq!(parsed["age"], 30);
306        assert_eq!(parsed["active"], true);
307    }
308
309    #[test]
310    fn array() {
311        let val = serde_json::json!([1, "two", null, false]);
312        let mp = json_to_msgpack(&val).unwrap();
313        let json_str = msgpack_to_json_string(&mp).unwrap();
314        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
315        assert_eq!(parsed, val);
316    }
317
318    #[test]
319    fn escaping() {
320        let val = serde_json::json!({"msg": "hello \"world\"\nnewline"});
321        let mp = json_to_msgpack(&val).unwrap();
322        let json_str = msgpack_to_json_string(&mp).unwrap();
323        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
324        assert_eq!(parsed["msg"], "hello \"world\"\nnewline");
325    }
326
327    #[test]
328    fn empty() {
329        assert_eq!(msgpack_to_json_string(&[]).unwrap(), "");
330    }
331
332    #[test]
333    fn instant_ext_renders_iso8601() {
334        use crate::json_msgpack::instant_ext::{InstantKind, write_instant};
335        let mut mp = vec![0x81, 0xA2, b't', b's'];
336        write_instant(&mut mp, InstantKind::Naive, 1_710_498_600_000_000);
337        let json_str = msgpack_to_json_string(&mp).unwrap();
338        assert_eq!(json_str, "{\"ts\":\"2024-03-15T10:30:00.000000Z\"}");
339
340        let unknown = [0xD7, 0x09, 0, 0, 0, 0, 0, 0, 0, 1];
341        assert_eq!(msgpack_to_json_string(&unknown).unwrap(), "null");
342    }
343
344    /// A non-finite float transcodes to its PostgreSQL text in a JSON
345    /// string, never `null`. A finite float stays a JSON number.
346    #[test]
347    fn non_finite_floats_render_postgres_text() {
348        let mut mp = vec![0x94];
349        for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 2.5] {
350            mp.push(0xCB);
351            mp.extend_from_slice(&f.to_be_bytes());
352        }
353        assert_eq!(
354            msgpack_to_json_string(&mp).unwrap(),
355            "[\"NaN\",\"Infinity\",\"-Infinity\",2.5]"
356        );
357    }
358
359    #[test]
360    fn nested() {
361        let val = serde_json::json!({"a": {"b": [1, 2, {"c": 3}]}});
362        let mp = json_to_msgpack(&val).unwrap();
363        let json_str = msgpack_to_json_string(&mp).unwrap();
364        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
365        assert_eq!(parsed, val);
366    }
367}