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:

  1. encode() — converts bytes to a lowercase hex string
  2. decode() — converts a hex string back to bytes
  3. encode_upper() — converts bytes to an uppercase hex string
  4. FromHex trait — decode hex strings into types implementing the trait
  5. 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 string
  • encode_upper() converts bytes to uppercase hex string
  • decode() converts hex string to bytes, handles both cases
  • FromHex trait for decoding into specific types
  • ToHex trait for encoding with more control
  • Errors include InvalidHexCharacter and OddLength
  • Strip 0x prefix manually before decoding
  • Enable serde feature for automatic hex serialization
  • Perfect for: cryptographic hashes, binary protocols, debugging, MAC addresses, colors
  • Simple, zero-dependency crate with no runtime overhead