serde_encoded_bytes/encoding/
hex.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use alloc::{format, string::String, vec::Vec};

use serde::de;

use super::Encoding;

/// Encodes the byte sequence into a `0x`-prefixed hexadecimal representation.
pub struct Hex;

impl Encoding for Hex {
    fn encode(bytes: &[u8]) -> String {
        format!("0x{}", hex::encode(bytes))
    }

    fn decode<E: de::Error>(string: &str) -> Result<Vec<u8>, E> {
        let digits = string.strip_prefix("0x").ok_or_else(|| {
            de::Error::invalid_value(
                de::Unexpected::Str(string),
                &"0x-prefixed hex-encoded bytes",
            )
        })?;
        hex::decode(digits).map_err(de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use alloc::string::{String, ToString};

    use serde::{Deserialize, Serialize};

    use super::Hex;
    use crate::ArrayLike;

    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
    struct ArrayStruct(#[serde(with = "ArrayLike::<Hex>")] [u8; 4]);

    fn hr_serialize<T: Serialize>(value: T) -> Result<String, String> {
        serde_json::to_string(&value).map_err(|err| err.to_string())
    }

    fn hr_deserialize<'de, T: Deserialize<'de>>(string: &'de str) -> Result<T, String> {
        serde_json::from_str::<T>(string).map_err(|err| err.to_string())
    }

    #[test]
    fn roundtrip() {
        let val = ArrayStruct([1, 0xf2, 3, 0xf4]);

        let val_str = hr_serialize(&val).unwrap();
        assert_eq!(val_str, "\"0x01f203f4\"");
        let val_back = hr_deserialize::<ArrayStruct>(&val_str).unwrap();
        assert_eq!(val, val_back);
    }

    #[test]
    fn errors() {
        assert_eq!(
            hr_deserialize::<ArrayStruct>("\"01f203f4\"").unwrap_err(),
            concat![
                "invalid value: string \"01f203f4\", expected 0x-prefixed ",
                "hex-encoded bytes at line 1 column 10"
            ]
        );
        assert_eq!(
            hr_deserialize::<ArrayStruct>("\"0\"").unwrap_err(),
            "invalid value: string \"0\", expected 0x-prefixed hex-encoded bytes at line 1 column 3"
        );
    }

    #[test]
    fn multi_byte_character() {
        // A regression test for a bug in validating a possible hex string
        // that could lead to a panic.
        // `str::get()` takes an index in bytes, but requires it to be aligned
        // to the end of a character, so it can fail for multi-byte characters
        // even if the index is technically lower than `str::len()`.

        // This is a unicode "AE" with a 3-byte UTF-8 encoding (0xE1 0xB4 0x81)
        assert_eq!(
            hr_deserialize::<ArrayStruct>("\"\u{1D01}\"").unwrap_err(),
            concat![
                "invalid value: string \"\u{1D01}\", expected 0x-prefixed ",
                "hex-encoded bytes at line 1 column 5"
            ]
        );
    }
}