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 }