Skip to main content

wtx/de/
hex.rs

1use core::fmt::{Display, Formatter};
2
3const LOWER_HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
4const UPPER_HEX_CHARS: &[u8; 16] = b"0123456789ABCDEF";
5
6/// Hex Encode Mode
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub enum HexEncMode {
9  /// <https://eips.ethereum.org/EIPS/eip-55>
10  #[cfg(feature = "sha3")]
11  Eip55,
12  /// Lower case characters ***with*** a `0x` prefix
13  WithPrefixLower,
14  /// Upper case characters ***with*** a `0x` prefix
15  WithPrefixUpper,
16  /// Lower case characters ***without*** a `0x` prefix
17  WithoutPrefixLower,
18  /// Upper case characters ***without*** a `0x` prefix
19  WithoutPrefixUpper,
20}
21
22/// Errors of hexadecimal operations
23#[derive(Debug)]
24pub enum HexError {
25  /// Provided buffer is too small
26  InsufficientBuffer,
27  /// Eip55 encoding only supports input data lesser or equal to 32 bytes
28  #[cfg(feature = "sha3")]
29  InvalidEip55Input,
30  /// Provided element is not a valid hex character
31  InvalidHexCharacter,
32  /// Provided data is not multiple of two
33  OddLen,
34}
35
36/// Auxiliary structure that will always output hexadecimal characters when displayed.
37#[derive(Debug)]
38pub struct HexDisplay<'bytes>(
39  /// Bytes.
40  pub &'bytes [u8],
41  /// See [`HexEncMode`].
42  ///
43  /// Defaults to [`HexEncMode::WithoutPrefixLower`] if `None`.
44  pub Option<HexEncMode>,
45);
46
47impl Display for HexDisplay<'_> {
48  #[inline]
49  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
50    let actual_mode = actual_mode(self.1);
51    let table = match actual_mode {
52      #[cfg(feature = "sha3")]
53      HexEncMode::Eip55 => return Err(core::fmt::Error),
54      HexEncMode::WithPrefixLower | HexEncMode::WithoutPrefixLower => LOWER_HEX_CHARS,
55      HexEncMode::WithPrefixUpper | HexEncMode::WithoutPrefixUpper => UPPER_HEX_CHARS,
56    };
57    if matches!(actual_mode, HexEncMode::WithPrefixLower | HexEncMode::WithPrefixUpper) {
58      write!(f, "0x")?;
59    }
60    for byte in self.0 {
61      let (lhs, rhs) = byte_to_hex(*byte, table);
62      write!(f, "{}{}", char::from(lhs), char::from(rhs))?;
63    }
64    Ok(())
65  }
66}
67
68/// Decodes `data` into `out` returning the affected part.
69#[inline]
70pub fn decode_hex<'out>(mut data: &[u8], out: &'out mut [u8]) -> crate::Result<&'out mut [u8]> {
71  data = if let [b'0', b'x' | b'X', rest @ ..] = data { rest } else { data };
72  let bytes_len = data.len() / 2;
73  let Some(actual_out) = out.get_mut(..bytes_len) else {
74    return Err(HexError::InsufficientBuffer.into());
75  };
76  let (arrays, rem) = data.as_chunks::<2>();
77  if !rem.is_empty() {
78    return Err(HexError::OddLen.into());
79  }
80  for ([a, b], byte) in arrays.iter().zip(&mut *actual_out) {
81    *byte = hex_to_bytes(*a, *b)?;
82  }
83  Ok(actual_out)
84}
85
86/// Encodes `data` into `out` returning the affected part.
87///
88/// `mode` defaults to [`HexEncMode::WithoutPrefixLower`] if `None`.
89#[inline]
90pub fn encode_hex<'out>(
91  data: &[u8],
92  mode: Option<HexEncMode>,
93  out: &'out mut [u8],
94) -> crate::Result<&'out str> {
95  let actual_mode = actual_mode(mode);
96  let mut hex_len = data.len().wrapping_mul(2);
97  let actual_out = match actual_mode {
98    #[cfg(feature = "sha3")]
99    HexEncMode::Eip55 => return encode_eip55(data, out),
100    HexEncMode::WithPrefixLower | HexEncMode::WithPrefixUpper => {
101      hex_len = hex_len.wrapping_add(2);
102      let Some([a, b, actual_out @ ..]) = out.get_mut(..hex_len) else {
103        return Err(HexError::InsufficientBuffer.into());
104      };
105      *a = b'0';
106      *b = b'x';
107      actual_out
108    }
109    HexEncMode::WithoutPrefixLower | HexEncMode::WithoutPrefixUpper => {
110      let Some((actual_out, _)) = out.split_at_mut_checked(hex_len) else {
111        return Err(HexError::InsufficientBuffer.into());
112      };
113      actual_out
114    }
115  };
116  let (arrays, _) = actual_out.as_chunks_mut::<2>();
117  let table = match actual_mode {
118    #[cfg(feature = "sha3")]
119    HexEncMode::Eip55 => return Ok(""),
120    HexEncMode::WithPrefixLower | HexEncMode::WithoutPrefixLower => LOWER_HEX_CHARS,
121    HexEncMode::WithPrefixUpper | HexEncMode::WithoutPrefixUpper => UPPER_HEX_CHARS,
122  };
123  for (byte, [a, b]) in data.iter().zip(arrays) {
124    let (lhs, rhs) = byte_to_hex(*byte, table);
125    *a = lhs;
126    *b = rhs;
127  }
128  // SAFETY: HEX is always UTF-8
129  unsafe { Ok(str::from_utf8_unchecked(out.get_mut(..hex_len).unwrap_or_default())) }
130}
131
132const fn actual_mode(hem: Option<HexEncMode>) -> HexEncMode {
133  if let Some(elem) = hem { elem } else { HexEncMode::WithoutPrefixLower }
134}
135
136#[expect(clippy::indexing_slicing, reason = "all bytes are limited to the array's length")]
137fn byte_to_hex(byte: u8, table: &[u8; 16]) -> (u8, u8) {
138  let lhs_idx: usize = (byte >> 4).into();
139  let rhs_idx: usize = (byte & 0b0000_1111).into();
140  (table[lhs_idx], table[rhs_idx])
141}
142
143#[cfg(feature = "sha3")]
144fn encode_eip55<'out>(data: &[u8], out: &'out mut [u8]) -> crate::Result<&'out str> {
145  use sha3::Digest;
146  if data.len() > 32 {
147    return Err(HexError::InvalidEip55Input.into());
148  }
149  let rslt_len = encode_hex(data, Some(HexEncMode::WithPrefixLower), out)?.len();
150  let Some([_, _, hex @ ..]) = out.get_mut(..rslt_len) else {
151    return Ok("");
152  };
153  let hash: [u8; 32] = {
154    let mut hasher = sha3::Keccak256::default();
155    hasher.update(&*hex);
156    hasher.finalize().into()
157  };
158  for (idx, byte) in hex.iter_mut().enumerate() {
159    let is_letter = byte.is_ascii_lowercase();
160    if !is_letter {
161      continue;
162    }
163    let half_idx = hash.get(idx / 2).copied().unwrap_or_default();
164    let nibble = if idx % 2 == 0 { half_idx >> 4 } else { half_idx & 0b0000_1111 };
165    if nibble >= 8 {
166      *byte = byte.to_ascii_uppercase();
167    }
168  }
169  // SAFETY: HEX is always UTF-8
170  unsafe { Ok(str::from_utf8_unchecked(out.get_mut(..rslt_len).unwrap_or_default())) }
171}
172
173fn hex_to_bytes(lhs: u8, rhs: u8) -> crate::Result<u8> {
174  fn half(byte: u8) -> crate::Result<u8> {
175    match byte {
176      b'A'..=b'F' => Ok(byte.wrapping_sub(b'A').wrapping_add(10)),
177      b'a'..=b'f' => Ok(byte.wrapping_sub(b'a').wrapping_add(10)),
178      b'0'..=b'9' => Ok(byte.wrapping_sub(b'0')),
179      _ => Err(HexError::InvalidHexCharacter.into()),
180    }
181  }
182  Ok((half(lhs)? << 4) | half(rhs)?)
183}
184
185#[cfg(test)]
186mod test {
187  use crate::{
188    collection::ArrayVectorU8,
189    de::{HexDisplay, HexEncMode, decode_hex, encode_hex},
190  };
191
192  #[test]
193  fn decode_has_correct_output() {
194    assert_eq!(decode_hex(b"61626364", &mut [0; 4]).unwrap(), b"abcd");
195    assert_eq!(decode_hex(b"0x6162636465", &mut [0; 5]).unwrap(), b"abcde");
196    assert!(decode_hex(b"6", &mut [0, 0, 0, 0]).is_err());
197  }
198
199  #[cfg(feature = "sha3")]
200  #[test]
201  fn eip55() {
202    let mut buf = [0u8; 44];
203    assert_eq!(
204      encode_hex(
205        &[
206          90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, 231, 239, 27, 234,
207          237,
208        ],
209        Some(HexEncMode::Eip55),
210        &mut buf
211      )
212      .unwrap(),
213      "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
214    );
215  }
216
217  #[test]
218  fn encode_has_correct_output() {
219    assert_eq!(encode_hex(&[], None, &mut [0u8; 8]).unwrap(), "");
220    assert_eq!(encode_hex(b"AZ", None, &mut [0u8; 8]).unwrap(), "415a");
221    assert_eq!(
222      encode_hex(b"AZ", Some(HexEncMode::WithoutPrefixUpper), &mut [0u8; 8]).unwrap(),
223      "415A"
224    );
225  }
226
227  #[test]
228  fn hex_display() {
229    assert_eq!(
230      &ArrayVectorU8::<u8, 16>::try_from(format_args!(
231        "{}",
232        HexDisplay(b"abcdZ", Some(HexEncMode::WithoutPrefixLower))
233      ))
234      .unwrap(),
235      "616263645a".as_bytes()
236    );
237    assert_eq!(
238      &ArrayVectorU8::<u8, 16>::try_from(format_args!(
239        "{}",
240        HexDisplay(b"abcdZ", Some(HexEncMode::WithPrefixLower))
241      ))
242      .unwrap(),
243      "0x616263645a".as_bytes()
244    );
245  }
246}