Skip to main content

deser_encoding/
lib.rs

1//! More encodings of bytes as strings for [deser](https://docs.rs/deser).
2//!
3//! deser writes bytes as base64 strings in formats without native bytes (like
4//! JSON) and provides the base64 encodings (see
5//! [bytes in deser](deser_core::adapters#bytes)).  This crate adds hexadecimal and
6//! base32:
7//!
8//! | Encoding           | Description                                        |
9//! |--------------------|----------------------------------------------------|
10//! | [`Hex`]            | hexadecimal, lowercase                             |
11//! | [`HexUpper`]       | hexadecimal, uppercase                             |
12//! | [`Base32`]         | base32 with padding                                |
13//! | [`Base32NoPad`]    | base32 without padding                             |
14//! | [`Base32Hex`]      | base32 with extended hex alphabet and padding      |
15//! | [`Base32HexNoPad`] | base32 with extended hex alphabet without padding  |
16//! | [`Base32Dnssec`]   | base32 of DNSSEC, lowercase extended hex alphabet  |
17//!
18//! Like the encodings of deser they are adapters which represent bytes as
19//! strings in all formats.  They can be used with
20//! [`BytesFallback`](deser_core::adapters::BytesFallback) to keep native bytes in
21//! formats that have them, and with
22//! [`BytesFormat`](deser_core::BytesFormat) to configure formats:
23//!
24//! ```
25//! use deser::adapters::BytesFallback;
26//! use deser::{BytesFormat, Deserialize, Serialize};
27//! use deser_encoding::Hex;
28//!
29//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
30//! pub struct Blob {
31//!     // a hex string in all formats
32//!     #[deser(as = Hex)]
33//!     digest: [u8; 4],
34//!     // hex in JSON, bytes in CBOR
35//!     #[deser(as = BytesFallback<Hex>)]
36//!     signature: Vec<u8>,
37//!     // base64 in JSON, bytes in CBOR
38//!     data: Vec<u8>,
39//! }
40//!
41//! let blob = Blob {
42//!     digest: [0xde, 0xad, 0xbe, 0xef],
43//!     signature: vec![1, 255],
44//!     data: vec![1, 255],
45//! };
46//! let json = deser_json::to_string(&blob).unwrap();
47//! assert_eq!(
48//!     json,
49//!     r#"{"digest":"deadbeef","signature":"01ff","data":"Af8="}"#
50//! );
51//! assert_eq!(deser_json::from_str::<Blob>(&json).unwrap(), blob);
52//!
53//! // all bytes as hex (in the context of the serialization)
54//! let hex = deser_json::SerializerConfig::builder()
55//!     .context(deser::Context::with(BytesFormat::encoded::<Hex>()))
56//!     .build();
57//! assert_eq!(hex.to_string(&b"\x01\xff").unwrap(), r#""01ff""#);
58//! ```
59//!
60//! All encodings decode lowercase and uppercase letters.
61#![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
78/// Returns the encoding which also decodes lowercase symbols.
79fn 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    /// Hexadecimal with lowercase digits.
127    ///
128    /// Lowercase and uppercase digits are accepted when decoding.
129    Hex,
130    "hex",
131    HEXLOWER_PERMISSIVE
132);
133
134encoding!(
135    /// Hexadecimal with uppercase digits.
136    ///
137    /// Lowercase and uppercase digits are accepted when decoding.
138    HexUpper,
139    "hex-upper",
140    HEXUPPER_PERMISSIVE
141);
142
143encoding!(
144    /// Base32 with padding (RFC 4648 section 6).
145    ///
146    /// Uppercase and lowercase letters are accepted when decoding.
147    Base32,
148    "base32",
149    BASE32,
150    case_insensitive
151);
152
153encoding!(
154    /// Base32 without padding.
155    ///
156    /// Uppercase and lowercase letters are accepted when decoding.
157    Base32NoPad,
158    "base32-nopad",
159    BASE32_NOPAD,
160    case_insensitive
161);
162
163encoding!(
164    /// Base32 with the extended hex alphabet and padding (RFC 4648
165    /// section 7).
166    ///
167    /// Uppercase and lowercase letters are accepted when decoding.
168    Base32Hex,
169    "base32hex",
170    BASE32HEX,
171    case_insensitive
172);
173
174encoding!(
175    /// Base32 with the extended hex alphabet without padding.
176    ///
177    /// Uppercase and lowercase letters are accepted when decoding.
178    Base32HexNoPad,
179    "base32hex-nopad",
180    BASE32HEX_NOPAD,
181    case_insensitive
182);
183
184encoding!(
185    /// Base32 of DNSSEC (RFC 5155 section 3.3): the extended hex alphabet
186    /// with lowercase letters and without padding.
187    ///
188    /// This is the encoding of hashed owner names in NSEC3 records.
189    /// Lowercase and uppercase letters are accepted when decoding.
190    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        // letters beyond the alphabet stay invalid in either case
242        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        // RFC 5155 appendix A: the hashed owner name of `example`
259        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        // native bytes in CBOR, except for the forced string
296        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}