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