Skip to main content

reifydb_type/util/
hex.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::{
5	error,
6	fmt::{self, Write},
7};
8
9/// Zero-allocation hex display wrapper.
10pub struct DisplayHex<'a>(&'a [u8]);
11
12/// Returns a `Display` wrapper that writes hex without heap allocation.
13pub fn display(data: &[u8]) -> DisplayHex<'_> {
14	DisplayHex(data)
15}
16
17impl fmt::Display for DisplayHex<'_> {
18	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19		for byte in self.0 {
20			write!(f, "{:02x}", byte)?;
21		}
22		Ok(())
23	}
24}
25
26/// Encode bytes to hex string
27pub fn encode(data: &[u8]) -> String {
28	let mut result = String::with_capacity(data.len() * 2);
29	for byte in data {
30		write!(result, "{:02x}", byte).unwrap();
31	}
32	result
33}
34
35/// Decode hex string to bytes
36pub fn decode(hex: &str) -> Result<Vec<u8>, DecodeError> {
37	let hex = hex.trim();
38
39	if hex.is_empty() {
40		return Ok(Vec::new());
41	}
42
43	if !hex.len().is_multiple_of(2) {
44		return Err(DecodeError::OddLength);
45	}
46
47	let mut result = Vec::with_capacity(hex.len() / 2);
48
49	for chunk in hex.as_bytes().chunks(2) {
50		let high = decode_hex_digit(chunk[0])?;
51		let low = decode_hex_digit(chunk[1])?;
52		result.push((high << 4) | low);
53	}
54
55	Ok(result)
56}
57
58fn decode_hex_digit(byte: u8) -> Result<u8, DecodeError> {
59	match byte {
60		b'0'..=b'9' => Ok(byte - b'0'),
61		b'a'..=b'f' => Ok(byte - b'a' + 10),
62		b'A'..=b'F' => Ok(byte - b'A' + 10),
63		_ => Err(DecodeError::InvalidCharacter(byte as char)),
64	}
65}
66
67#[derive(Debug)]
68pub enum DecodeError {
69	InvalidCharacter(char),
70	OddLength,
71}
72
73impl fmt::Display for DecodeError {
74	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75		match self {
76			DecodeError::InvalidCharacter(ch) => {
77				write!(f, "Invalid hex character: '{}'", ch)
78			}
79			DecodeError::OddLength => {
80				write!(f, "Hex string has odd length")
81			}
82		}
83	}
84}
85
86impl error::Error for DecodeError {}
87
88#[cfg(test)]
89pub mod tests {
90	use super::*;
91
92	#[test]
93	fn test_encode() {
94		assert_eq!(encode(b"Hello"), "48656c6c6f");
95		assert_eq!(encode(b""), "");
96		assert_eq!(encode(&[0x00, 0xFF, 0x42]), "00ff42");
97	}
98
99	#[test]
100	fn test_decode() {
101		assert_eq!(decode("48656c6c6f").unwrap(), b"Hello");
102		assert_eq!(decode("48656C6C6F").unwrap(), b"Hello");
103		assert_eq!(decode("").unwrap(), b"");
104		assert_eq!(decode("00ff42").unwrap(), vec![0x00, 0xFF, 0x42]);
105	}
106
107	#[test]
108	fn test_decode_errors() {
109		assert!(decode("xyz").is_err());
110		assert!(decode("48656c6c6").is_err()); // odd length
111	}
112
113	#[test]
114	fn test_roundtrip() {
115		let data = b"Hello, World! \x00\x01\x02\xFF";
116		let encoded = encode(data);
117		let decoded = decode(&encoded).unwrap();
118		assert_eq!(decoded, data);
119	}
120}