forest/utils/encoding/
hex.rs1use anyhow::Context as _;
10
11#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
12#[error(transparent)]
13pub struct DecodeError(#[from] faster_hex_private::Error);
14
15pub fn encode(data: impl AsRef<[u8]>) -> String {
17 faster_hex_private::hex_string(data.as_ref())
18}
19
20pub fn encode_prefixed(data: impl AsRef<[u8]>) -> String {
22 let data = data.as_ref();
23 let mut buf = vec![0; 2 + data.len() * 2];
24 let (prefix, digits) = buf.split_at_mut(2);
25 prefix.copy_from_slice(b"0x");
26 faster_hex_private::hex_encode(data, digits).expect("output buffer is sized to fit");
27 debug_assert!(buf.is_ascii());
28 unsafe { String::from_utf8_unchecked(buf) }
30}
31
32pub fn parse_prefixed_int<T>(input: &str) -> anyhow::Result<T>
36where
37 T: num_traits::Num,
38 <T as num_traits::Num>::FromStrRadixErr: std::fmt::Display,
39{
40 let digits = input
41 .strip_prefix("0x")
42 .with_context(|| format!("not a 0x-prefixed hex integer: {input}"))?;
43 anyhow::ensure!(
44 !digits.starts_with(['+', '-']),
45 "signed hex integer: {input}"
46 );
47 T::from_str_radix(digits, 16).map_err(|e| anyhow::anyhow!("invalid hex integer {input}: {e}"))
48}
49
50pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
52 let input = input.as_ref();
53 let mut out = vec![0; input.len() / 2];
54 faster_hex_private::hex_decode(input, &mut out)?;
55 Ok(out)
56}
57
58pub mod serde {
61 use serde::{Deserialize as _, Deserializer, Serializer, de};
62
63 pub fn serialize<S: Serializer>(
64 data: impl AsRef<[u8]>,
65 serializer: S,
66 ) -> Result<S::Ok, S::Error> {
67 serializer.serialize_str(&super::encode(data))
68 }
69
70 pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
71 where
72 D: Deserializer<'de>,
73 T: TryFrom<Vec<u8>>,
74 {
75 let s = String::deserialize(deserializer)?;
76 let bytes = super::decode(&s).map_err(de::Error::custom)?;
77 let len = bytes.len();
78 T::try_from(bytes).map_err(|_| de::Error::custom(format!("invalid length {len}")))
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use quickcheck_macros::quickcheck;
86 use rstest::rstest;
87
88 #[quickcheck]
89 fn encode_matches_hex_crate(data: Vec<u8>) -> bool {
90 encode(&data) == ::hex::encode(&data)
91 }
92
93 #[quickcheck]
94 fn encode_prefixed_is_ascii_with_prefix(data: Vec<u8>) -> bool {
95 let s = encode_prefixed(&data);
96 s.is_ascii() && s.strip_prefix("0x") == Some(encode(&data).as_str())
97 }
98
99 #[quickcheck]
102 fn decode_matches_hex_crate(data: Vec<u8>, junk: String) -> bool {
103 [::hex::encode(&data), junk]
104 .iter()
105 .all(|s| match (decode(s), ::hex::decode(s)) {
106 (Ok(ours), Ok(theirs)) => ours == theirs,
107 (Err(_), Err(_)) => true,
108 _ => false,
109 })
110 }
111
112 #[quickcheck]
113 fn decode_roundtrip_any_case(data: Vec<u8>, flips: Vec<bool>) -> bool {
114 let s: String = encode(&data)
115 .chars()
116 .zip(flips.into_iter().chain(std::iter::repeat(false)))
117 .map(|(c, up)| if up { c.to_ascii_uppercase() } else { c })
118 .collect();
119 decode(&s).unwrap() == data
120 }
121
122 #[quickcheck]
123 fn decode_no_panic(input: Vec<u8>) {
124 let _ = decode(&input);
125 }
126
127 #[quickcheck]
128 fn serde_matches_hex_crate(data: Vec<u8>) -> bool {
129 #[derive(::serde::Serialize, ::serde::Deserialize)]
130 struct Ours(#[serde(with = "crate::utils::encoding::hex::serde")] Vec<u8>);
131 #[derive(::serde::Serialize)]
132 struct Theirs(#[serde(with = "::hex::serde")] Vec<u8>);
133
134 let ours = serde_json::to_string(&Ours(data.clone())).unwrap();
135 ours == serde_json::to_string(&Theirs(data.clone())).unwrap()
136 && serde_json::from_str::<Ours>(&ours).unwrap().0 == data
137 }
138
139 #[test]
140 fn encode_matches_expectations() {
141 assert_eq!(encode([]), "");
142 assert_eq!(encode([0x00, 0xab, 0xff]), "00abff");
143 assert_eq!(encode_prefixed([]), "0x");
144 assert_eq!(encode_prefixed([0x00, 0xab, 0xff]), "0x00abff");
145 }
146
147 #[test]
148 fn decode_accepts_mixed_case() {
149 assert_eq!(decode("").unwrap(), Vec::<u8>::new());
150 assert_eq!(decode("00abff").unwrap(), [0x00, 0xab, 0xff]);
151 assert_eq!(decode("00AbFF").unwrap(), [0x00, 0xab, 0xff]);
152 }
153
154 #[test]
155 fn decode_rejects_invalid_input() {
156 for invalid in ["abc", "00gg", "0x00"] {
157 assert!(decode(invalid).is_err(), "{invalid:?} should be rejected");
158 }
159 }
160
161 #[rstest]
162 #[case("0x0", 0)]
163 #[case("0x1a", 26)]
164 #[case("0x1A", 26)]
165 fn parse_prefixed_int_accepts(#[case] input: &str, #[case] expected: u64) {
166 assert_eq!(parse_prefixed_int::<u64>(input).unwrap(), expected);
167 }
168
169 #[rstest]
170 #[case("")]
171 #[case("0")]
172 #[case("1a")]
173 #[case("0x")]
174 #[case("0X1a")]
175 #[case("0xg")]
176 #[case(" 0x1")]
177 #[case("0x1 ")]
178 #[case("0é")]
180 #[case("0x\u{e9}")]
181 #[case("0x-1")]
182 #[case("0x+1")]
183 #[case("0x10000000000000000")]
184 fn parse_prefixed_int_rejects(#[case] input: &str) {
185 assert!(parse_prefixed_int::<u64>(input).is_err());
186 assert!(parse_prefixed_int::<i64>(input).is_err());
187 }
188
189 #[test]
190 fn parse_prefixed_int_is_bounded_by_target_type() {
191 assert_eq!(
192 parse_prefixed_int::<u64>("0x8000000000000000").unwrap(),
193 1 << 63
194 );
195 assert!(parse_prefixed_int::<i64>("0x8000000000000000").is_err());
196 }
197
198 #[quickcheck]
199 fn parse_prefixed_int_no_panic(input: String) {
200 for candidate in [input.clone(), format!("0x{input}")] {
201 let _ = parse_prefixed_int::<u64>(&candidate);
202 let _ = parse_prefixed_int::<i64>(&candidate);
203 }
204 }
205}