Skip to main content

aven_protocol/
base64_bytes.rs

1//! JSON transport spelling for opaque byte payloads: one canonical padded
2//! standard base64 string. The bytes themselves are what callers hash, sign
3//! and store.
4use base64::{Engine, engine::general_purpose::STANDARD};
5use serde::{Deserializer, Serializer, de};
6
7/// Serialized length of `n` bytes, excluding the JSON quotes.
8pub const fn encoded_len(n: usize) -> usize {
9    match base64::encoded_len(n, true) {
10        Some(len) => len,
11        None => panic!("base64 length overflow"),
12    }
13}
14
15pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
16    serializer.serialize_str(&STANDARD.encode(bytes))
17}
18
19pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
20    deserializer.deserialize_str(Visitor(usize::MAX))
21}
22
23/// Refuses a string that would decode to more than `N` bytes before decoding it.
24pub fn bounded<'de, D: Deserializer<'de>, const N: usize>(
25    deserializer: D,
26) -> Result<Vec<u8>, D::Error> {
27    deserializer.deserialize_str(Visitor(encoded_len(N)))
28}
29
30/// The same spelling for an optional payload, with `null` for `None`.
31pub mod option {
32    use base64::Engine;
33    use serde::{Deserialize, Deserializer, Serializer};
34
35    #[derive(Deserialize)]
36    struct Bytes(#[serde(with = "super")] Vec<u8>);
37
38    pub fn serialize<S: Serializer>(
39        bytes: &Option<Vec<u8>>,
40        serializer: S,
41    ) -> Result<S::Ok, S::Error> {
42        match bytes {
43            Some(bytes) => serializer.serialize_some(&super::STANDARD.encode(bytes)),
44            None => serializer.serialize_none(),
45        }
46    }
47
48    pub fn deserialize<'de, D: Deserializer<'de>>(
49        deserializer: D,
50    ) -> Result<Option<Vec<u8>>, D::Error> {
51        Ok(Option::<Bytes>::deserialize(deserializer)?.map(|bytes| bytes.0))
52    }
53}
54
55struct Visitor(usize);
56impl de::Visitor<'_> for Visitor {
57    type Value = Vec<u8>;
58    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59        write!(f, "a base64 string of at most {} characters", self.0)
60    }
61    fn visit_str<E: de::Error>(self, value: &str) -> Result<Vec<u8>, E> {
62        if value.len() > self.0 {
63            return Err(E::custom("byte limit"));
64        }
65        STANDARD.decode(value).map_err(E::custom)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use serde::{Deserialize, Serialize};
72
73    #[derive(Serialize, Deserialize, PartialEq, Debug)]
74    struct Field(#[serde(with = "super")] Vec<u8>);
75
76    #[test]
77    fn round_trips_with_exact_length() {
78        for (n, encoded) in [
79            (0, 0),
80            (1, 4),
81            (2, 4),
82            (3, 4),
83            (4, 8),
84            (255, 340),
85            (1024, 1368),
86        ] {
87            let bytes: Vec<u8> = (0..n).map(|i| (i * 7) as u8).collect();
88            let json = serde_json::to_string(&Field(bytes.clone())).unwrap();
89            assert_eq!(super::encoded_len(n), encoded);
90            assert_eq!(json.len(), encoded + 2);
91            assert_eq!(serde_json::from_str::<Field>(&json).unwrap().0, bytes);
92        }
93    }
94
95    #[test]
96    fn rejects_other_spellings() {
97        for json in [
98            "[1,2,3]",
99            "\"AQID\\n\"",
100            "\"AQI\"",
101            "\"AQJ=\"",
102            "\"AQ-_\"",
103            "\"AQ==AQ==\"",
104        ] {
105            assert!(serde_json::from_str::<Field>(json).is_err(), "{json}");
106        }
107        #[derive(Deserialize)]
108        struct Bounded(#[serde(deserialize_with = "super::bounded::<_, 3>")] Vec<u8>);
109        assert_eq!(
110            serde_json::from_str::<Bounded>("\"AQID\"").unwrap().0,
111            [1, 2, 3]
112        );
113        assert!(serde_json::from_str::<Bounded>("\"AQIDBA==\"").is_err());
114        // An escaped JSON string still decodes to the same bytes.
115        assert_eq!(
116            serde_json::from_str::<Field>("\"\\u0041QID\"").unwrap().0,
117            [1, 2, 3]
118        );
119    }
120}