irox_tools/
hex.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5//!
6//! Hexdump & Hex manipulation
7
8crate::cfg_feature_alloc! {
9    extern crate alloc;
10}
11use crate::buf::{FixedU8Buf, StrBuf};
12use crate::cfg_feature_alloc;
13use core::fmt::Write;
14use irox_bits::{BitsError, BitsErrorKind, Error, ErrorKind, FormatBits, MutBits};
15
16/// 0-9, A-F
17pub static HEX_UPPER_CHARS: [char; 16] = [
18    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
19];
20/// 0-9, a-f
21pub static HEX_LOWER_CHARS: [char; 16] = [
22    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
23];
24
25///
26/// Dumps the contents of this data structure in a pretty 16 slot wide format, like the output of
27/// `hexdump -C`
28pub trait HexDump {
29    crate::cfg_feature_std! {
30        /// Hexdump this data structure to stdout
31        fn hexdump(&self);
32    }
33
34    /// Hexdump to the specified writer.
35    fn hexdump_to<T: MutBits + ?Sized>(&self, out: &mut T) -> Result<(), Error>;
36}
37
38impl<S: AsRef<[u8]>> HexDump for S {
39    crate::cfg_feature_std! {
40        fn hexdump(&self) {
41            let _ = self.hexdump_to(&mut irox_bits::BitsWrapper::Borrowed(&mut std::io::stdout().lock()));
42        }
43    }
44
45    fn hexdump_to<T: MutBits + ?Sized>(&self, out: &mut T) -> Result<(), Error> {
46        let mut idx = 0;
47        let chunks = self.as_ref().chunks(16);
48        let mut out: FormatBits<T> = out.into();
49        for chunk in chunks {
50            write!(out, "{idx:08X}  ")?;
51            for v in chunk {
52                write!(out, "{v:02X} ")?;
53            }
54            for _i in 0..(16 - chunk.len()) {
55                write!(out, "   ")?;
56            }
57            write!(out, " |")?;
58            for v in chunk {
59                match *v {
60                    0..=0x1F | 0x7F..=0xA0 | 0xFF => {
61                        // nonprintables
62                        write!(out, ".")?;
63                    }
64                    p => {
65                        // printables
66                        write!(out, "{}", p as char)?;
67                    }
68                }
69            }
70            for _i in 0..(16 - chunk.len()) {
71                write!(out, " ")?;
72            }
73            writeln!(out, "|")?;
74            idx += 16;
75        }
76        Ok(())
77    }
78}
79cfg_feature_alloc! {
80    /// Prints the values in the slice as a static rust-type array
81    pub fn to_hex_array(value: &[u8]) -> alloc::string::String {
82        let mut out = alloc::vec::Vec::new();
83        for v in value {
84            out.push(format!("0x{:02X}", v));
85        }
86        let joined = out.join(",");
87
88        format!("[{joined}]")
89    }
90}
91
92pub const fn hex_char_to_nibble(ch: char) -> Result<u8, Error> {
93    Ok(match ch {
94        '0' => 0,
95        '1' => 1,
96        '2' => 2,
97        '3' => 3,
98        '4' => 4,
99        '5' => 5,
100        '6' => 6,
101        '7' => 7,
102        '8' => 8,
103        '9' => 9,
104        'a' | 'A' => 0xA,
105        'b' | 'B' => 0xB,
106        'c' | 'C' => 0xC,
107        'd' | 'D' => 0xD,
108        'e' | 'E' => 0xE,
109        'f' | 'F' => 0xF,
110        _ => return ErrorKind::InvalidData.err("Invalid hex character"),
111    })
112}
113/// Static equivalent of `format!("{:X}", val);`
114pub const fn nibble_to_hex_char(val: u8) -> Result<char, Error> {
115    Ok(match val {
116        0x0 => '0',
117        0x1 => '1',
118        0x2 => '2',
119        0x3 => '3',
120        0x4 => '4',
121        0x5 => '5',
122        0x6 => '6',
123        0x7 => '7',
124        0x8 => '8',
125        0x9 => '9',
126        0xA => 'A',
127        0xB => 'B',
128        0xC => 'C',
129        0xD => 'D',
130        0xE => 'E',
131        0xF => 'F',
132        _ => return ErrorKind::InvalidData.err("Invalid hex character"),
133    })
134}
135
136crate::cfg_feature_alloc! {
137    ///
138    /// Parses the provided string, a series of hex characters [a-fA-F0-9] and converts them to the
139    /// associated byte format.
140    pub fn from_hex_str(hex: &str) -> Result<alloc::boxed::Box<[u8]>, Error> {
141        let len = hex.len();
142        let mut out = alloc::vec::Vec::with_capacity(len * 2);
143
144        let mut val = 0u8;
145        let mut idx = 0;
146        for ch in hex.chars() {
147            if ch == ' ' {
148                continue;
149            }
150            let ch = hex_char_to_nibble(ch)?;
151            if idx & 0x1 == 0 {
152                val |= (ch << 4) & 0xF0;
153            } else {
154                val |= ch & 0xF;
155                out.push(val);
156                val = 0;
157            }
158            idx += 1;
159        }
160
161        Ok(out.into_boxed_slice())
162    }
163}
164
165///
166/// Parses the provided string, a series of hex characters [a-fA-F0-9] and converts them to the
167/// associated byte format.  Returns the number of bytes written.
168pub fn from_hex_into<T: MutBits>(hex: &str, out: &mut T) -> Result<usize, Error> {
169    let mut val = 0u8;
170    let mut idx = 0;
171    let mut wrote = 0;
172    for ch in hex.chars() {
173        if ch == ' ' {
174            continue;
175        }
176        let ch = hex_char_to_nibble(ch)?;
177        if idx & 0x1 == 0 {
178            val |= (ch << 4) & 0xF0;
179        } else {
180            val |= ch & 0xF;
181            out.write_u8(val)?;
182            wrote += 1;
183            val = 0;
184        }
185        idx += 1;
186    }
187
188    Ok(wrote)
189}
190
191///
192/// Attempts to fill a static array buffer with the hex data within the provided string,
193/// returns the buffer if the string and buffer are perfectly matched.
194pub fn try_from_hex_str<const N: usize>(str: &str) -> Result<[u8; N], BitsError> {
195    let mut buf = FixedU8Buf::<N>::new();
196    if from_hex_into(str, &mut buf)? != N {
197        return Err(BitsErrorKind::UnexpectedEof.into());
198    }
199    Ok(buf.take())
200}
201
202crate::cfg_feature_alloc! {
203    ///
204    /// Prints the value to a uppercase hex string
205    pub fn to_hex_str_upper(val: &[u8]) -> alloc::string::String {
206        let len = val.len() * 2;
207        let mut out = alloc::string::String::with_capacity(len);
208
209        for v in val {
210            let _ = write!(&mut out, "{v:02X}");
211        }
212
213        out
214    }
215}
216
217crate::cfg_feature_alloc! {
218    ///
219    /// Prints the value to a lowercase hex string
220    pub fn to_hex_str_lower(val: &[u8]) -> alloc::string::String {
221        let len = val.len() * 2;
222        let mut out = alloc::string::String::with_capacity(len);
223
224        for v in val {
225            let _ = write!(&mut out, "{v:02x}");
226        }
227
228        out
229    }
230}
231
232///
233/// Prints the value to a lowercase hex string and stores it in the provided
234/// [`StrBuf`].  The size of the StrBuf must be `>= 2x val.len()`
235pub fn to_hex_strbuf_lower<const N: usize>(val: &[u8], buf: &mut StrBuf<N>) -> Result<(), Error> {
236    let len = val.len() * 2;
237    if N < len {
238        return Err(ErrorKind::UnexpectedEof.into());
239    }
240    for v in val {
241        write!(buf, "{v:02x}")?;
242    }
243
244    Ok(())
245}
246
247///
248/// Prints the value to a uppercase hex string and stores it in the provided
249/// [`StrBuf`].  The size of the StrBuf must be `>= 2x val.len()`
250pub fn to_hex_strbuf_upper<const N: usize>(val: &[u8], buf: &mut StrBuf<N>) -> Result<(), Error> {
251    let len = val.len() * 2;
252    if N < len {
253        return Err(ErrorKind::UnexpectedEof.into());
254    }
255    for v in val {
256        write!(buf, "{v:02X}")?;
257    }
258
259    Ok(())
260}
261
262#[doc(hidden)]
263#[allow(clippy::indexing_slicing)]
264pub const fn hex_len(vals: &[&[u8]]) -> Option<usize> {
265    let mut out = 0;
266    let mut idx = 0;
267    while idx < vals.len() {
268        let val = vals[idx];
269        let len = val.len();
270
271        out += len;
272        idx += 1;
273    }
274    if out & 0x01 == 0x01 {
275        None
276    } else {
277        Some(out / 2)
278    }
279}
280#[doc(hidden)]
281#[allow(clippy::indexing_slicing)]
282pub const fn raw_hex<const L: usize>(vals: &[&[u8]]) -> Result<[u8; L], char> {
283    let mut out = [0u8; L];
284    let mut outidx = 0;
285    let mut idx = 0;
286    while idx < vals.len() {
287        let val = vals[idx];
288        let mut inneridx = 0;
289        while inneridx < val.len() {
290            let a = val[inneridx] as char;
291            let Ok(a) = hex_char_to_nibble(a) else {
292                return Err(a);
293            };
294            inneridx += 1;
295            let b = val[inneridx] as char;
296            let Ok(b) = hex_char_to_nibble(b) else {
297                return Err(b);
298            };
299            inneridx += 1;
300            out[outidx] = (a << 4) | b;
301            outidx += 1;
302        }
303        idx += 1;
304    }
305    Ok(out)
306}
307
308#[allow(unused_macros)]
309#[macro_export]
310///
311/// Const compile-time evaluation of the provided string literals
312/// ```
313/// let raw_hex = irox_tools::hex!("C0ffee" "BeEf");
314//  assert_eq_hex_slice!(&[0xc0, 0xff, 0xee, 0xbe, 0xef] as &[u8], &raw_hex);
315/// ```
316macro_rules! hex {
317    ($($input:literal)+) => {{
318        const VALS: &[& 'static [u8]] = &[$($input.as_bytes(),)*];
319        const LEN: usize = match $crate::hex::hex_len(VALS) {
320            Some(v) => v,
321            None => panic!("Hex string is an odd length")
322        };
323        const RTN: [u8;LEN] = match $crate::hex::raw_hex::<LEN>(VALS) {
324            Ok(v) => v,
325            Err(_) => panic!("Hex string contains invalid character")
326        };
327        RTN
328    }};
329}
330
331#[cfg(test)]
332#[cfg(feature = "std")]
333mod tests {
334    extern crate alloc;
335    use crate::hex::HexDump;
336    use alloc::vec::Vec;
337
338    #[test]
339    pub fn test() -> Result<(), irox_bits::Error> {
340        let mut buf: Vec<u8> = Vec::new();
341        for v in u8::MIN..=u8::MAX {
342            buf.push(v);
343        }
344
345        buf.hexdump();
346
347        Ok(())
348    }
349
350    #[test]
351    pub fn const_hex_test() -> Result<(), irox_bits::Error> {
352        let raw_hex = hex!("");
353        assert_eq_hex_slice!(&[] as &[u8], &raw_hex);
354        let raw_hex = hex!("00");
355        assert_eq_hex_slice!(&[0x0u8], &raw_hex);
356        raw_hex.hexdump();
357        Ok(())
358    }
359}