Skip to main content

reifydb_type/util/
hex.rs

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