Yet another way to safely read into an uninitialized owned buffer

The new ReadBuf API is great for safely reading into uninitialized borrowed buffers. But it does not help much with owned buffers, as ReadBuf borrows and there’s no way to convert the buffer into one of the correct type:

fn read<R>(r: &mut R, len: usize) -> Result<Box<[u8]>>
    where R: Read
{
    let mut buf = Box::new_uninit_slice(len);
    let mut read_buf = ReadBuf::uninit(&mut buf);
    r.read_buf_exact(&mut read_buf)?;
    // Can’t return Box<[u8]> here without unsafe.
}

Luckily, taking a close look at the implementation of Read for Take, it properly supports the ReadBuf API if the underlying Read does. And the default implementation of Read::read_to_end uses the ReadBuf API, abstracting away the unsafe Vec::set_len! So we can implement the above function like follows:

use std::io::{ErrorKind, Read, Result};

fn read<R>(r: &mut R, len: usize) -> Result<Vec<u8>>
    where R: Read
{
    let mut buf = Vec::with_capacity(len);
    let len_u64 = u64::try_from(len).expect("len too big");
    let actual = r.take(len_u64).read_to_end(&mut buf)?;
    if actual == len {
        Ok(buf)
    } else {
        Err(ErrorKind::UnexpectedEof.into())
    }
}

I’ve been using this function and it works well. It can also be adapted to a read_exact_onto<R>(r: &mut R, len: usize, buf: &mut Vec<u8>)-style API.

But I have two questions:

  • Are there other ways to read into an owned buffer without initializing it, without using unsafe?
  • What would be the best way to cope with the fallibility of u64::try_from(len: usize)?

I don't think the standard library currently provides any safe ways of doing it other than take. That said, you may find it interesting that the Tokio crates provides a very similar ReadBuf api that does allow doing so via the BufMut trait from the bytes crate, which is implemented for many container types. You can find the method in question here.

I note that you could return Box<[u8]> from your method by using the take trick and then calling into_boxed_slice.

As for the fallability of u64::try_from(usize), I probably wouldn't bother to handle it.

5 Likes

If you don't want a panicking implementation, there's still the easy option of writing

u64::try_from(len).map_err(|e| io::Error::new(ErrorKind::Other, e))?

or once io::Error::other() is stabilized,

u64::try_from(len).map_err(io::Error::other)?
3 Likes