Skip to main content

rune_hex/
lib.rs

1//! Hex encoding and decoding for byte slices and files.
2//!
3//! Converts between raw bytes and their hexadecimal string representation.
4//! Decoding accepts mixed-case input and optional `0x` prefix.
5//! The library has zero dependencies.
6//!
7//! # Features
8//!
9//! - [`encode`] — encode bytes to a lowercase hex string.
10//! - [`encode_upper`] — encode bytes to an uppercase hex string.
11//! - [`decode`] — decode a hex string to bytes; accepts mixed case and `0x` prefix.
12//! - [`HexError`] — error type for malformed input.
13//!
14//! # Quick Start
15//!
16//! ```rust
17//! use rune_hex::{encode, decode};
18//!
19//! let hex = encode(b"hello");
20//! assert_eq!(hex, "68656c6c6f");
21//!
22//! let bytes = decode(&hex).unwrap();
23//! assert_eq!(bytes, b"hello");
24//! ```
25
26use std::fmt;
27
28/// Error returned when [`decode`] encounters invalid hex input.
29#[derive(Debug, PartialEq, Eq)]
30pub enum HexError {
31    /// Input has an odd number of hex digits (each byte needs two).
32    OddLength,
33    /// A character in the input is not a valid hex digit.
34    InvalidChar(char),
35}
36
37impl fmt::Display for HexError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            HexError::OddLength => f.write_str("odd number of hex digits"),
41            HexError::InvalidChar(c) => write!(f, "invalid hex character: {c:?}"),
42        }
43    }
44}
45
46impl std::error::Error for HexError {}
47
48/// Encodes bytes as a lowercase hex string.
49///
50/// # Examples
51///
52/// ```rust
53/// use rune_hex::encode;
54///
55/// assert_eq!(encode(b"\xde\xad\xbe\xef"), "deadbeef");
56/// assert_eq!(encode(b""), "");
57/// ```
58pub fn encode(bytes: &[u8]) -> String {
59    bytes
60        .iter()
61        .flat_map(|b| nibble_to_hex(b >> 4, false).chain(nibble_to_hex(b & 0xf, false)))
62        .collect()
63}
64
65/// Encodes bytes as an uppercase hex string.
66///
67/// # Examples
68///
69/// ```rust
70/// use rune_hex::encode_upper;
71///
72/// assert_eq!(encode_upper(b"\xde\xad\xbe\xef"), "DEADBEEF");
73/// ```
74pub fn encode_upper(bytes: &[u8]) -> String {
75    bytes
76        .iter()
77        .flat_map(|b| nibble_to_hex(b >> 4, true).chain(nibble_to_hex(b & 0xf, true)))
78        .collect()
79}
80
81fn nibble_to_hex(nibble: u8, upper: bool) -> std::array::IntoIter<char, 1> {
82    let ch = match nibble {
83        0..=9 => b'0' + nibble,
84        _ if upper => b'A' + nibble - 10,
85        _ => b'a' + nibble - 10,
86    };
87    [ch as char].into_iter()
88}
89
90/// Decodes a hex string to bytes.
91///
92/// Accepts lowercase, uppercase, and mixed-case input. An optional `0x` or
93/// `0X` prefix is stripped before decoding.
94///
95/// # Errors
96///
97/// Returns [`HexError::OddLength`] if the number of hex digits is odd, or
98/// [`HexError::InvalidChar`] if any character is not a valid hex digit.
99///
100/// # Examples
101///
102/// ```rust
103/// use rune_hex::decode;
104///
105/// assert_eq!(decode("deadbeef").unwrap(), b"\xde\xad\xbe\xef");
106/// assert_eq!(decode("DEADBEEF").unwrap(), b"\xde\xad\xbe\xef");
107/// assert_eq!(decode("0xDeAdBeEf").unwrap(), b"\xde\xad\xbe\xef");
108/// assert_eq!(decode("").unwrap(), b"");
109/// ```
110pub fn decode(hex: &str) -> Result<Vec<u8>, HexError> {
111    let hex = hex
112        .strip_prefix("0x")
113        .or_else(|| hex.strip_prefix("0X"))
114        .unwrap_or(hex);
115
116    if !hex.len().is_multiple_of(2) {
117        return Err(HexError::OddLength);
118    }
119
120    hex.as_bytes()
121        .chunks(2)
122        .map(|pair| {
123            let hi = from_hex_char(pair[0] as char)?;
124            let lo = from_hex_char(pair[1] as char)?;
125            Ok((hi << 4) | lo)
126        })
127        .collect()
128}
129
130fn from_hex_char(c: char) -> Result<u8, HexError> {
131    match c {
132        '0'..='9' => Ok(c as u8 - b'0'),
133        'a'..='f' => Ok(c as u8 - b'a' + 10),
134        'A'..='F' => Ok(c as u8 - b'A' + 10),
135        _ => Err(HexError::InvalidChar(c)),
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn encode_empty() {
145        assert_eq!(encode(b""), "");
146    }
147
148    #[test]
149    fn encode_single_byte() {
150        assert_eq!(encode(b"\xff"), "ff");
151        assert_eq!(encode(b"\x00"), "00");
152    }
153
154    #[test]
155    fn encode_lowercase() {
156        assert_eq!(encode(b"\xde\xad\xbe\xef"), "deadbeef");
157    }
158
159    #[test]
160    fn encode_upper_uppercase() {
161        assert_eq!(encode_upper(b"\xde\xad\xbe\xef"), "DEADBEEF");
162    }
163
164    #[test]
165    fn encode_roundtrip() {
166        let original = b"Hello, world!\x00\xff";
167        assert_eq!(decode(&encode(original)).unwrap(), original);
168    }
169
170    #[test]
171    fn decode_empty() {
172        assert_eq!(decode("").unwrap(), b"");
173    }
174
175    #[test]
176    fn decode_lowercase() {
177        assert_eq!(decode("deadbeef").unwrap(), b"\xde\xad\xbe\xef");
178    }
179
180    #[test]
181    fn decode_uppercase() {
182        assert_eq!(decode("DEADBEEF").unwrap(), b"\xde\xad\xbe\xef");
183    }
184
185    #[test]
186    fn decode_mixed_case() {
187        assert_eq!(decode("DeAdBeEf").unwrap(), b"\xde\xad\xbe\xef");
188    }
189
190    #[test]
191    fn decode_0x_prefix() {
192        assert_eq!(decode("0xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
193        assert_eq!(decode("0Xdeadbeef").unwrap(), b"\xde\xad\xbe\xef");
194    }
195
196    #[test]
197    fn decode_odd_length_errors() {
198        assert_eq!(decode("abc").unwrap_err(), HexError::OddLength);
199    }
200
201    #[test]
202    fn decode_invalid_char_errors() {
203        assert!(matches!(
204            decode("zz").unwrap_err(),
205            HexError::InvalidChar('z')
206        ));
207    }
208
209    #[test]
210    fn encode_all_bytes() {
211        let all: Vec<u8> = (0u8..=255).collect();
212        let hex = encode(&all);
213        assert_eq!(hex.len(), 512);
214        assert_eq!(decode(&hex).unwrap(), all);
215    }
216}