mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 16:48:58 +00:00
413 lines
13 KiB
Rust
413 lines
13 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Bytes-backed object reader implementation.
|
|
|
|
use bytes::Bytes;
|
|
use std::io;
|
|
use std::pin::Pin;
|
|
use std::task::{Context, Poll};
|
|
use tokio::io::{AsyncRead, ReadBuf};
|
|
|
|
/// Errors that can occur during Bytes-backed read operations.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ZeroCopyReadError {
|
|
/// I/O error occurred.
|
|
Io(String),
|
|
/// Memory mapping error.
|
|
Mmap(String),
|
|
/// Invalid offset or size.
|
|
InvalidRange,
|
|
}
|
|
|
|
impl std::fmt::Display for ZeroCopyReadError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Io(msg) => write!(f, "I/O error: {}", msg),
|
|
Self::Mmap(msg) => write!(f, "Mmap error: {}", msg),
|
|
Self::InvalidRange => write!(f, "Invalid offset or size"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ZeroCopyReadError {}
|
|
|
|
impl From<io::Error> for ZeroCopyReadError {
|
|
fn from(err: io::Error) -> Self {
|
|
Self::Io(err.to_string())
|
|
}
|
|
}
|
|
|
|
/// Bytes-backed object reader.
|
|
///
|
|
/// `from_bytes` wraps existing `Bytes` without copying, but file constructors
|
|
/// copy file data into owned `Bytes` after mmap or normal reads.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// use bytes::Bytes;
|
|
/// use rustfs_io_core::BytesBufferedReader;
|
|
///
|
|
/// // Create from bytes without copying the `Bytes` buffer
|
|
/// let data = Bytes::from("hello world");
|
|
/// let reader = BytesBufferedReader::from_bytes(data);
|
|
///
|
|
/// // Read using AsyncRead trait
|
|
/// let mut buf = vec![0u8; 1024];
|
|
/// let n = reader.read(&mut buf[..]).await?;
|
|
/// ```
|
|
pub struct BytesBufferedReader {
|
|
/// Internal data source (could be mmap or owned bytes)
|
|
data: Bytes,
|
|
/// Current read position
|
|
pos: usize,
|
|
}
|
|
|
|
/// Historical name for the bytes-backed object reader.
|
|
#[deprecated(
|
|
since = "1.0.0-beta.8",
|
|
note = "use BytesBufferedReader; file constructors copy into owned Bytes"
|
|
)]
|
|
pub type ZeroCopyObjectReader = BytesBufferedReader;
|
|
|
|
impl BytesBufferedReader {
|
|
/// Create a reader from existing bytes.
|
|
///
|
|
/// This is a true zero-copy operation - the Bytes are wrapped
|
|
/// without any allocation or copying.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `data` - Bytes to wrap
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let data = Bytes::from("hello world");
|
|
/// let reader = BytesBufferedReader::from_bytes(data);
|
|
/// ```
|
|
pub fn from_bytes(data: Bytes) -> Self {
|
|
Self { data, pos: 0 }
|
|
}
|
|
|
|
/// Create a Bytes-backed reader from a file using mmap-then-copy.
|
|
///
|
|
/// This maps the requested file range and copies it into owned `Bytes`
|
|
/// before returning. It does not expose the mmap as a zero-copy buffer.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `path` - Path to the file to memory map
|
|
/// * `offset` - Offset within the file to start reading
|
|
/// * `size` - Number of bytes to read
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A reader backed by copied file data.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the file cannot be memory mapped.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let reader = BytesBufferedReader::from_file_mmap_path("large_file.bin", 0, 1024).await?;
|
|
/// ```
|
|
#[cfg(unix)]
|
|
// SAFETY: The mmap is created from a read-only file handle for the
|
|
// caller-provided range, then copied into owned `Bytes` before the file and
|
|
// mapping are dropped.
|
|
#[allow(unsafe_code)]
|
|
pub async fn from_file_mmap_path(path: &std::path::Path, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
|
|
use memmap2::MmapOptions;
|
|
|
|
let path = path.to_path_buf();
|
|
let (offset, size) = (offset, size);
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
// Open the file in sync context
|
|
let std_file = std::fs::File::open(&path).map_err(|e| ZeroCopyReadError::Io(e.to_string()))?;
|
|
|
|
// SAFETY: `std_file` remains open while the mapping is created and
|
|
// copied, and the mapped bytes are not exposed beyond this closure.
|
|
let mmap = unsafe { MmapOptions::new().offset(offset).len(size).map(&std_file) }
|
|
.map_err(|e| ZeroCopyReadError::Mmap(e.to_string()))?;
|
|
|
|
// Convert to Bytes (this is a copy, but only done once)
|
|
Ok(Self {
|
|
data: Bytes::copy_from_slice(&mmap),
|
|
pos: 0,
|
|
})
|
|
})
|
|
.await
|
|
.map_err(|e| ZeroCopyReadError::Io(e.to_string()))?
|
|
}
|
|
|
|
/// Create a Bytes-backed reader from a file using normal reads.
|
|
///
|
|
/// This path reads the requested range into an owned buffer and wraps it in
|
|
/// `Bytes`. It does not perform mmap or zero-copy file I/O.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `file` - File to read from
|
|
/// * `offset` - Offset within the file to start reading
|
|
/// * `size` - Number of bytes to map
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A reader backed by copied file data.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the file cannot be read.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let file = tokio::fs::File::open("large_file.bin").await?;
|
|
/// let reader = BytesBufferedReader::from_file_read(&file, 0, 1024).await?;
|
|
/// ```
|
|
#[cfg(unix)]
|
|
pub async fn from_file_read(file: &tokio::fs::File, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
|
|
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
|
|
|
let mut cloned = file.try_clone().await?;
|
|
cloned.seek(SeekFrom::Start(offset)).await?;
|
|
|
|
let mut buffer = vec![0u8; size];
|
|
cloned.read_exact(&mut buffer).await?;
|
|
|
|
Ok(Self {
|
|
data: Bytes::from(buffer),
|
|
pos: 0,
|
|
})
|
|
}
|
|
|
|
/// Create a Bytes-backed reader from a file (non-Unix fallback).
|
|
///
|
|
/// On platforms that don't support mmap, this falls back to regular file I/O.
|
|
#[cfg(not(unix))]
|
|
pub async fn from_file_read(file: &tokio::fs::File, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
|
|
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
|
|
|
let mut cloned = file.try_clone().await?;
|
|
cloned.seek(SeekFrom::Start(offset)).await?;
|
|
|
|
let mut buffer = vec![0u8; size];
|
|
cloned.read_exact(&mut buffer).await?;
|
|
|
|
Ok(Self {
|
|
data: Bytes::from(buffer),
|
|
pos: 0,
|
|
})
|
|
}
|
|
|
|
/// Historical name for `from_file_read`.
|
|
#[deprecated(
|
|
since = "1.0.0-beta.8",
|
|
note = "use from_file_read; this method performs normal reads into owned Bytes"
|
|
)]
|
|
pub async fn from_file_mmap(file: &tokio::fs::File, offset: u64, size: usize) -> Result<Self, ZeroCopyReadError> {
|
|
Self::from_file_read(file, offset, size).await
|
|
}
|
|
|
|
/// Get the remaining data as Bytes (zero-copy).
|
|
///
|
|
/// This returns a slice of the remaining data without copying.
|
|
/// The returned Bytes shares the underlying memory with this reader.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let remaining = reader.remaining_bytes();
|
|
/// println!("Remaining: {} bytes", remaining.len());
|
|
/// ```
|
|
pub fn remaining_bytes(&self) -> Bytes {
|
|
self.data.slice(self.pos..)
|
|
}
|
|
|
|
/// Get the total length of the data.
|
|
pub fn len(&self) -> usize {
|
|
self.data.len()
|
|
}
|
|
|
|
/// Check if the reader has reached the end.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.pos >= self.data.len()
|
|
}
|
|
|
|
/// Get the current read position.
|
|
pub fn position(&self) -> usize {
|
|
self.pos
|
|
}
|
|
}
|
|
|
|
impl AsyncRead for BytesBufferedReader {
|
|
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
|
let remaining = self.data.len() - self.pos;
|
|
if remaining == 0 {
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
|
|
let to_read = std::cmp::min(remaining, buf.remaining());
|
|
let slice = &self.data[self.pos..self.pos + to_read];
|
|
buf.put_slice(slice);
|
|
self.pos += to_read;
|
|
|
|
Poll::Ready(Ok(()))
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for BytesBufferedReader {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("BytesBufferedReader")
|
|
.field("data_len", &self.data.len())
|
|
.field("pos", &self.pos)
|
|
.field("remaining", &(self.data.len() - self.pos))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::PathBuf;
|
|
use tokio::io::AsyncReadExt;
|
|
|
|
fn temp_file_path(test_name: &str) -> PathBuf {
|
|
let nonce = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("system time should be after unix epoch")
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!("rustfs-io-core-{test_name}-{}-{nonce}", std::process::id()))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_from_bytes() {
|
|
let data = Bytes::from("hello world");
|
|
let mut reader = BytesBufferedReader::from_bytes(data.clone());
|
|
|
|
let mut buf = [0u8; 11];
|
|
let n = reader.read(&mut buf[..]).await.unwrap();
|
|
|
|
assert_eq!(n, 11);
|
|
assert_eq!(&buf[..n], b"hello world");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_preferred_reader_alias() {
|
|
let data = Bytes::from("hello world");
|
|
let mut reader = BytesBufferedReader::from_bytes(data);
|
|
|
|
let mut buf = [0u8; 5];
|
|
let n = reader.read(&mut buf[..]).await.expect("read bytes from alias");
|
|
|
|
assert_eq!(n, 5);
|
|
assert_eq!(&buf[..n], b"hello");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_from_file_read_reads_requested_range() {
|
|
let path = temp_file_path("from-file-read");
|
|
tokio::fs::write(&path, b"hello world")
|
|
.await
|
|
.expect("write temp file for reader test");
|
|
|
|
let file = tokio::fs::File::open(&path).await.expect("open temp file for reader test");
|
|
let mut reader = BytesBufferedReader::from_file_read(&file, 6, 5)
|
|
.await
|
|
.expect("read requested range into Bytes");
|
|
|
|
let mut output = Vec::new();
|
|
reader.read_to_end(&mut output).await.expect("drain reader output");
|
|
|
|
assert_eq!(output, b"world");
|
|
|
|
let _ = tokio::fs::remove_file(path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[allow(deprecated)]
|
|
async fn test_from_file_mmap_legacy_alias_reads_requested_range() {
|
|
let path = temp_file_path("from-file-mmap");
|
|
tokio::fs::write(&path, b"hello world")
|
|
.await
|
|
.expect("write temp file for legacy reader test");
|
|
|
|
let file = tokio::fs::File::open(&path)
|
|
.await
|
|
.expect("open temp file for legacy reader test");
|
|
let mut reader = BytesBufferedReader::from_file_mmap(&file, 0, 5)
|
|
.await
|
|
.expect("read requested range through legacy alias");
|
|
|
|
let mut output = Vec::new();
|
|
reader.read_to_end(&mut output).await.expect("drain legacy reader output");
|
|
|
|
assert_eq!(output, b"hello");
|
|
|
|
let _ = tokio::fs::remove_file(path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_remaining_bytes() {
|
|
let data = Bytes::from("hello world");
|
|
let reader = BytesBufferedReader::from_bytes(data);
|
|
|
|
let remaining = reader.remaining_bytes();
|
|
assert_eq!(remaining.len(), 11);
|
|
assert_eq!(&remaining[..], b"hello world");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_position() {
|
|
let data = Bytes::from("hello world");
|
|
let mut reader = BytesBufferedReader::from_bytes(data);
|
|
|
|
assert_eq!(reader.position(), 0);
|
|
|
|
let mut buf = [0u8; 5];
|
|
reader.read_exact(&mut buf[..]).await.unwrap();
|
|
|
|
assert_eq!(reader.position(), 5);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_is_empty() {
|
|
let data = Bytes::from("");
|
|
let reader = BytesBufferedReader::from_bytes(data);
|
|
assert!(reader.is_empty());
|
|
|
|
let data = Bytes::from("hello");
|
|
let reader = BytesBufferedReader::from_bytes(data);
|
|
assert!(!reader.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[allow(deprecated)]
|
|
async fn test_legacy_reader_alias() {
|
|
let data = Bytes::from("hello world");
|
|
let mut reader = ZeroCopyObjectReader::from_bytes(data);
|
|
|
|
let mut buf = [0u8; 5];
|
|
let n = reader.read(&mut buf[..]).await.expect("read bytes through legacy alias");
|
|
|
|
assert_eq!(n, 5);
|
|
assert_eq!(&buf[..n], b"hello");
|
|
}
|
|
}
|