How to convert a `Vec<u8>` to a different struct, and dropping Vec, whilst having ownership on other struct

136 Views Asked by At

I have a function that reads a Vec<u8> from disk that I want to interpret as the ext2 super block (which is a packed struct).

So I used:

unsafe {&*(raw_super.as_ptr() as *const Super block)}

The problem is when the function returns the Vec is dropped, so the super block is only 0s

I want a way to transform the Vec into a super block and own the data in super block, with if possible a small cost abstraction

2

There are 2 best solutions below

0
cafce25 On

As others have noted in the comments before, you have to assert a couple of preconditions first:

  • The endianness is correct (ext2 uses little endian)
  • The alignment of u8 and SuperBlock is the same (but u8 and a #[repr(packed)] struct always fulfill that packed is implicitly packed(1))
  • The size of the Vec<u8> (it's len()) is the same as the size of a SuperBlock
  • The order of the fields in the struct is known and the same as specified for ext2

So you can convert your Vec<u8> to a Box<SuperBlock> like this:

// `C` so the order of the fields in memory is known
// `packed` so the alignment is 1 and there is no padding
#[repr(C, packed)]
pub struct SuperBlock {
    foo: u16,
    bar: u32,
}

// ext2 superblocks are always little endian, so this works only for those platforms
#[cfg(target_endian = "little")]
impl SuperBlock {
    /// ## Errors
    /// Returns the vector unchanged when it's length isn't exactly the size of a `SuperBlock`
    pub fn from_vec(v: Vec<u8>) -> Result<Box<Self>, Vec<u8>> {
        if v.len() != std::mem::size_of::<SuperBlock>() {
            return Err(v);
        }
        // SAFETY:
        // `#[repr(packed)] structs have an alignment of 1, the same as u8
        // v.len() and size_of::<SuperBlock>() are the same as asserted above
        Ok(unsafe { Box::from_raw(Box::into_raw(v.into_boxed_slice()).cast::<SuperBlock>()) })
    }
}

Playground

Technically, this does not drop the Vec, instead it reuses the same allocation for the returned Box<SuperBlock>

6
user4815162342 On

You could use the excellent bytemuck crate to safely cast the data in your Vec<u8> to &SuperBlock. For example:

use bytemuck::{Pod, Zeroable};

#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C, packed)]
pub struct SuperBlock {
    foo: u16,
    bar: u32,
}

pub struct OwnedSuperBlock {
    data: Vec<u8>,
}

impl OwnedSuperBlock {
    pub fn new(data: Vec<u8>) -> Self {
        OwnedSuperBlock { data }
    }

    pub fn as_super_block(&self) -> &SuperBlock {
        bytemuck::from_bytes(&self.data)
    }
}

Playground

This code doesn't use unsafe owing to guarantees provided by bytemuck::Pod trait and derive macro. They ensure that it's safe to cast bytes in the vector to a &SuperBlock, also checking that the bytes are correctly sized and aligned. Finally, the whole thing is zero-copy and involves minimal run-time cost (just the data size check).

Note that this code assumes that the on-disk format uses the same endianness as the architecture you compile your program on.