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