Convert Hex to Base64
Found a website called cryptopals with programming challenges. Possibly an interesting way to learn some Rust.
Set 1, challenge 1 is to convert a hex string to base64. My first attempt converted the input “directly” to base64 (that is, every 3 hex characters to 2 base64 characters) but then I saw that other challenges were using byte sequences, so it made more sense to break it down a bit.
Converting a hex string to a byte vector:
/// Convert a sequence of bytes in hexadecimal notation to a byte array
pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
const HEX: &str = "0123456789abcdef";
let mut result = Vec::new();
let mut chars = hex.chars();
while let Some(ch) = chars.next() {
let n1 = HEX.find(ch).ok_or(format!("Invalid character {}", ch))?;
let ch = chars.next().ok_or(String::from("Odd-sized hex string"))?;
let n2 = HEX.find(ch).ok_or(format!("Invalid character {}", ch))?;
result.push((n1 << 4 | n2) as u8);
}
Ok(result)
}
I wasn’t sure about using a vector, but returning a slice doesn’t seem to be the done thing, and returning an array requires knowing the length of the array up-front. So a vector it is.
Things I don’t really like:
- Using
while
- I wanted to use something likeexact_chunks
orchunks
, butchars()
does not return anything that supports that. I may be wrong of course. - Not sure about
ok_or(...)?
but it seems to work. It feels like an extra step (conveting an optional to aResult
only to use the question mark operator). - Repeating
"Invalid character {}"
. Not great - This doesn’t support uppercase hex. It’s easy to fix though.
Converting a byte vector to base64:
/// Convert a byte vector to base64 representation of the input
pub fn bytes_to_base64(bytes: Vec<u8>) -> String {
const BASE64: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let iter = bytes.chunks_exact(3);
let remainder = iter.remainder();
for chunk in iter {
let v0 = chunk[0] >> 2;
result.push(BASE64.chars().nth(v0 as usize).unwrap());
let v1 = (chunk[0] << 4 | chunk[1] >> 4) & 0x3f;
result.push(BASE64.chars().nth(v1 as usize).unwrap());
let v2 = (chunk[1] << 2 | chunk[2] >> 6) & 0x3f;
result.push(BASE64.chars().nth(v2 as usize).unwrap());
let v3 = chunk[2] & 0x3f;
result.push(BASE64.chars().nth(v3 as usize).unwrap());
}
if remainder.len() == 1 {
let v0 = remainder[0] >> 2;
result.push(BASE64.chars().nth(v0 as usize).unwrap());
let v1 = (remainder[0] << 4) & 0x3f;
result.push(BASE64.chars().nth(v1 as usize).unwrap());
result.push('=');
result.push('=');
} else if remainder.len() == 2 {
let v0 = remainder[0] >> 2;
result.push(BASE64.chars().nth(v0 as usize).unwrap());
let v1 = (remainder[0] << 4 | remainder[1] >> 4) & 0x3f;
result.push(BASE64.chars().nth(v1 as usize).unwrap());
let v2 = remainder[1] << 2 & 0x3f;
result.push(BASE64.chars().nth(v2 as usize).unwrap());
result.push('=');
}
result
}
I don’t like some of the repetition - the special cases at the end. There may be a smarter way to do it, but on the other hand base64 will only be used for output so it doesn’t have to be the most elegant or efficient code. Maybe later…
And now the conversion is straight-forward:
/// Convert a sequence of bytes in hexadecimal notation to the same sequence in base64 notation
pub fn hex_to_base64(hex: &str) -> Result<String, String> {
let bytes = hex_to_bytes(hex)?;
Ok(bytes_to_base64(bytes))
}