How do I encode and decode hexadecimal data with hex in Rust?
Walkthrough
The hex crate provides utilities for encoding binary data to hexadecimal strings and decoding hex strings back to binary. It's a simple, zero-dependency crate that handles the common task of representing bytes as hex characters. This is essential for cryptographic hashes, binary protocols, debugging binary data, and any situation where you need a human-readable representation of raw bytes. The crate offers both a simple functional API and a trait-based approach for more ergonomic usage.
Key concepts:
- encode() — converts bytes to a lowercase hex string
- decode() — converts a hex string back to bytes
- encode_upper() — converts bytes to an uppercase hex string
- FromHex trait — decode hex strings into types implementing the trait
- Error handling — robust error handling for invalid hex input
Code Example
# Cargo.toml
[dependencies]
hex = "0.4"use hex::{encode, decode};
fn main() {
let bytes = b"hello";
let hex_string = encode(bytes);
println!("Hex: {}", hex_string); // "68656c6c6f"
let decoded = decode(&hex_string).unwrap();
println!("Decoded: {:?}", decoded); // [104, 101, 108, 108, 111]
}Basic Encoding
use hex::{encode, encode_upper};
fn main() {
// Encode bytes to lowercase hex
let bytes = b"Hello, World!";
let hex_lower = encode(bytes);
println!("Lowercase: {}", hex_lower);
// Encode bytes to uppercase hex
let hex_upper = encode_upper(bytes);
println!("Uppercase: {}", hex_upper);
// Encode a slice
let data = [0x00, 0xff, 0xab, 0xcd];
println!("Hex: {}", encode(data));
// Encode Vec<u8>
let vec = vec![0xde, 0xad, 0xbe, 0xef];
println!("Hex: {}", encode(vec));
}Basic Decoding
use hex::decode;
fn main() {
// Decode hex string to bytes
let hex = "48656c6c6f";
let bytes = decode(hex).unwrap();
println!("Bytes: {:?}", bytes);
println!("As string: {}", String::from_utf8_lossy(&bytes));
// Case-insensitive decoding
let mixed_case = "DeadBEEF";
let bytes = decode(mixed_case).unwrap();
println!("Decoded: {:x?}", bytes);
// With or without 0x prefix
let with_prefix = "0x1234";
let without_prefix = "1234";
// Note: hex crate doesn't handle 0x prefix by default
// Strip it manually if needed
let stripped = with_prefix.strip_prefix("0x").unwrap_or(with_prefix);
let bytes = decode(stripped).unwrap();
println!("Stripped and decoded: {:?}", bytes);
}Error Handling
use hex::{decode, FromHexError};
fn main() {
// Invalid hex characters
match decode("hello world") {
Ok(bytes) => println!("Decoded: {:?}", bytes),
Err(FromHexError::InvalidHexCharacter { c, index }) => {
println!("Invalid character '{}' at index {}", c, index);
}
Err(e) => println!("Error: {}", e),
}
// Odd length hex string
match decode("abc") {
Ok(bytes) => println!("Decoded: {:?}", bytes),
Err(FromHexError::OddLength) => {
println!("Hex string must have even length");
}
Err(e) => println!("Error: {}", e),
}
// Safe decode function
fn safe_decode(hex: &str) -> Result<Vec<u8>, String> {
decode(hex).map_err(|e| format!("Hex decode error: {}", e))
}
match safe_decode("invalid") {
Ok(bytes) => println!("Success: {:?}", bytes),
Err(e) => println!("Error: {}", e),
}
}FromHex Trait
use hex::FromHex;
fn main() {
// Decode into Vec<u8>
let bytes = Vec::from_hex("48656c6c6f").unwrap();
println!("Vec: {:?}", bytes);
// Decode into fixed-size array
let array: [u8; 4] = <[u8; 4]>::from_hex("deadbeef").unwrap();
println!("Array: {:x?}", array);
// Works with any type that implements FromHex
fn decode_to<T: FromHex>(hex: &str) -> Result<T, T::Error> {
T::from_hex(hex)
}
let vec: Vec<u8> = decode_to("cafebabe").unwrap();
println!("Decoded: {:x?}", vec);
}ToHex Trait
use hex::ToHex;
fn main() {
let bytes = b"hello";
// Encode to String
let hex_string = bytes.encode_hex::<String>();
println!("Hex: {}", hex_string);
// Encode to uppercase
let hex_upper = bytes.encode_hex_upper::<String>();
println!("Upper: {}", hex_upper);
// Write to existing buffer
let mut buffer = String::new();
bytes.write_hex(&mut buffer).unwrap();
println!("Buffer: {}", buffer);
}Cryptographic Hashes
use hex::{encode, decode};
use std::collections::HashMap;
// Simulated hash storage
struct HashStore {
hashes: HashMap<String, Vec<u8>>,
}
impl HashStore {
fn new() -> Self {
Self { hashes: HashMap::new() }
}
fn store(&mut self, key: &str, hash: &[u8]) {
self.hashes.insert(key.to_string(), hash.to_vec());
}
fn get_hex(&self, key: &str) -> Option<String> {
self.hashes.get(key).map(|bytes| encode(bytes))
}
fn verify(&self, key: &str, hex_hash: &str) -> bool {
match decode(hex_hash) {
Ok(provided) => {
self.hashes.get(key)
.map(|stored| stored == &provided)
.unwrap_or(false)
}
Err(_) => false,
}
}
}
fn main() {
let mut store = HashStore::new();
// Store a "hash" (simulated SHA-256 would be 32 bytes)
let hash = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
store.store("file.txt", &hash);
// Get as hex
println!("Stored hash: {}", store.get_hex("file.txt").unwrap());
// Verify
println!("Valid: {}", store.verify("file.txt", "123456789abcdef0"));
println!("Invalid: {}", store.verify("file.txt", "aabbccdd"));
}Binary Protocol Debugging
use hex::{encode, decode};
fn main() {
// Simulated binary packet
let packet: Vec<u8> = vec![
0x7e, // Start byte
0x01, // Version
0x00, 0x10, // Length (16)
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21, // Payload
0x7e, // End byte
];
// Print packet as hex for debugging
println!("Packet: {}", encode(&packet));
// Format nicely
fn format_hex_dump(data: &[u8], bytes_per_line: usize) {
for (i, chunk) in data.chunks(bytes_per_line).enumerate() {
let hex: Vec<String> = chunk.iter().map(|b| format!("{:02x}", b)).collect();
let ascii: String = chunk.iter()
.map(|b| if b.is_ascii_graphic() { *b as char } else { '.' })
.collect();
println!("{:08x} {:<width$} {}",
i * bytes_per_line,
hex.join(" "),
ascii,
width = bytes_per_line * 3 - 1
);
}
}
println!("\nHex dump:");
format_hex_dump(&packet, 8);
}Configuration Files with Hex
use hex::{encode, decode};
use std::collections::HashMap;
fn parse_hex_config(config: &HashMap<&str, &str>) -> Result<HashMap<String, Vec<u8>>, String> {
let mut result = HashMap::new();
for (key, hex_value) in config {
let bytes = decode(hex_value)
.map_err(|e| format!("Failed to decode '{}' for key '{}': {}", hex_value, key, e))?;
result.insert(key.to_string(), bytes);
}
Ok(result)
}
fn main() {
let mut config = HashMap::new();
config.insert("api_key", "deadbeef12345678");
config.insert("secret", "cafebabedeadbeef");
match parse_hex_config(&config) {
Ok(parsed) => {
for (key, value) in &parsed {
println!("{}: {}", key, encode(value));
}
}
Err(e) => println!("Error: {}", e),
}
}Hex Color Conversion
use hex::{encode, decode};
struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
fn from_hex(hex: &str) -> Result<Self, String> {
let clean = hex.strip_prefix('#').unwrap_or(hex);
if clean.len() != 6 {
return Err("Hex color must be 6 characters".to_string());
}
let bytes = decode(clean)
.map_err(|e| format!("Invalid hex: {}", e))?;
Ok(Self {
r: bytes[0],
g: bytes[1],
b: bytes[2],
})
}
fn to_hex(&self) -> String {
encode([self.r, self.g, self.b])
}
fn to_hex_pretty(&self) -> String {
format!("#{}", self.to_hex())
}
}
fn main() {
let red = Color::from_hex("ff0000").unwrap();
println!("Red: rgb({}, {}, {})", red.r, red.g, red.b);
println!("Back to hex: {}", red.to_hex_pretty());
let blue = Color::from_hex("#0000ff").unwrap();
println!("Blue: {}", blue.to_hex_pretty());
}MAC Address Handling
use hex::{encode, decode};
struct MacAddress([u8; 6]);
impl MacAddress {
fn from_hex(hex: &str) -> Result<Self, String> {
let clean: String = hex.chars().filter(|c| *c != ':' && *c != '-').collect();
if clean.len() != 12 {
return Err("MAC address must have 12 hex digits".to_string());
}
let bytes = decode(&clean)
.map_err(|e| format!("Invalid hex: {}", e))?;
let mut arr = [0u8; 6];
arr.copy_from_slice(&bytes);
Ok(Self(arr))
}
fn to_hex(&self) -> String {
encode(self.0)
}
fn to_colon_hex(&self) -> String {
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
self.0[0], self.0[1], self.0[2],
self.0[3], self.0[4], self.0[5])
}
fn to_dash_hex(&self) -> String {
format!("{:02X}-{:02X}-{:02X}-{:02X}-{:02X}-{:02X}",
self.0[0], self.0[1], self.0[2],
self.0[3], self.0[4], self.0[5])
}
}
fn main() {
let mac = MacAddress::from_hex("aa:bb:cc:dd:ee:ff").unwrap();
println!("Plain: {}", mac.to_hex());
println!("Colon: {}", mac.to_colon_hex());
println!("Dash: {}", mac.to_dash_hex());
}UUID Representation
use hex::{encode, decode};
struct Uuid([u8; 16]);
impl Uuid {
fn from_hex(hex: &str) -> Result<Self, String> {
let clean: String = hex.chars().filter(|c| *c != '-').collect();
if clean.len() != 32 {
return Err("UUID must have 32 hex digits".to_string());
}
let bytes = decode(&clean)
.map_err(|e| format!("Invalid hex: {}", e))?;
let mut arr = [0u8; 16];
arr.copy_from_slice(&bytes);
Ok(Self(arr))
}
fn to_hex(&self) -> String {
encode(self.0)
}
fn to_hyphenated(&self) -> String {
let h = self.to_hex();
format!("{}-{}-{}-{}-{}",
&h[0..8], &h[8..12], &h[12..16], &h[16..20], &h[20..32])
}
}
fn main() {
let uuid = Uuid::from_hex("550e8400-e29b-41d4-a716-446655440000").unwrap();
println!("Plain: {}", uuid.to_hex());
println!("Hyphenated: {}", uuid.to_hyphenated());
}Hex Editor View
use hex::encode;
fn hex_view(data: &[u8]) {
println!("Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII");
println!("-------- -------------------------------- ----------------");
for (offset, chunk) in data.chunks(16).enumerate() {
let mut hex_part = String::new();
let mut ascii_part = String::new();
for (i, byte) in chunk.iter().enumerate() {
if i == 8 {
hex_part.push(' ');
}
hex_part.push_str(&format!("{:02x} ", byte));
if byte.is_ascii_graphic() {
ascii_part.push(*byte as char);
} else {
ascii_part.push('.');
}
}
// Pad if chunk is less than 16 bytes
for i in chunk.len()..16 {
if i == 8 {
hex_part.push(' ');
}
hex_part.push_str(" ");
}
println!("{:08x} {} {}", offset * 16, hex_part, ascii_part);
}
}
fn main() {
let data = b"Hello, World! This is some binary data with special bytes like \x00\xff\xab.";
hex_view(data);
}Comparing Hex Values
use hex::{encode, decode};
fn compare_hex(a: &str, b: &str) -> Result<bool, String> {
let bytes_a = decode(a).map_err(|e| format!("Decode A: {}", e))?;
let bytes_b = decode(b).map_err(|e| format!("Decode B: {}", e))?;
Ok(bytes_a == bytes_b)
}
// Case-insensitive comparison
fn compare_hex_nocase(a: &str, b: &str) -> Result<bool, String> {
Ok(a.to_lowercase() == b.to_lowercase())
}
fn main() {
println!("Equal: {}", compare_hex("ABCDEF", "abcdef").unwrap());
println!("Equal: {}", compare_hex("123456", "123457").unwrap());
}Streaming Large Data
use hex::encode;
fn main() {
// For large data, encode in chunks
let large_data: Vec<u8> = (0..255).collect();
// Process in chunks to avoid large allocations
let mut hex_string = String::with_capacity(large_data.len() * 2);
for chunk in large_data.chunks(32) {
hex_string.push_str(&encode(chunk));
}
println!("Hex length: {}", hex_string.len());
println!("First 20 chars: {}", &hex_string[..20]);
}Binary to Hex Table
use hex::encode;
fn main() {
// Quick reference table
println!("Decimal Hex Binary");
println!("------- ----- ------");
for i in 0u8..=15 {
let hex = encode(&[i]);
println!("{:7} {:5} {:06b}", i, hex, i);
}
}Parsing Hex Literals
use hex::decode;
fn parse_hex_literal(s: &str) -> Result<Vec<u8>, String> {
let clean = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
decode(clean).map_err(|e| format!("Invalid hex literal: {}", e))
}
fn main() {
let with_prefix = parse_hex_literal("0xdeadbeef").unwrap();
println!("Parsed: {:x?}", with_prefix);
let without_prefix = parse_hex_literal("cafebabe").unwrap();
println!("Parsed: {:x?}", without_prefix);
}Encoding Structs
use hex::encode;
#[derive(Debug)]
#[repr(C)]
struct PacketHeader {
magic: u32,
version: u16,
flags: u16,
}
fn main() {
let header = PacketHeader {
magic: 0x12345678,
version: 0x0100,
flags: 0x0001,
};
// Convert to bytes (be careful with endianness and padding!)
let bytes: [u8; std::mem::size_of::<PacketHeader>()] = unsafe {
std::mem::transmute(header)
};
println!("Header as hex: {}", encode(bytes));
println!("Size: {} bytes", std::mem::size_of::<PacketHeader>());
}Working with Serde
# Cargo.toml
[dependencies]
hex = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct KeyPair {
#[serde(with = "hex")]
public_key: Vec<u8>,
#[serde(with = "hex")]
private_key: Vec<u8>,
}
fn main() {
let keypair = KeyPair {
public_key: vec![0x02, 0x03, 0x04, 0x05],
private_key: vec![0xde, 0xad, 0xbe, 0xef],
};
let json = serde_json::to_string(&keypair).unwrap();
println!("JSON: {}", json);
let parsed: KeyPair = serde_json::from_str(&json).unwrap();
println!("Parsed: {:?}", parsed);
}Summary
encode()converts bytes to lowercase hex stringencode_upper()converts bytes to uppercase hex stringdecode()converts hex string to bytes, handles both casesFromHextrait for decoding into specific typesToHextrait for encoding with more control- Errors include
InvalidHexCharacterandOddLength - Strip
0xprefix manually before decoding - Enable
serdefeature for automatic hex serialization - Perfect for: cryptographic hashes, binary protocols, debugging, MAC addresses, colors
- Simple, zero-dependency crate with no runtime overhead
