diff --git a/tokio/src/fs/file.rs b/tokio/src/fs/file.rs index efce9fda990..b127e91ea27 100644 --- a/tokio/src/fs/file.rs +++ b/tokio/src/fs/file.rs @@ -538,6 +538,174 @@ impl File { pub fn set_max_buf_size(&mut self, max_buf_size: usize) { self.max_buf_size = max_buf_size; } + + /// Reads a number of bytes starting from a given offset. + /// + /// Returns the number of bytes read. + /// + /// The offset is relative to the start of the file and thus independent + /// from the current cursor. + /// + /// The current file cursor is not affected by this function. + /// + /// It is not an error to return with a short read. + /// + /// # Examples + /// + /// ```no_run + /// use tokio::fs::File; + /// use tokio::io::AsyncSeekExt; + /// + /// # async fn dox() -> std::io::Result<()> { + /// let mut buf = vec![0_u8; 10]; + /// let mut file = File::open("foo.txt").await?; + /// file.read_at(&mut buf, 5).await?; + /// + /// assert_eq!(file.stream_position().await.unwrap(), 0); + /// # Ok(()) + /// # } + /// ``` + #[cfg(unix)] + #[cfg_attr(docsrs, doc(cfg(unix)))] + pub async fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { + fn _read_at(std: &StdFile, n: usize, offset: u64) -> io::Result> { + use std::os::unix::fs::FileExt; + let mut buf: Vec = vec![0; n]; + let n_read = std.read_at(&mut buf, offset)?; + buf.truncate(n_read); + + Ok(buf) + } + + let std = self.std.clone(); + let n = buf.len(); + let bytes_read = asyncify(move || _read_at(&std, n, offset)).await?; + let len = bytes_read.len(); + buf[..len].copy_from_slice(&bytes_read); + + Ok(len) + } + + /// Writes a number of bytes starting from a given offset. + /// + /// Returns the number of bytes written. + /// + /// The offset is relative to the start of the file and thus independent from + /// the current cursor. + /// + /// The current file cursor is not affected by this function. + /// + /// When writing beyond the end of the file, the file is appropriately + /// extended and the intermediate bytes are initialized with the value 0. + /// + /// It is not an error to return a short write. + /// + /// # Examples + /// + /// ```no_run + /// use tokio::fs::File; + /// use tokio::io::AsyncSeekExt; + /// + /// # async fn dox() -> std::io::Result<()> { + /// let mut file = File::open("foo.txt").await?; + /// file.write_at(b"foo", 5).await?; + /// + /// assert_eq!(file.stream_position().await.unwrap(), 0); + /// # Ok(()) + /// # } + /// ``` + #[cfg(unix)] + #[cfg_attr(docsrs, doc(cfg(unix)))] + pub async fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { + use std::os::unix::fs::FileExt; + + let std = self.std.clone(); + let buf_clone = buf.to_vec(); + asyncify(move || std.write_at(&buf_clone, offset)).await + } + + /// Seeks to a given position and reads a number of bytes. + /// + /// Returns the number of bytes read. + /// + /// The offset is relative to the start of the file and thus independent from + /// the current cursor. The current cursor is affected by this function, it + /// is set to the end of the read. + /// + /// Reading beyond the end of the file will always return with a length of 0. + /// + /// It is not an error to return with a short read. When returning from + /// such a short read, the file pointer is still updated. + /// + /// # Examples + /// + /// ```no_run + /// use tokio::fs::File; + /// use tokio::io::AsyncSeekExt; + /// + /// # async fn dox() -> std::io::Result<()> { + /// let mut file = File::open("foo.txt").await?; + /// let mut buf = vec![0_u8; 10]; + /// file.seek_read(&mut buf, 5).await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub async fn seek_read(&self, buf: &[u8], offset: u64) -> io::Result { + fn _read_at(std: &StdFile, n: usize, offset: u64) -> io::Result> { + use std::os::windows::fs::FileExt; + let mut buf: Vec = vec![0; n]; + let n_read = std.seek_read(&mut buf, offset)?; + buf.truncate(n_read); + + Ok(buf) + } + + let std = self.std.clone(); + let n = buf.len(); + let bytes_read = asyncify(move || _read_at(&std, n, offset)).await?; + let len = bytes_read.len(); + buf[..len].copy_from_slice(&bytes_read); + + Ok(len) + } + + /// Seeks to a given position and writes a number of bytes. + /// + /// Returns the number of bytes written. + /// + /// The offset is relative to the start of the file and thus independent from + /// the current cursor. The current cursor is affected by this function, it + /// is set to the end of the write. + /// + /// When writing beyond the end of the file, the file is appropriately + /// extended and the intermediate bytes are set to zero. + /// + /// It is not an error to return a short write. When returning from such a + /// short write, the file pointer is still updated. + /// + /// # Examples + /// + /// ```no_run + /// use tokio::fs::File; + /// use tokio::io::AsyncSeekExt; + /// + /// # async fn dox() -> std::io::Result<()> { + /// let mut file = File::open("foo.txt").await?; + /// file.seek_write(b"foo", 5).await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub async fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result { + use std::os::windows::fs::FileExt; + + let std = self.std.clone(); + let buf_clone = buf.to_vec(); + asyncify(move || std.seek_write(&buf_clone, offset)).await + } } impl AsyncRead for File { diff --git a/tokio/src/fs/mocks.rs b/tokio/src/fs/mocks.rs index a2ce1cd6ca3..235f314ed05 100644 --- a/tokio/src/fs/mocks.rs +++ b/tokio/src/fs/mocks.rs @@ -52,6 +52,18 @@ mock! { impl std::os::unix::io::FromRawFd for File { unsafe fn from_raw_fd(h: std::os::unix::io::RawFd) -> Self; } + + #[cfg(unix)] + impl std::os::unix::fs::FileExt for File { + fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result; + fn write_at(&self, buf: &[u8], offset: u64) -> io::Result; + } + + #[cfg(windows)] + impl std::os::windows::fs::FileExt for File { + fn seek_read(&self, buf: &mut [u8], offset: u64) -> io::Result; + fn seek_write(&self, buf: &[u8], offset: u64) -> io::Result; + } } impl Read for MockFile { diff --git a/tokio/tests/io_read_at.rs b/tokio/tests/io_read_at.rs new file mode 100644 index 00000000000..a1de9ad89db --- /dev/null +++ b/tokio/tests/io_read_at.rs @@ -0,0 +1,21 @@ +#![warn(rust_2018_idioms)] +#![cfg(all(feature = "full", not(target_os = "wasi")))] // WASM does support this, but it is nightly + +use tempfile::tempdir; +use tokio::fs; +use tokio::io::AsyncSeekExt; + +#[tokio::test] +#[cfg(unix)] +async fn read_at() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("a.txt"); + fs::write(&file_path, b"HelloWorld").await.unwrap(); + let mut file = fs::File::open(file_path.as_path()).await.unwrap(); + + let mut buf = [0_u8; 10]; + assert_eq!(file.read_at(&mut buf, 5).await.unwrap(), 5); + assert_eq!(&buf[..5], b"World"); + + assert_eq!(file.stream_position().await.unwrap(), 0); +} diff --git a/tokio/tests/io_write_at.rs b/tokio/tests/io_write_at.rs new file mode 100644 index 00000000000..a87f9a2da8d --- /dev/null +++ b/tokio/tests/io_write_at.rs @@ -0,0 +1,26 @@ +#![warn(rust_2018_idioms)] +#![cfg(all(feature = "full", not(target_os = "wasi")))] // WASM does support this, but it is nightly + +use tempfile::tempdir; +use tokio::fs; +use tokio::fs::OpenOptions; +use tokio::io::AsyncSeekExt; + +#[tokio::test] +#[cfg(unix)] +async fn write_at() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("a.txt"); + fs::write(&file_path, b"Hello File").await.unwrap(); + let mut file = OpenOptions::new() + .write(true) + .open(file_path.as_path()) + .await + .unwrap(); + + assert_eq!(file.write_at(b"World", 5).await.unwrap(), 5); + let contents = fs::read(file_path.as_path()).await.unwrap(); + assert_eq!(contents, b"HelloWorld"); + + assert_eq!(file.stream_position().await.unwrap(), 0); +}