Skip to main content

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