dactyl/traits/
hex.rs

1/*!
2# Dactyl: Hex Decode Trait
3*/
4
5#![expect(
6	clippy::cast_lossless,
7	clippy::cast_possible_truncation,
8	trivial_numeric_casts,
9	reason = "Macros made me do it.",
10)]
11
12use crate::int;
13
14
15
16/// # Hex (ASCII Bytes) to Unsigned.
17///
18/// This trait exposes the method `htou` to decode an ASCII byte slice of
19/// hex values into a proper unsigned integer.
20///
21/// This method returns `None` if the slice is empty or the resulting value is
22/// too big for the type.
23///
24/// (Excessive leading zeroes are fine and will simply be stripped.)
25///
26/// For signed integers, see [`HexToSigned`].
27///
28/// ## Examples
29///
30/// ```
31/// use dactyl::traits::HexToUnsigned;
32///
33/// assert_eq!(u8::htou(b"d"),   Some(13));
34/// assert_eq!(u8::htou(b"D"),   Some(13));
35/// assert_eq!(u8::htou(b"0d"),  Some(13));
36/// assert_eq!(u8::htou(b"00D"), Some(13));
37///
38/// // These are no good.
39/// assert!(u8::htou(&[]).is_none());      // Empty.
40/// assert!(u8::htou(b"+13").is_none());   // "+"
41/// assert!(u8::htou(b" 13  ").is_none()); // Whitespace.
42/// assert!(u8::htou(b"1234").is_none());  // Too big.
43/// ```
44pub trait HexToUnsigned: Sized {
45	/// # Hex (ASCII Bytes) to Unsigned.
46	fn htou(hex: &[u8]) -> Option<Self>;
47}
48
49/// # Helper: Signed Impls.
50///
51/// Signed types use their unsigned counterpart's decoder, then are transmuted
52/// after to handle any wrapping.
53macro_rules! unsigned {
54	($($ty:ty)+) => ($(
55		impl HexToUnsigned for $ty {
56			#[inline]
57			/// # Hex (ASCII Bytes) to Unsigned.
58			fn htou(mut src: &[u8]) -> Option<Self> {
59				// Must not be empty, at least to start with.
60				if src.is_empty() { return None; }
61
62				// Trim leading zeroes.
63				while let [ b'0', rest @ .. ] = src { src = rest; }
64
65				// The result must be within twice the byte size of the
66				// primitive.
67				if size_of::<Self>() * 2 < src.len() { return None; }
68
69				// Add up the rest!
70				let mut out: Self = 0;
71				while let [ v, rest @ .. ] = src {
72					out *= 16;
73					let v = (*v as char).to_digit(16)?;
74					out += v as Self;
75					src = rest;
76				}
77
78				Some(out)
79			}
80		}
81	)+);
82}
83
84unsigned!(u8 u16 u32 u64 u128);
85
86impl HexToUnsigned for usize {
87	#[inline]
88	/// # Hex (ASCII Bytes) to Unsigned.
89	fn htou(src: &[u8]) -> Option<Self> {
90		<int!(@alias usize)>::htou(src).map(|n| n as Self)
91	}
92}
93
94
95
96/// # Hex (ASCII Bytes) to Unsigned.
97///
98/// This trait exposes the method `htou` to decode an ASCII byte slice of
99/// hex values into a proper unsigned integer.
100///
101/// This method returns `None` if the slice is empty or the resulting value is
102/// too big for the type.
103///
104/// (Excessive leading zeroes are fine and will simply be stripped.)
105///
106/// For signed integers, see [`HexToSigned`].
107///
108/// ## Examples
109///
110/// ```
111/// use dactyl::traits::HexToSigned;
112///
113/// assert_eq!(i8::htoi(b"fB"),  Some(-5));
114/// assert_eq!(i8::htoi(b"0FB"), Some(-5));
115///
116/// // These are no good.
117/// assert!(i8::htoi(&[]).is_none());      // Empty.
118/// assert!(i8::htoi(b"-fb").is_none());   // "-"
119/// assert!(i8::htoi(b" FB  ").is_none()); // Whitespace.
120/// assert!(i8::htoi(b"FFB").is_none());   // Too big.
121/// ```
122pub trait HexToSigned: Sized {
123	/// # Hex (ASCII Bytes) to Signed.
124	fn htoi(hex: &[u8]) -> Option<Self>;
125}
126
127/// # Helper: Signed Impls.
128///
129/// Signed types use their unsigned counterpart's decoder, then are transmuted
130/// after to handle any wrapping.
131macro_rules! signed {
132	($($ty:ident)+) => ($(
133		impl HexToSigned for $ty {
134			#[inline]
135			/// # Hex (ASCII Bytes) to Signed.
136			fn htoi(src: &[u8]) -> Option<Self> {
137				<int!(@flip $ty)>::htou(src).map(<int!(@flip $ty)>::cast_signed)
138			}
139		}
140	)+);
141}
142
143signed!{ i8 i16 i32 i64 i128 isize }
144
145
146
147#[cfg(test)]
148mod tests {
149	use super::*;
150
151	#[cfg(not(miri))]
152	const SAMPLE_SIZE: usize = 1_000_000;
153
154	#[cfg(miri)]
155	const SAMPLE_SIZE: usize = 500; // Miri runs way too slow for a million tests.
156
157	macro_rules! test_all {
158		($tfn:ident, $hfn:ident, $ty:ty) => (
159			#[test]
160			fn $tfn() {
161				for i in <$ty>::MIN..=<$ty>::MAX { hex!($hfn, i, $ty); }
162			}
163		);
164	}
165	macro_rules! test_rng {
166		($tfn:ident, $hfn:ident, $ty:ident) => (
167			#[test]
168			fn $tfn() {
169				// Test a reasonable random range of values.
170				let mut rng = fastrand::Rng::new();
171				for i in std::iter::repeat_with(|| rng.$ty(..)).take(SAMPLE_SIZE) {
172					hex!($hfn, i, $ty);
173				}
174
175				// Explicitly test the min, max, and zero.
176				for i in [<$ty>::MIN, 0, <$ty>::MAX] { hex!($hfn, i, $ty); }
177			}
178		);
179	}
180
181	macro_rules! hex {
182		($fn:ident, $num:ident, $ty:ty) => (
183			// Unpadded lower, upper.
184			let mut s = format!("{:x}", $num);
185			assert_eq!(<$ty>::$fn(s.as_bytes()), Some($num));
186			s.make_ascii_uppercase();
187			assert_eq!(<$ty>::$fn(s.as_bytes()), Some($num));
188
189			// Padded upper, lower.
190			let width = std::mem::size_of::<$ty>() * 2;
191			if s.len() < width {
192				while s.len() < width { s.insert(0, '0'); }
193				assert_eq!(<$ty>::$fn(s.as_bytes()), Some($num));
194				s.make_ascii_lowercase();
195				assert_eq!(<$ty>::$fn(s.as_bytes()), Some($num));
196			}
197		);
198	}
199
200	// Test full set for small types.
201	test_all!(t_u8, htou, u8);
202	#[cfg(not(miri))] test_all!(t_u16, htou, u16);
203
204	test_all!(t_i8, htoi, i8);
205	#[cfg(not(miri))] test_all!(t_i16, htoi, i16);
206
207	// Test random range for larger types.
208	#[cfg(miri)] test_rng!(t_u16, htou, u16);
209	test_rng!(t_u32, htou, u32);
210	test_rng!(t_u64, htou, u64);
211	test_rng!(t_u128, htou, u128);
212	test_rng!(t_usize, htou, usize);
213
214	#[cfg(miri)] test_rng!(t_i16, htoi, i16);
215	test_rng!(t_i32, htoi, i32);
216	test_rng!(t_i64, htoi, i64);
217	test_rng!(t_i128, htoi, i128);
218	test_rng!(t_isize, htoi, isize);
219}