From 11a3c32b60c865769001d5b24a00e73eda5c5cfa Mon Sep 17 00:00:00 2001 From: Pavel Litvinenko Date: Thu, 15 Jan 2026 17:23:35 +0300 Subject: [PATCH 1/3] Add read_to for zero-allocation reads --- src/lib.rs | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7c978c1..07f4921 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -197,6 +197,30 @@ impl RandomAccessDisk { pub fn builder(filename: impl AsRef) -> Builder { Builder::new(filename) } + + /// Read bytes at `offset` into the provided `buf`. + /// + /// Returns the number of bytes read. This avoids allocating a new buffer + /// on each read, unlike [`RandomAccess::read`]. + pub async fn read_to( + &mut self, + offset: u64, + buf: &mut [u8], + ) -> Result { + let length = buf.len() as u64; + if offset + length > self.length { + return Err(RandomAccessError::OutOfBounds { + offset, + end: Some(offset + length), + length: self.length, + }); + } + + let file = self.file.as_mut().expect("self.file was None."); + file.seek(SeekFrom::Start(offset)).await?; + let bytes_read = file.read(buf).await?; + Ok(bytes_read) + } } #[async_trait::async_trait] @@ -235,18 +259,8 @@ impl RandomAccess for RandomAccessDisk { offset: u64, length: u64, ) -> Result, RandomAccessError> { - if offset + length > self.length { - return Err(RandomAccessError::OutOfBounds { - offset, - end: Some(offset + length), - length: self.length, - }); - } - - let file = self.file.as_mut().expect("self.file was None."); let mut buffer = vec![0; length as usize]; - file.seek(SeekFrom::Start(offset)).await?; - let _bytes_read = file.read(&mut buffer[..]).await?; + self.read_to(offset, &mut buffer).await?; Ok(buffer) } From b54542a9ac4ffd34a45fd130d627c87f98033506 Mon Sep 17 00:00:00 2001 From: Pavel Litvinenko Date: Thu, 15 Jan 2026 17:32:12 +0300 Subject: [PATCH 2/3] Add tests for read_to --- tests/test.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test.rs b/tests/test.rs index a0e67f4..d2222e9 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -371,3 +371,50 @@ async fn can_del_long_more_than_block() { .unwrap(); assert_eq!(5, file.len().await.unwrap()); } + +#[async_test] +async fn can_read_to() { + let dir = Builder::new() + .prefix("random-access-disk") + .tempdir() + .unwrap(); + let mut file = rad::RandomAccessDisk::open(dir.path().join("17.db")) + .await + .unwrap(); + file.write(0, b"hello").await.unwrap(); + file.write(5, b" world").await.unwrap(); + let mut buf = [0u8; 11]; + let bytes_read = file.read_to(0, &mut buf).await.unwrap(); + assert_eq!(bytes_read, 11); + assert_eq!(&buf, b"hello world"); +} + +#[async_test] +async fn can_read_to_partial() { + let dir = Builder::new() + .prefix("random-access-disk") + .tempdir() + .unwrap(); + let mut file = rad::RandomAccessDisk::open(dir.path().join("18.db")) + .await + .unwrap(); + file.write(0, b"hello world").await.unwrap(); + let mut buf = [0u8; 5]; + let bytes_read = file.read_to(6, &mut buf).await.unwrap(); + assert_eq!(bytes_read, 5); + assert_eq!(&buf, b"world"); +} + +#[async_test] +async fn read_to_out_of_bounds() { + let dir = Builder::new() + .prefix("random-access-disk") + .tempdir() + .unwrap(); + let mut file = rad::RandomAccessDisk::open(dir.path().join("19.db")) + .await + .unwrap(); + file.write(0, b"hello").await.unwrap(); + let mut buf = [0u8; 10]; + assert!(file.read_to(0, &mut buf).await.is_err()); +} From 282c56ad1d088b8f56f1a5be7e512cc4a4d4e7e6 Mon Sep 17 00:00:00 2001 From: Pavel Litvinenko Date: Thu, 15 Jan 2026 17:35:56 +0300 Subject: [PATCH 3/3] Update minor version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0520128..07d1679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" name = "random-access-disk" readme = "README.md" repository = "https://github.com/datrs/random-access-disk" -version = "3.0.1" +version = "3.1.0" edition = "2021" [dependencies]