1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum HexCase {
6 Lower,
7 Upper,
8}
9
10#[must_use]
11pub fn hex_encode(input: &[u8]) -> String {
12 encode_hex(input, HexCase::Lower)
13}
14
15#[must_use]
16pub fn hex_encode_upper(input: &[u8]) -> String {
17 encode_hex(input, HexCase::Upper)
18}
19
20pub fn hex_decode(input: &str) -> Option<Vec<u8>> {
21 let value = strip_hex_prefix(input);
22 if !value.len().is_multiple_of(2) || !value.chars().all(is_hex_char) {
23 return None;
24 }
25
26 let mut output = Vec::with_capacity(value.len() / 2);
27 let bytes = value.as_bytes();
28 let mut index = 0;
29
30 while index < bytes.len() {
31 let high = decode_nibble(bytes[index])?;
32 let low = decode_nibble(bytes[index + 1])?;
33 output.push((high << 4) | low);
34 index += 2;
35 }
36
37 Some(output)
38}
39
40#[must_use]
41pub fn is_hex(input: &str) -> bool {
42 let value = strip_hex_prefix(input);
43 value.len().is_multiple_of(2) && value.chars().all(is_hex_char)
44}
45
46#[must_use]
47pub fn is_hex_char(c: char) -> bool {
48 c.is_ascii_hexdigit()
49}
50
51#[must_use]
52pub fn strip_hex_prefix(input: &str) -> &str {
53 input
54 .strip_prefix("0x")
55 .or_else(|| input.strip_prefix("0X"))
56 .or_else(|| input.strip_prefix('#'))
57 .unwrap_or(input)
58}
59
60#[must_use]
61pub fn ensure_hex_prefix(input: &str) -> String {
62 format!("0x{}", strip_hex_prefix(input))
63}
64
65pub fn normalize_hex(input: &str, case: HexCase) -> Option<String> {
66 let value = strip_hex_prefix(input);
67 if !value.chars().all(is_hex_char) {
68 return None;
69 }
70
71 Some(match case {
72 HexCase::Lower => value.to_ascii_lowercase(),
73 HexCase::Upper => value.to_ascii_uppercase(),
74 })
75}
76
77fn encode_hex(input: &[u8], case: HexCase) -> String {
78 let digits = match case {
79 HexCase::Lower => b"0123456789abcdef",
80 HexCase::Upper => b"0123456789ABCDEF",
81 };
82 let mut output = String::with_capacity(input.len() * 2);
83
84 for byte in input {
85 output.push(digits[(byte >> 4) as usize] as char);
86 output.push(digits[(byte & 0x0f) as usize] as char);
87 }
88
89 output
90}
91
92fn decode_nibble(byte: u8) -> Option<u8> {
93 match byte {
94 b'0'..=b'9' => Some(byte - b'0'),
95 b'a'..=b'f' => Some(byte - b'a' + 10),
96 b'A'..=b'F' => Some(byte - b'A' + 10),
97 _ => None,
98 }
99}