Skip to main content

kittycad_modeling_cmds/
base64.rs

1//! Base64 data that encodes to url safe base64, but can decode from multiple
2//! base64 implementations to account for various clients and libraries. Compatible
3//! with serde and JsonSchema.
4
5use std::{convert::TryFrom, fmt};
6
7use serde::{
8    de::{Error, Unexpected, Visitor},
9    Deserialize, Deserializer, Serialize, Serializer,
10};
11
12static ALLOWED_DECODING_FORMATS: &[data_encoding::Encoding] = &[
13    data_encoding::BASE64,
14    data_encoding::BASE64URL,
15    data_encoding::BASE64URL_NOPAD,
16    data_encoding::BASE64_MIME,
17    data_encoding::BASE64_NOPAD,
18];
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[cfg_attr(
22    feature = "python",
23    pyo3::pyclass(from_py_object),
24    pyo3_stub_gen::derive::gen_stub_pyclass
25)]
26/// A container for binary that should be base64 encoded in serialisation. In reverse
27/// when deserializing, will decode from many different types of base64 possible.
28pub struct Base64Data(pub Vec<u8>);
29
30impl Base64Data {
31    /// Return is the data is empty.
32    pub fn is_empty(&self) -> bool {
33        self.0.is_empty()
34    }
35}
36
37impl fmt::Display for Base64Data {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "{}", data_encoding::BASE64URL_NOPAD.encode(&self.0))
40    }
41}
42
43impl From<Base64Data> for Vec<u8> {
44    fn from(data: Base64Data) -> Vec<u8> {
45        data.0
46    }
47}
48
49impl From<Vec<u8>> for Base64Data {
50    fn from(data: Vec<u8>) -> Base64Data {
51        Base64Data(data)
52    }
53}
54
55impl AsRef<[u8]> for Base64Data {
56    fn as_ref(&self) -> &[u8] {
57        &self.0
58    }
59}
60
61/// Error returned when invalid Base64 data was sent.
62#[derive(Default, Debug)]
63pub struct CouldNotDecodeBase64Data;
64
65impl std::fmt::Display for CouldNotDecodeBase64Data {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "Could not decode base64 data")
68    }
69}
70
71impl std::error::Error for CouldNotDecodeBase64Data {}
72
73impl TryFrom<&str> for Base64Data {
74    type Error = CouldNotDecodeBase64Data;
75
76    fn try_from(v: &str) -> Result<Self, Self::Error> {
77        for config in ALLOWED_DECODING_FORMATS {
78            if let Ok(data) = config.decode(v.as_bytes()) {
79                return Ok(Base64Data(data));
80            }
81        }
82
83        Err(CouldNotDecodeBase64Data)
84    }
85}
86
87struct Base64DataVisitor;
88
89impl Visitor<'_> for Base64DataVisitor {
90    type Value = Base64Data;
91
92    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
93        write!(formatter, "a base64 encoded string")
94    }
95
96    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
97    where
98        E: Error,
99    {
100        // Forgive alt base64 decoding formats
101        for config in ALLOWED_DECODING_FORMATS {
102            if let Ok(data) = config.decode(v.as_bytes()) {
103                return Ok(Base64Data(data));
104            }
105        }
106
107        Err(serde::de::Error::invalid_value(Unexpected::Str(v), &self))
108    }
109}
110
111impl<'de> Deserialize<'de> for Base64Data {
112    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
113    where
114        D: Deserializer<'de>,
115    {
116        deserializer.deserialize_str(Base64DataVisitor)
117    }
118}
119
120impl Serialize for Base64Data {
121    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
122    where
123        S: Serializer,
124    {
125        let encoded = data_encoding::BASE64URL_NOPAD.encode(&self.0);
126        serializer.serialize_str(&encoded)
127    }
128}
129
130impl schemars::JsonSchema for Base64Data {
131    fn schema_name() -> String {
132        "Base64Data".to_string()
133    }
134
135    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
136        let mut obj = gen.root_schema_for::<String>().schema;
137        // From: https://swagger.io/specification/#data-types
138        obj.format = Some("byte".to_string());
139        schemars::schema::Schema::Object(obj)
140    }
141
142    fn is_referenceable() -> bool {
143        false
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use std::convert::TryFrom;
150
151    use crate::base64::Base64Data;
152
153    #[test]
154    fn test_base64_try_from() {
155        assert!(Base64Data::try_from("aGVsbG8=").is_ok());
156        assert!(Base64Data::try_from("abcdefghij").is_err());
157    }
158}