1#![doc(html_logo_url = "https://raw.githubusercontent.com/mitsuhiko/deser/main/artwork/logo.svg")]
62
63use std::sync::LazyLock;
64
65use data_encoding::Encoding;
66use deser_core::adapters::BytesEncoding;
67use deser_core::{Error, ErrorKind};
68
69fn decode(encoding: &Encoding, name: &str, s: &str) -> Result<Vec<u8>, Error> {
70 encoding.decode(s.as_bytes()).map_err(|err| {
71 Error::new(
72 ErrorKind::InvalidValue,
73 format!("invalid {} string: {}", name, err),
74 )
75 })
76}
77
78fn case_insensitive(encoding: &Encoding) -> Encoding {
80 let mut spec = encoding.specification();
81 let upper: String = spec
82 .symbols
83 .chars()
84 .filter(char::is_ascii_uppercase)
85 .collect();
86 spec.translate.from.push_str(&upper.to_ascii_lowercase());
87 spec.translate.to.push_str(&upper);
88 spec.encoding().expect("valid case insensitive encoding")
89}
90
91macro_rules! encoding {
92 ($(#[$meta:meta])* $ty:ident, $name:expr, $encoding:ident) => {
93 encoding!($(#[$meta])* $ty, $name, $encoding, &data_encoding::$encoding);
94 };
95 ($(#[$meta:meta])* $ty:ident, $name:expr, $encoding:ident, case_insensitive) => {
96 encoding!(
97 $(#[$meta])* $ty,
98 $name,
99 $encoding,
100 {
101 static DECODER: LazyLock<Encoding> =
102 LazyLock::new(|| case_insensitive(&data_encoding::$encoding));
103 &*DECODER
104 }
105 );
106 };
107 ($(#[$meta:meta])* $ty:ident, $name:expr, $encoding:ident, $decoder:expr) => {
108 $(#[$meta])*
109 pub struct $ty;
110
111 impl BytesEncoding for $ty {
112 const NAME: &'static str = $name;
113
114 fn encode(bytes: &[u8], out: &mut String) {
115 data_encoding::$encoding.encode_append(bytes, out);
116 }
117
118 fn decode(s: &str) -> Result<Vec<u8>, Error> {
119 decode($decoder, Self::NAME, s)
120 }
121 }
122 };
123}
124
125encoding!(
126 Hex,
130 "hex",
131 HEXLOWER_PERMISSIVE
132);
133
134encoding!(
135 HexUpper,
139 "hex-upper",
140 HEXUPPER_PERMISSIVE
141);
142
143encoding!(
144 Base32,
148 "base32",
149 BASE32,
150 case_insensitive
151);
152
153encoding!(
154 Base32NoPad,
158 "base32-nopad",
159 BASE32_NOPAD,
160 case_insensitive
161);
162
163encoding!(
164 Base32Hex,
169 "base32hex",
170 BASE32HEX,
171 case_insensitive
172);
173
174encoding!(
175 Base32HexNoPad,
179 "base32hex-nopad",
180 BASE32HEX_NOPAD,
181 case_insensitive
182);
183
184encoding!(
185 Base32Dnssec,
191 "base32-dnssec",
192 BASE32_DNSSEC
193);
194
195#[cfg(test)]
196mod tests {
197 use deser::adapters::{As, BytesFallback};
198 use deser::{BytesFormat, Deserialize, Serialize};
199
200 use super::*;
201
202 fn encode<E: BytesEncoding>(bytes: &[u8]) -> String {
203 let mut rv = String::from(">");
204 E::encode(bytes, &mut rv);
205 rv[1..].to_string()
206 }
207
208 #[test]
209 fn test_hex() {
210 assert_eq!(encode::<Hex>(b"\x00\x1f\xab"), "001fab");
211 assert_eq!(encode::<HexUpper>(b"\x00\x1f\xab"), "001FAB");
212 assert_eq!(encode::<Hex>(b""), "");
213 for decode in [Hex::decode, HexUpper::decode] {
214 assert_eq!(decode("001fAB").unwrap(), b"\x00\x1f\xab");
215 assert_eq!(decode("09afAF").unwrap(), b"\x09\xaf\xaf");
216 assert_eq!(decode("").unwrap(), b"");
217 for invalid in ["0", "0g", "0/", "0:", "@0", "G0", "0`", "g0", "\u{ff}"] {
218 assert!(decode(invalid).is_err(), "{invalid:?}");
219 }
220 }
221 assert_eq!(
222 Hex::decode("zz").unwrap_err().to_string(),
223 "InvalidValue: invalid hex string: invalid symbol at 0"
224 );
225 }
226
227 #[test]
228 fn test_base32() {
229 assert_eq!(encode::<Base32>(b"foo"), "MZXW6===");
230 assert_eq!(encode::<Base32NoPad>(b"foo"), "MZXW6");
231 assert_eq!(encode::<Base32Hex>(b"foo"), "CPNMU===");
232 assert_eq!(encode::<Base32HexNoPad>(b"foo"), "CPNMU");
233 assert_eq!(Base32::decode("MZXW6===").unwrap(), b"foo");
234 assert_eq!(Base32NoPad::decode("MZXW6").unwrap(), b"foo");
235 assert_eq!(Base32Hex::decode("CPNMU===").unwrap(), b"foo");
236 assert_eq!(Base32HexNoPad::decode("CPNMU").unwrap(), b"foo");
237 assert_eq!(Base32::decode("mzXw6===").unwrap(), b"foo");
238 assert_eq!(Base32NoPad::decode("mzXw6").unwrap(), b"foo");
239 assert_eq!(Base32Hex::decode("cpNmu===").unwrap(), b"foo");
240 assert_eq!(Base32HexNoPad::decode("cpNmu").unwrap(), b"foo");
241 assert!(Base32Hex::decode("W0======").is_err());
243 assert!(Base32Hex::decode("w0======").is_err());
244 assert!(Base32::decode("MZXW6").is_err());
245 assert!(Base32NoPad::decode("MZXW6===").is_err());
246 assert_eq!(
247 Base32::decode("x").unwrap_err().to_string(),
248 "InvalidValue: invalid base32 string: invalid length at 0"
249 );
250 }
251
252 #[test]
253 fn test_base32_dnssec() {
254 assert_eq!(encode::<Base32Dnssec>(b"foo"), "cpnmu");
255 assert_eq!(Base32Dnssec::decode("cpnmu").unwrap(), b"foo");
256 assert_eq!(Base32Dnssec::decode("CPNmu").unwrap(), b"foo");
257 assert!(Base32Dnssec::decode("cpnmu===").is_err());
258 let hash = Hex::decode("065368abeed7ec6e9feba96b8c8bc3e8b791f716").unwrap();
260 assert_eq!(
261 encode::<Base32Dnssec>(&hash),
262 "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom"
263 );
264 }
265
266 #[derive(Debug, PartialEq, Serialize, Deserialize)]
267 struct Blob {
268 #[deser(as = Hex)]
269 forced: [u8; 2],
270 #[deser(as = BytesFallback<Hex>)]
271 fallback: Vec<u8>,
272 #[deser(as = Option<BytesFallback<Base32>>)]
273 optional: Option<Vec<u8>>,
274 }
275
276 fn blob() -> Blob {
277 Blob {
278 forced: [1, 255],
279 fallback: vec![2, 254],
280 optional: Some(b"foo".to_vec()),
281 }
282 }
283
284 #[test]
285 fn test_adapters() {
286 let json = deser_json::to_string(&blob()).unwrap();
287 assert_eq!(
288 json,
289 r#"{"forced":"01ff","fallback":"02fe","optional":"MZXW6==="}"#
290 );
291 assert_eq!(deser_json::from_str::<Blob>(&json).unwrap(), blob());
292 let upper = r#"{"forced":"01FF","fallback":"02FE","optional":"MZXW6==="}"#;
293 assert_eq!(deser_json::from_str::<Blob>(upper).unwrap(), blob());
294
295 let cbor = deser_cbor::to_vec(&blob()).unwrap();
297 assert!(cbor.windows(4).any(|x| x == b"01ff"));
298 assert!(!cbor.windows(4).any(|x| x == b"02fe"));
299 assert_eq!(deser_cbor::from_slice::<Blob>(&cbor).unwrap(), blob());
300
301 let err = deser_json::from_str::<As<[u8; 2], Hex>>(r#""01""#).unwrap_err();
302 assert_eq!(err.kind(), ErrorKind::WrongLength);
303 let err = deser_json::from_str::<As<Vec<u8>, Hex>>("1").unwrap_err();
304 assert!(
305 err.to_string()
306 .contains("unexpected unsigned integer, expected bytes or hex string"),
307 "{err}"
308 );
309 }
310
311 #[test]
312 fn test_bytes_format() {
313 const HEX: BytesFormat = BytesFormat::encoded::<Hex>();
314 assert_eq!(HEX.name(), "hex");
315 assert_ne!(HEX, BytesFormat::encoded::<HexUpper>());
316 assert_eq!(HEX.encode(b"\x01\xff").as_deref(), Some("01ff"));
317 assert_eq!(HEX.decode("01FF").unwrap(), b"\x01\xff");
318
319 let context = deser::Context::with(HEX);
320 assert_eq!(
321 deser_json::Deserializer::from_str(r#""01ff""#)
322 .deserialize_with::<Vec<u8>, _>(|driver| driver.set_context(context.clone()))
323 .unwrap(),
324 [1, 255]
325 );
326 }
327}