@kornel, those seem like decent ideas, if I wasn't constrained by formats I cannot control. Luckily I can control what this will be used for (3 different file types/structures so far), and so "well if you use it with XYZ rust type, it'll break" isn't a large concern of mine. I'm decoding C-defined POD types, that's it. That it will break on some things you can pass in is OK for now. I have read the Rust Koans and so I know I'm violating the 3rd, but I will definitely be looking into bytemuck as others have suggested, so I can see if applying the POD trait there is appropriate to constrain this further so it can't be misused.
I have constrained the unsafe usage to a single line now, and I don't think I have any memory unsafety anymore, as long as T is a POD type, which I can't enforce with baseline Rust it seems.
// Reads in the specified struct from the specified path. If there is not an exact match on size, returns an error
fn read_struct<T: Default, P: AsRef<Path>>(path: P) -> std::io::Result<Box<T>> {
let path = path.as_ref();
// Compare the type's size to that of the file we're reading from, returning on error
let struct_size = ::std::mem::size_of::<T>();
let num_bytes = fs::metadata(path)?.len() as usize;
if struct_size != num_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Length of file did not match structure specified. File length: {}, Expected: {}",
num_bytes, struct_size
),
));
}
// Open the file to read from
let mut reader = BufReader::new(File::open(path)?);
// Initialize the memory we're going to write into
let mut boxed_value = Box::new(T::default());
// Get a pointer to write to directly from the file. Pointer doesn't leak, as boxed_value still owns the memory
let ptr_from_box: *mut T = &mut *boxed_value;
// Turn the pointer we have into a slice so we can read into it from BufReader
let buffer = unsafe { slice::from_raw_parts_mut(ptr_from_box as *mut u8, num_bytes)};
// Do the actual read from disk into the memory that boxed_value allocated above. If this goes wrong, boxed_value still owns the memory, and destructs it
reader.read_exact(buffer)?;
// Read went fine, so return the box, as it's been populated with the contents of the file
Ok(boxed_value)
}
I agree that the Box::new_uninit() functionality is what I really want so I don't need to initialize it, but for stable right now I think what's above only has known limitations (TOCTOU, and POD dependence), and no actual flaws. But that's the whole point of posting this topic: to have those pointed out to me.