I’m trying to parse a file format, for which I have a reference i.e at offset X read Y bytes to get Z information.
I want to use u32::from_le_bytes() to convert parts of the byte array into a u32. However, since slices are dynamically sized in Rust I need to call <[u8; 4]>::try_from() on the slice to ensure it is the right size, which feels very verbose.
In my case, the array is guaranteed to be of size 16 and the slice is 2..6. Using try_from() therefore feels like unnecessary work as statically, I can’t see how it would fail.
fn parse(data: &[u8;16]) -> Result<u32, &'static str> {
let id_bytes = match <[u8; 4]>::try_from(&data[2..6]) {
Ok(array) => array,
Err(_e) => return Err("Failed to parse.")
};
let id : u32 = u32::from_le_bytes(id_bytes);
Ok(id)
}
Alternatively, I could unwrap try_from() to make it a bit shorter, but again I feel like that is trying to hide an error that doesn’t exist.
fn parse2(data: &[u8;16]) -> Result<u32, &'static str> {
let id_bytes = <[u8; 4]>::try_from(&data[2..6]).unwrap();
let id : u32 = u32::from_le_bytes(id_bytes);
Ok(id)
}
I could of course just get the values one by one, and the compiler doesn’t seem to complain about this, but obviously this isn’t sustainable for writing multiple u32, or even a u128.
fn parse3(data: &[u8;16]) -> Result<u32, &'static str> {
let id : u32 = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
Ok(id)
}
Lastly, I could also use copy_from_slice into an appropriately sized array:
fn parse4(data: &[u8;16]) -> Result<u32, &'static str> {
let mut id_bytes : [u8;4] = [0x00;4];
id_bytes.copy_from_slice(&data[2..6]);
let id : u32 = u32::from_le_bytes(id_bytes);
Ok(id)
}
Is there perhaps a better way I haven’t thought of, and is there any clear winner in terms of speed or idiomacy?
>Solution :
unwrap()ing is fine. Since an error is a bug, you should panic (and not return Err) on error; and since it is clear why the code cannot fail, you can use unwrap() and not expect().
However, if you want, you can use constant magic to index the array with a static guarantee you won’t fail. I don’t know if I would recommend this, but this is a viable option.
use std::ops::Index;
// We cannot use `const END: usize`, because of limited const generics capabilities in current Rust.
pub struct ConstRange<const START: usize, const LEN: usize>;
impl<T, const START: usize, const LEN: usize, const N: usize> Index<ConstRange<START, LEN>>
for [T; N]
{
type Output = [T; LEN];
fn index(&self, _: ConstRange<START, LEN>) -> &Self::Output {
struct Validate<const START: usize, const LEN: usize, const N: usize>;
impl<const START: usize, const LEN: usize, const N: usize> Validate<START, LEN, N> {
const VALID: () = if START + LEN > N {
panic!("too big indices")
};
}
// This makes invalid indexing fails at compile-time instead of at runtime.
// Only works for `cargo check`, not `cargo build`, but hey, it works!
_ = Validate::<START, LEN, N>::VALID;
self[START..][..LEN].try_into().unwrap()
}
}
/// Short for "const range", because `array[const_range!(1..3)]` is too long.
#[macro_export]
macro_rules! cr {
( $start:literal .. $end:literal ) => {
$crate::ConstRange::<$start, { $end - $start }>
};
}
Then use this like:
fn parse(data: &[u8; 16]) -> Result<u32, &'static str> {
let id_bytes = data[cr!(2..6)];
let id: u32 = u32::from_le_bytes(id_bytes);
Ok(id)
}
You can expand it with other types of ranges or mutable indexing. This example is minimal.
Don’t worry about speed: the optimizer will optimize the check away anyway.