taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

hex.rs (3866B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::fmt::Display;
     18 
     19 pub const HEX_ALPHABET: &[u8] = b"0123456789abcdef";
     20 
     21 /** Encode a single byte to two hex characters */
     22 #[inline(always)]
     23 fn encode_byte(byte: u8, encoded: &mut [u8]) {
     24     encoded[0] = HEX_ALPHABET[(byte >> 4) as usize];
     25     encoded[1] = HEX_ALPHABET[(byte & 0x0F) as usize];
     26 }
     27 
     28 /** Encode bytes using lowercase hexadecimal */
     29 pub fn encode(bytes: impl AsRef<[u8]>) -> String {
     30     let bytes = bytes.as_ref();
     31     let mut buf = vec![0u8; bytes.len() * 2];
     32 
     33     for (chunk, buf) in bytes.iter().zip(buf.chunks_exact_mut(2)) {
     34         encode_byte(*chunk, buf);
     35     }
     36 
     37     // SAFETY: only contains ASCII characters from HEX_ALPHABET
     38     unsafe { String::from_utf8_unchecked(buf) }
     39 }
     40 
     41 /** Format bytes using lowercase hexadecimal */
     42 pub fn fmt(bytes: impl AsRef<[u8]>) -> impl Display {
     43     std::fmt::from_fn(move |f| {
     44         for &byte in bytes.as_ref() {
     45             let mut out = [0u8; 2];
     46             encode_byte(byte, &mut out);
     47             // SAFETY: out contains only ASCII characters from HEX_ALPHABET
     48             f.write_str(unsafe { std::str::from_utf8_unchecked(&out) })?;
     49         }
     50         Ok(())
     51     })
     52 }
     53 
     54 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
     55 pub enum HexError {
     56     #[error("invalid hex format")]
     57     Format,
     58     #[error("invalid length: hex input must be a multiple of 2")]
     59     Length,
     60 }
     61 
     62 const HEX_INV: [u8; 256] = {
     63     let mut table = [255u8; 256];
     64     let mut i = 0;
     65     while i < 10 {
     66         table[(b'0' + i) as usize] = i;
     67         i += 1;
     68     }
     69     i = 0;
     70     while i < 6 {
     71         table[(b'a' + i) as usize] = 10 + i;
     72         table[(b'A' + i) as usize] = 10 + i;
     73         i += 1;
     74     }
     75     table
     76 };
     77 
     78 /** Decode a hexadecimal string */
     79 pub fn decode(encoded: impl AsRef<[u8]>) -> Result<Vec<u8>, HexError> {
     80     let encoded = encoded.as_ref();
     81     if encoded.len() % 2 != 0 {
     82         return Err(HexError::Length);
     83     }
     84 
     85     let mut decoded = Vec::with_capacity(encoded.len() / 2);
     86     let mut invalid = false;
     87 
     88     for chunk in encoded.chunks_exact(2) {
     89         let hi = HEX_INV[chunk[0] as usize];
     90         let lo = HEX_INV[chunk[1] as usize];
     91 
     92         invalid |= hi == 255 || lo == 255;
     93 
     94         decoded.push((hi << 4) | lo);
     95     }
     96 
     97     if invalid {
     98         return Err(HexError::Format);
     99     }
    100 
    101     Ok(decoded)
    102 }
    103 
    104 #[cfg(test)]
    105 mod test {
    106     use super::*;
    107 
    108     #[test]
    109     fn hex() {
    110         for (decoded, encoded) in [
    111             ("", ""),
    112             ("f", "66"),
    113             ("fo", "666f"),
    114             ("foobar", "666f6f626172"),
    115             ("kiwi", "6b697769"),
    116             ("Hello, world!", "48656c6c6f2c20776f726c6421"),
    117         ] {
    118             assert_eq!(encode(decoded.as_bytes()), encoded);
    119             assert_eq!(fmt(decoded).to_string(), encoded);
    120             assert_eq!(decode(encoded.as_bytes()).unwrap(), decoded.as_bytes());
    121         }
    122 
    123         // Case insensitivity
    124         assert_eq!(decode("48656C6C6F").unwrap(), b"Hello");
    125 
    126         // Invalid length
    127         assert_eq!(decode("666"), Err(HexError::Length));
    128 
    129         // Invalid characters
    130         assert_eq!(decode("6g"), Err(HexError::Format));
    131         assert_eq!(decode("6\x00"), Err(HexError::Format));
    132     }
    133 }