Skip to main content

mermaid_cli/utils/
serde_base64.rs

1//! Serde adapter: `Vec<u8>` ↔ base64 string.
2//!
3//! Raw byte payloads (pasted images, tool artifacts) ride inside recorded
4//! `Msg` JSON for `--record` / `--replay`. Serde's default `Vec<u8>`
5//! representation is an array of numbers — ~4x the size and unreadable in a
6//! JSONL line; base64 keeps recordings compact while still round-tripping
7//! bit-exactly.
8//!
9//! Use with `#[serde(with = "crate::utils::serde_base64")]` on a `Vec<u8>`
10//! field.
11
12use base64::{Engine as _, engine::general_purpose};
13use serde::{Deserialize, Deserializer, Serializer};
14
15pub fn serialize<S: Serializer>(bytes: &[u8], ser: S) -> Result<S::Ok, S::Error> {
16    ser.serialize_str(&general_purpose::STANDARD.encode(bytes))
17}
18
19pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<u8>, D::Error> {
20    let s = String::deserialize(de)?;
21    general_purpose::STANDARD
22        .decode(s.as_bytes())
23        .map_err(serde::de::Error::custom)
24}
25
26/// Base64 serde adapter for opaque UTF-8 strings. The wire value is encoded
27/// before persistence redaction sees it, then validated as UTF-8 while loading.
28pub mod string {
29    use base64::{Engine as _, engine::general_purpose};
30    use serde::{Deserialize, Deserializer, Serializer};
31
32    pub fn serialize<S: Serializer>(value: &str, ser: S) -> Result<S::Ok, S::Error> {
33        ser.serialize_str(&general_purpose::STANDARD.encode(value.as_bytes()))
34    }
35
36    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<String, D::Error> {
37        let encoded = String::deserialize(de)?;
38        let bytes = general_purpose::STANDARD
39            .decode(encoded.as_bytes())
40            .map_err(serde::de::Error::custom)?;
41        String::from_utf8(bytes).map_err(serde::de::Error::custom)
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use serde::{Deserialize, Serialize};
48
49    #[derive(Serialize, Deserialize, PartialEq, Debug)]
50    struct Blob {
51        #[serde(with = "super")]
52        data: Vec<u8>,
53    }
54
55    #[test]
56    fn round_trips_bytes_as_base64_string() {
57        let blob = Blob {
58            data: vec![0, 1, 2, 250, 255],
59        };
60        let json = serde_json::to_string(&blob).unwrap();
61        assert_eq!(json, r#"{"data":"AAEC+v8="}"#);
62        let back: Blob = serde_json::from_str(&json).unwrap();
63        assert_eq!(back, blob);
64    }
65
66    #[test]
67    fn rejects_invalid_base64() {
68        let err = serde_json::from_str::<Blob>(r#"{"data":"not base64!!"}"#);
69        assert!(err.is_err());
70    }
71
72    #[test]
73    fn opaque_string_round_trips_and_rejects_non_utf8() {
74        #[derive(Serialize, Deserialize, PartialEq, Debug)]
75        struct Opaque {
76            #[serde(with = "super::string")]
77            value: String,
78        }
79        let value = Opaque {
80            value: "eyJopaque.payload.signature".to_string(),
81        };
82        let json = serde_json::to_string(&value).unwrap();
83        assert!(!json.contains("eyJopaque.payload.signature"));
84        assert_eq!(serde_json::from_str::<Opaque>(&json).unwrap(), value);
85        assert!(serde_json::from_str::<Opaque>(r#"{"value":"/w=="}"#).is_err());
86    }
87}