mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 08:06:54 +00:00
feat(storage): add direct chunk GET fast path (#2351)
Signed-off-by: houseme <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: cxymds <Cxymds@qq.com>
This commit is contained in:
@@ -29,11 +29,21 @@ pub const RUSTFS_MULTIPART_CHECKSUM: &str = "x-rustfs-multipart-checksum";
|
||||
/// RustFS multipart checksum type metadata key
|
||||
pub const RUSTFS_MULTIPART_CHECKSUM_TYPE: &str = "x-rustfs-multipart-checksum-type";
|
||||
|
||||
const AMZ_CHECKSUM_ALGORITHM: &str = "x-amz-checksum-algorithm";
|
||||
const AMZ_SDK_CHECKSUM_ALGORITHM: &str = "x-amz-sdk-checksum-algorithm";
|
||||
|
||||
/// Checksum type enumeration with flags
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ChecksumType(pub u32);
|
||||
|
||||
impl ChecksumType {
|
||||
fn algorithm_from_headers(headers: &HeaderMap) -> Option<&str> {
|
||||
headers
|
||||
.get(AMZ_CHECKSUM_ALGORITHM)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.or_else(|| headers.get(AMZ_SDK_CHECKSUM_ALGORITHM).and_then(|v| v.to_str().ok()))
|
||||
}
|
||||
|
||||
/// Checksum will be sent in trailing header
|
||||
pub const TRAILING: ChecksumType = ChecksumType(1 << 0);
|
||||
|
||||
@@ -156,10 +166,7 @@ impl ChecksumType {
|
||||
|
||||
pub fn from_header(headers: &HeaderMap) -> Self {
|
||||
Self::from_string_with_obj_type(
|
||||
headers
|
||||
.get("x-amz-checksum-algorithm")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or(""),
|
||||
Self::algorithm_from_headers(headers).unwrap_or(""),
|
||||
headers.get("x-amz-checksum-type").and_then(|v| v.to_str().ok()).unwrap_or(""),
|
||||
)
|
||||
}
|
||||
@@ -573,7 +580,7 @@ pub fn get_content_checksum(headers: &HeaderMap) -> Result<Option<Checksum>, std
|
||||
fn get_content_checksum_direct(headers: &HeaderMap) -> (ChecksumType, String) {
|
||||
let mut checksum_type = ChecksumType::NONE;
|
||||
|
||||
if let Some(alg) = headers.get("x-amz-checksum-algorithm").and_then(|v| v.to_str().ok()) {
|
||||
if let Some(alg) = ChecksumType::algorithm_from_headers(headers) {
|
||||
checksum_type = ChecksumType::from_string_with_obj_type(
|
||||
alg,
|
||||
headers.get("x-amz-checksum-type").and_then(|s| s.to_str().ok()).unwrap_or(""),
|
||||
@@ -1131,7 +1138,8 @@ fn crc64_combine(poly: u64, crc1: u64, crc2: u64, len2: i64) -> u64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Checksum, ChecksumType};
|
||||
use super::{AMZ_SDK_CHECKSUM_ALGORITHM, Checksum, ChecksumType, get_content_checksum_direct};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
|
||||
#[test]
|
||||
fn crc64_nvme_add_part_matches_full_object_checksum() {
|
||||
@@ -1186,4 +1194,24 @@ mod tests {
|
||||
assert_eq!(combined.encoded, expected.encoded);
|
||||
assert_eq!(combined.raw, expected.raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checksum_type_from_header_supports_sdk_checksum_algorithm_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AMZ_SDK_CHECKSUM_ALGORITHM, HeaderValue::from_static("CRC32"));
|
||||
|
||||
assert_eq!(ChecksumType::from_header(&headers), ChecksumType::CRC32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_content_checksum_direct_supports_sdk_checksum_algorithm_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AMZ_SDK_CHECKSUM_ALGORITHM, HeaderValue::from_static("CRC32"));
|
||||
headers.insert("x-amz-checksum-crc32", HeaderValue::from_static("nct/nQ=="));
|
||||
|
||||
let (checksum_type, checksum_value) = get_content_checksum_direct(&headers);
|
||||
|
||||
assert_eq!(checksum_type, ChecksumType::CRC32);
|
||||
assert_eq!(checksum_value, "nct/nQ==");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::{BlockReadable, BoxReadBlockFuture, Reader};
|
||||
use pin_project_lite::pin_project;
|
||||
use rustfs_utils::compress::{CompressionAlgorithm, compress_block, decompress_block};
|
||||
use rustfs_utils::{put_uvarint, uvarint};
|
||||
@@ -85,6 +86,31 @@ where
|
||||
read_buffer: vec![0u8; block_size],
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_buffered(&mut self, buf: &mut [u8]) -> usize {
|
||||
if self.pos >= self.buffer.len() || buf.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let to_copy = min(buf.len(), self.buffer.len() - self.pos);
|
||||
buf[..to_copy].copy_from_slice(&self.buffer[self.pos..self.pos + to_copy]);
|
||||
self.pos += to_copy;
|
||||
if self.pos == self.buffer.len() {
|
||||
self.buffer.clear();
|
||||
self.pos = 0;
|
||||
}
|
||||
to_copy
|
||||
}
|
||||
|
||||
fn queue_compressed_block(&mut self, uncompressed_data: &[u8]) -> io::Result<()> {
|
||||
let out = build_compressed_block(uncompressed_data, self.compression_algorithm);
|
||||
self.written += out.len();
|
||||
self.uncomp_written += uncompressed_data.len();
|
||||
self.index.add(self.written as i64, self.uncomp_written as i64)?;
|
||||
self.buffer = out;
|
||||
self.pos = 0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> TryGetIndex for CompressReader<R> {
|
||||
@@ -170,6 +196,50 @@ where
|
||||
|
||||
delegate_reader_capabilities_generic_no_index!(CompressReader<R>, inner);
|
||||
|
||||
impl<R> BlockReadable for CompressReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut written = self.copy_buffered(buf);
|
||||
while written < buf.len() {
|
||||
if self.done {
|
||||
break;
|
||||
}
|
||||
|
||||
self.temp_buffer.resize(self.block_size, 0);
|
||||
let n = {
|
||||
let inner = &mut self.inner;
|
||||
let temp = &mut self.temp_buffer[..self.block_size];
|
||||
match inner.read_block(temp).await {
|
||||
Ok(n) => n,
|
||||
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => 0,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
};
|
||||
|
||||
if n == 0 {
|
||||
self.done = true;
|
||||
self.temp_buffer.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let block = self.temp_buffer[..n].to_vec();
|
||||
self.temp_buffer.clear();
|
||||
self.queue_compressed_block(&block)?;
|
||||
written += self.copy_buffered(&mut buf[written..]);
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// A reader wrapper that decompresses data on the fly using DEFLATE algorithm.
|
||||
/// Header format:
|
||||
@@ -390,6 +460,7 @@ fn build_compressed_block(uncompressed_data: &[u8], compression_algorithm: Compr
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BlockReadable, WarpReader};
|
||||
use rand::RngExt;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
@@ -479,4 +550,27 @@ mod tests {
|
||||
|
||||
assert_eq!(&decompressed, &data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compress_reader_read_block_round_trips() {
|
||||
let data = b"hello world, hello world, hello world!";
|
||||
let reader = Cursor::new(data.to_vec());
|
||||
let mut compress_reader = CompressReader::new(WarpReader::new(reader), CompressionAlgorithm::Gzip);
|
||||
let mut compressed = Vec::new();
|
||||
let mut buf = [0u8; 19];
|
||||
|
||||
loop {
|
||||
let n = compress_reader.read_block(&mut buf).await.unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
compressed.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
|
||||
let mut decompress_reader = DecompressReader::new(Cursor::new(compressed), CompressionAlgorithm::Gzip);
|
||||
let mut decompressed = Vec::new();
|
||||
decompress_reader.read_to_end(&mut decompressed).await.unwrap();
|
||||
|
||||
assert_eq!(&decompressed, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::{BlockReadable, BoxReadBlockFuture, Reader};
|
||||
use aes_gcm::aead::Aead;
|
||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||
use pin_project_lite::pin_project;
|
||||
@@ -59,6 +60,69 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt_segment_bytes(cipher: &Aes256Gcm, nonce_bytes: &[u8; 12], plaintext: &[u8]) -> std::io::Result<Vec<u8>> {
|
||||
let nonce = Nonce::try_from(nonce_bytes.as_slice()).map_err(|_| Error::other("invalid nonce length"))?;
|
||||
let plaintext_len = plaintext.len();
|
||||
let crc = {
|
||||
let mut hasher = crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32IsoHdlc);
|
||||
hasher.update(plaintext);
|
||||
hasher.finalize() as u32
|
||||
};
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, plaintext)
|
||||
.map_err(|e| Error::other(format!("encrypt error: {e}")))?;
|
||||
let int_len = put_uvarint_len(plaintext_len as u64);
|
||||
let clen = int_len + ciphertext.len() + 4;
|
||||
let mut header = [0u8; 8];
|
||||
header[0] = 0x00;
|
||||
header[1] = (clen & 0xFF) as u8;
|
||||
header[2] = ((clen >> 8) & 0xFF) as u8;
|
||||
header[3] = ((clen >> 16) & 0xFF) as u8;
|
||||
header[4] = (crc & 0xFF) as u8;
|
||||
header[5] = ((crc >> 8) & 0xFF) as u8;
|
||||
header[6] = ((crc >> 16) & 0xFF) as u8;
|
||||
header[7] = ((crc >> 24) & 0xFF) as u8;
|
||||
debug!(
|
||||
"encrypt block header typ=0 len={} header={:?} plaintext_len={} ciphertext_len={}",
|
||||
clen,
|
||||
header,
|
||||
plaintext_len,
|
||||
ciphertext.len()
|
||||
);
|
||||
let mut out = Vec::with_capacity(8 + int_len + ciphertext.len());
|
||||
out.extend_from_slice(&header);
|
||||
let mut plaintext_len_buf = vec![0u8; int_len];
|
||||
put_uvarint(&mut plaintext_len_buf, plaintext_len as u64);
|
||||
out.extend_from_slice(&plaintext_len_buf);
|
||||
out.extend_from_slice(&ciphertext);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
impl<R> EncryptReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn copy_buffered(&mut self, buf: &mut [u8]) -> usize {
|
||||
if self.buffer_pos >= self.buffer.len() || buf.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let to_copy = buf.len().min(self.buffer.len() - self.buffer_pos);
|
||||
buf[..to_copy].copy_from_slice(&self.buffer[self.buffer_pos..self.buffer_pos + to_copy]);
|
||||
self.buffer_pos += to_copy;
|
||||
if self.buffer_pos == self.buffer.len() {
|
||||
self.buffer.clear();
|
||||
self.buffer_pos = 0;
|
||||
}
|
||||
to_copy
|
||||
}
|
||||
|
||||
fn encrypt_segment(&self, plaintext: &[u8]) -> std::io::Result<Vec<u8>> {
|
||||
let nonce = derive_block_nonce(&self.base_nonce, self.block_index);
|
||||
encrypt_segment_bytes(&self.cipher, &nonce, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsyncRead for EncryptReader<R>
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + Sync,
|
||||
@@ -97,49 +161,8 @@ where
|
||||
*this.buffer_pos += to_copy;
|
||||
Poll::Ready(Ok(()))
|
||||
} else {
|
||||
// Encrypt the chunk
|
||||
let block_nonce = derive_block_nonce(this.base_nonce, *this.block_index);
|
||||
let nonce = Nonce::try_from(block_nonce.as_slice()).map_err(|_| Error::other("invalid nonce length"))?;
|
||||
let plaintext = &this.read_buffer[..n];
|
||||
let plaintext_len = plaintext.len();
|
||||
let crc = {
|
||||
let mut hasher = crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32IsoHdlc);
|
||||
hasher.update(plaintext);
|
||||
hasher.finalize() as u32
|
||||
};
|
||||
let ciphertext = this
|
||||
.cipher
|
||||
.encrypt(&nonce, plaintext)
|
||||
.map_err(|e| Error::other(format!("encrypt error: {e}")))?;
|
||||
let int_len = put_uvarint_len(plaintext_len as u64);
|
||||
let clen = int_len + ciphertext.len() + 4;
|
||||
// Header: 8 bytes
|
||||
// 0: type (0 = encrypted, 0xFF = end)
|
||||
// 1-3: length (little endian u24, ciphertext length)
|
||||
// 4-7: CRC32 of ciphertext (little endian u32)
|
||||
let mut header = [0u8; 8];
|
||||
header[0] = 0x00; // 0 = encrypted
|
||||
header[1] = (clen & 0xFF) as u8;
|
||||
header[2] = ((clen >> 8) & 0xFF) as u8;
|
||||
header[3] = ((clen >> 16) & 0xFF) as u8;
|
||||
header[4] = (crc & 0xFF) as u8;
|
||||
header[5] = ((crc >> 8) & 0xFF) as u8;
|
||||
header[6] = ((crc >> 16) & 0xFF) as u8;
|
||||
header[7] = ((crc >> 24) & 0xFF) as u8;
|
||||
debug!(
|
||||
"encrypt block header typ=0 len={} header={:?} plaintext_len={} ciphertext_len={}",
|
||||
clen,
|
||||
header,
|
||||
plaintext_len,
|
||||
ciphertext.len()
|
||||
);
|
||||
let mut out = Vec::with_capacity(8 + int_len + ciphertext.len());
|
||||
out.extend_from_slice(&header);
|
||||
let mut plaintext_len_buf = [0u8; 10];
|
||||
let encoded_len = put_uvarint(&mut plaintext_len_buf, plaintext_len as u64);
|
||||
out.extend_from_slice(&plaintext_len_buf[..encoded_len]);
|
||||
out.extend_from_slice(&ciphertext);
|
||||
*this.buffer = out;
|
||||
*this.buffer = encrypt_segment_bytes(this.cipher, &block_nonce, &this.read_buffer[..n])?;
|
||||
*this.buffer_pos = 0;
|
||||
*this.block_index += 1;
|
||||
let to_copy = std::cmp::min(buf.remaining(), this.buffer.len());
|
||||
@@ -164,6 +187,50 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for EncryptReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut written = self.copy_buffered(buf);
|
||||
while written < buf.len() {
|
||||
if self.finished {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut plaintext = vec![0u8; 8 * 1024];
|
||||
let n = match self.inner.read_block(&mut plaintext).await {
|
||||
Ok(n) => n,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => 0,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
if n == 0 {
|
||||
self.buffer = [0xFF, 0, 0, 0, 0, 0, 0, 0].to_vec();
|
||||
self.buffer_pos = 0;
|
||||
self.finished = true;
|
||||
} else {
|
||||
self.buffer = self.encrypt_segment(&plaintext[..n])?;
|
||||
self.buffer_pos = 0;
|
||||
}
|
||||
|
||||
let copied = self.copy_buffered(&mut buf[written..]);
|
||||
written += copied;
|
||||
|
||||
if copied == 0 && self.finished {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
/// A reader wrapper that decrypts data on the fly using AES-256-GCM.
|
||||
/// This is a demonstration. For production, use a secure and audited crypto library.
|
||||
@@ -486,7 +553,7 @@ mod tests {
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use crate::HardLimitReader;
|
||||
use crate::{BlockReadable, HardLimitReader, WarpReader};
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
@@ -674,6 +741,35 @@ mod tests {
|
||||
assert_eq!(&decrypted, &data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encrypt_reader_read_block_round_trips() {
|
||||
let data = b"hello sse encrypt via blocks";
|
||||
let mut key = [0u8; 32];
|
||||
let mut nonce = [0u8; 12];
|
||||
rand::rng().fill_bytes(&mut key);
|
||||
rand::rng().fill_bytes(&mut nonce);
|
||||
|
||||
let reader = Cursor::new(data.to_vec());
|
||||
let mut encrypt_reader = EncryptReader::new(WarpReader::new(reader), key, nonce);
|
||||
let mut encrypted = Vec::new();
|
||||
let mut buf = [0u8; 17];
|
||||
|
||||
loop {
|
||||
let n = encrypt_reader.read_block(&mut buf).await.unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
encrypted.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
|
||||
let reader = Cursor::new(encrypted);
|
||||
let mut decrypt_reader = DecryptReader::new(WarpReader::new(reader), key, nonce);
|
||||
let mut decrypted = Vec::new();
|
||||
decrypt_reader.read_to_end(&mut decrypted).await.unwrap();
|
||||
|
||||
assert_eq!(&decrypted, data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decrypt_reader_large_with_small_chunks() {
|
||||
let size = 1024 * 1024;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::{EtagResolvable, HashReaderDetector, HashReaderMut};
|
||||
use crate::{BlockReadable, BoxReadBlockFuture, EtagResolvable, HashReaderDetector, HashReaderMut, Reader};
|
||||
use md5::{Digest, Md5};
|
||||
use pin_project_lite::pin_project;
|
||||
use std::pin::Pin;
|
||||
@@ -135,9 +135,41 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for EtagReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let n = match self.inner.read_block(buf).await {
|
||||
Ok(n) => n,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => 0,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
if n > 0 {
|
||||
self.md5.update(&buf[..n]);
|
||||
return Ok(n);
|
||||
}
|
||||
|
||||
self.finished = true;
|
||||
if let Some(checksum) = &self.checksum {
|
||||
let etag = self.md5.clone().finalize().to_vec();
|
||||
let etag_hex = hex_simd::encode_to_string(etag, hex_simd::AsciiCase::Lower);
|
||||
if checksum != &etag_hex {
|
||||
error!("Checksum mismatch, expected={:?}, actual={:?}", checksum, etag_hex);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Checksum mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BlockReadable, WarpReader};
|
||||
use rand::RngExt;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
@@ -180,6 +212,24 @@ mod tests {
|
||||
assert_eq!(etag, Some(expected));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_etag_reader_read_block_updates_checksum() {
|
||||
let data = b"hello world";
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(data);
|
||||
let expected = faster_hex::hex_string(hasher.finalize().as_slice()).to_string();
|
||||
let reader = BufReader::new(&data[..]);
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut etag_reader = EtagReader::new(reader, Some(expected.clone()));
|
||||
|
||||
let mut buf = [0_u8; 32];
|
||||
let n = etag_reader.read_block(&mut buf).await.unwrap();
|
||||
assert_eq!(n, data.len());
|
||||
assert_eq!(&buf[..n], data);
|
||||
assert_eq!(etag_reader.read_block(&mut buf).await.unwrap(), 0);
|
||||
assert_eq!(etag_reader.try_resolve_etag(), Some(expected));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_etag_reader_multiple_get() {
|
||||
let data = b"abc123";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{BlockReadable, BoxReadBlockFuture, Reader};
|
||||
use pin_project_lite::pin_project;
|
||||
use std::io::{Error, Result};
|
||||
use std::pin::Pin;
|
||||
@@ -61,11 +62,51 @@ where
|
||||
|
||||
delegate_reader_capabilities_generic!(HardLimitReader<R>, inner);
|
||||
|
||||
impl<R> BlockReadable for HardLimitReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move {
|
||||
if self.remaining < 0 {
|
||||
return Err(Error::other("input provided more bytes than specified"));
|
||||
}
|
||||
|
||||
let max_len = match usize::try_from(self.remaining) {
|
||||
Ok(remaining) => remaining.min(buf.len()),
|
||||
Err(_) => buf.len(),
|
||||
};
|
||||
|
||||
if max_len == 0 {
|
||||
let mut probe = [0_u8; 1];
|
||||
match self.inner.read_block(&mut probe).await {
|
||||
Ok(0) => return Ok(0),
|
||||
Ok(n) => {
|
||||
self.remaining -= n as i64;
|
||||
return Err(Error::other("input provided more bytes than specified"));
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(0),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
let n = self.inner.read_block(&mut buf[..max_len]).await?;
|
||||
self.remaining -= n as i64;
|
||||
if self.remaining < 0 {
|
||||
return Err(Error::other("input provided more bytes than specified"));
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::{BlockReadable, WarpReader};
|
||||
use rustfs_utils::read_full;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
|
||||
@@ -128,4 +169,20 @@ mod tests {
|
||||
assert_eq!(n, 0);
|
||||
assert_eq!(&buf, data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hardlimit_reader_read_block_enforces_limit() {
|
||||
let data = b"abcdef";
|
||||
let reader = BufReader::new(&data[..]);
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hardlimit = HardLimitReader::new(reader, 3);
|
||||
|
||||
let mut buf = [0_u8; 8];
|
||||
let n = hardlimit.read_block(&mut buf).await.unwrap();
|
||||
assert_eq!(n, 3);
|
||||
assert_eq!(&buf[..n], b"abc");
|
||||
|
||||
let err = hardlimit.read_block(&mut buf).await.unwrap_err();
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::Other);
|
||||
}
|
||||
}
|
||||
|
||||
+179
-72
@@ -90,7 +90,10 @@ use crate::ChecksumType;
|
||||
use crate::Sha256Hasher;
|
||||
use crate::compress_index::{Index, TryGetIndex};
|
||||
use crate::get_content_checksum;
|
||||
use crate::{DynReader, EtagReader, EtagResolvable, HardLimitReader, HashReaderDetector, WarpReader, boxed_reader, wrap_reader};
|
||||
use crate::{
|
||||
BlockReadable, BoxReadBlockFuture, DynReader, EtagReader, EtagResolvable, HardLimitReader, HashReaderDetector, WarpReader,
|
||||
boxed_reader, wrap_reader,
|
||||
};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose;
|
||||
use http::HeaderMap;
|
||||
@@ -408,6 +411,23 @@ impl HashReader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_auto_checksum(&mut self, checksum_type: ChecksumType) -> Result<(), std::io::Error> {
|
||||
if !checksum_type.is_set() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(hasher) = checksum_type.hasher() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid checksum type"));
|
||||
};
|
||||
|
||||
self.content_hash = Some(Checksum {
|
||||
checksum_type,
|
||||
..Default::default()
|
||||
});
|
||||
self.content_hasher = Some(hasher);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn checksum(&self) -> Option<Checksum> {
|
||||
if self
|
||||
.content_hash
|
||||
@@ -449,6 +469,97 @@ impl HashReader {
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub fn finalize_content_hash(&mut self) -> std::io::Result<Option<Checksum>> {
|
||||
self.finish_checksum_validation()?;
|
||||
Ok(self.content_hash.clone())
|
||||
}
|
||||
|
||||
fn update_read_state(&mut self, data: &[u8]) -> std::io::Result<()> {
|
||||
self.bytes_read += data.len() as u64;
|
||||
|
||||
if data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(hasher) = self.content_sha256_hasher.as_mut() {
|
||||
hasher.write_all(data)?;
|
||||
}
|
||||
|
||||
if let Some(hasher) = self.content_hasher.as_mut() {
|
||||
hasher.write_all(data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_checksum_validation(&mut self) -> std::io::Result<()> {
|
||||
if self.checksum_on_finish {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let (Some(mut hasher), Some(expected_sha256)) = (self.content_sha256_hasher.take(), self.content_sha256.as_ref()) {
|
||||
let sha256 = hex_simd::encode_to_string(hasher.finalize(), hex_simd::AsciiCase::Lower);
|
||||
if sha256 != *expected_sha256 {
|
||||
error!("SHA256 mismatch, expected={:?}, actual={:?}", expected_sha256, sha256);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "SHA256 mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut expected_content_hash) = self.content_hash.clone()
|
||||
&& let Some(mut hasher) = self.content_hasher.take()
|
||||
{
|
||||
if expected_content_hash.checksum_type.trailing()
|
||||
&& let Some(trailer) = self.trailer_s3s.as_ref()
|
||||
&& let Some(Some(checksum_str)) = trailer.read(|headers| {
|
||||
expected_content_hash
|
||||
.checksum_type
|
||||
.key()
|
||||
.and_then(|key| headers.get(key).and_then(|value| value.to_str().ok().map(|s| s.to_string())))
|
||||
})
|
||||
{
|
||||
expected_content_hash.encoded = checksum_str;
|
||||
expected_content_hash.raw = general_purpose::STANDARD
|
||||
.decode(&expected_content_hash.encoded)
|
||||
.map_err(|_| std::io::Error::other("Invalid base64 checksum"))?;
|
||||
|
||||
if expected_content_hash.raw.is_empty() {
|
||||
return Err(std::io::Error::other("Content hash mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
let content_hash = hasher.finalize();
|
||||
if expected_content_hash.encoded.is_empty() {
|
||||
expected_content_hash.raw = content_hash.clone();
|
||||
expected_content_hash.encoded = general_purpose::STANDARD.encode(&content_hash);
|
||||
self.content_hash = Some(expected_content_hash);
|
||||
} else if content_hash != expected_content_hash.raw {
|
||||
let expected_hex = hex_simd::encode_to_string(&expected_content_hash.raw, hex_simd::AsciiCase::Lower);
|
||||
let actual_hex = hex_simd::encode_to_string(content_hash, hex_simd::AsciiCase::Lower);
|
||||
error!(
|
||||
"Content hash mismatch, type={:?}, encoded={:?}, expected={:?}, actual={:?}",
|
||||
expected_content_hash.checksum_type, expected_content_hash.encoded, expected_hex, actual_hex
|
||||
);
|
||||
let checksum_err = crate::errors::ChecksumMismatch {
|
||||
want: expected_hex,
|
||||
got: actual_hex,
|
||||
};
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, checksum_err));
|
||||
}
|
||||
}
|
||||
|
||||
self.checksum_on_finish = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_block(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let n = self.inner.read_block(buf).await?;
|
||||
self.update_read_state(&buf[..n])?;
|
||||
if n == 0 {
|
||||
self.finish_checksum_validation()?;
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl HashReaderMut for HashReader {
|
||||
@@ -508,84 +619,23 @@ impl HashReaderMut for HashReader {
|
||||
|
||||
impl AsyncRead for HashReader {
|
||||
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
let this = self.project();
|
||||
let this = self.get_mut();
|
||||
|
||||
let before = buf.filled().len();
|
||||
match this.inner.poll_read(cx, buf) {
|
||||
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Ok(())) => {
|
||||
let data = &buf.filled()[before..];
|
||||
let filled = data.len();
|
||||
|
||||
*this.bytes_read += filled as u64;
|
||||
|
||||
if filled > 0 {
|
||||
// Update SHA256 hasher
|
||||
if let Some(hasher) = this.content_sha256_hasher
|
||||
&& let Err(e) = hasher.write_all(data)
|
||||
{
|
||||
error!("SHA256 hasher write error, error={:?}", e);
|
||||
return Poll::Ready(Err(std::io::Error::other(e)));
|
||||
}
|
||||
|
||||
// Update content hasher
|
||||
if let Some(hasher) = this.content_hasher
|
||||
&& let Err(e) = hasher.write_all(data)
|
||||
{
|
||||
return Poll::Ready(Err(std::io::Error::other(e)));
|
||||
}
|
||||
if let Err(e) = this.update_read_state(data) {
|
||||
error!("hash reader state update error, error={:?}", e);
|
||||
return Poll::Ready(Err(std::io::Error::other(e)));
|
||||
}
|
||||
|
||||
if filled == 0 && !*this.checksum_on_finish {
|
||||
// check SHA256
|
||||
if let (Some(hasher), Some(expected_sha256)) = (this.content_sha256_hasher, this.content_sha256) {
|
||||
let sha256 = hex_simd::encode_to_string(hasher.finalize(), hex_simd::AsciiCase::Lower);
|
||||
if sha256 != *expected_sha256 {
|
||||
error!("SHA256 mismatch, expected={:?}, actual={:?}", expected_sha256, sha256);
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "SHA256 mismatch")));
|
||||
}
|
||||
}
|
||||
|
||||
// check content hasher
|
||||
if let (Some(hasher), Some(expected_content_hash)) = (this.content_hasher, this.content_hash) {
|
||||
if expected_content_hash.checksum_type.trailing()
|
||||
&& let Some(trailer) = this.trailer_s3s.as_ref()
|
||||
&& let Some(Some(checksum_str)) = trailer.read(|headers| {
|
||||
expected_content_hash
|
||||
.checksum_type
|
||||
.key()
|
||||
.and_then(|key| headers.get(key).and_then(|value| value.to_str().ok().map(|s| s.to_string())))
|
||||
})
|
||||
{
|
||||
expected_content_hash.encoded = checksum_str;
|
||||
expected_content_hash.raw = general_purpose::STANDARD
|
||||
.decode(&expected_content_hash.encoded)
|
||||
.map_err(|_| std::io::Error::other("Invalid base64 checksum"))?;
|
||||
|
||||
if expected_content_hash.raw.is_empty() {
|
||||
return Poll::Ready(Err(std::io::Error::other("Content hash mismatch")));
|
||||
}
|
||||
}
|
||||
|
||||
let content_hash = hasher.finalize();
|
||||
|
||||
if content_hash != expected_content_hash.raw {
|
||||
let expected_hex = hex_simd::encode_to_string(&expected_content_hash.raw, hex_simd::AsciiCase::Lower);
|
||||
let actual_hex = hex_simd::encode_to_string(content_hash, hex_simd::AsciiCase::Lower);
|
||||
error!(
|
||||
"Content hash mismatch, type={:?}, encoded={:?}, expected={:?}, actual={:?}",
|
||||
expected_content_hash.checksum_type, expected_content_hash.encoded, expected_hex, actual_hex
|
||||
);
|
||||
// Use ChecksumMismatch error so that API layer can return BadDigest
|
||||
let checksum_err = crate::errors::ChecksumMismatch {
|
||||
want: expected_hex,
|
||||
got: actual_hex,
|
||||
};
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, checksum_err)));
|
||||
}
|
||||
}
|
||||
|
||||
*this.checksum_on_finish = true;
|
||||
if filled == 0
|
||||
&& let Err(e) = this.finish_checksum_validation()
|
||||
{
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
@@ -623,13 +673,39 @@ impl TryGetIndex for HashReader {
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockReadable for HashReader {
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async move { self.read_block(buf).await })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{DecryptReader, EncryptReader, encrypt_reader, wrap_reader};
|
||||
use rand::RngExt;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, BufReader, ReadBuf};
|
||||
|
||||
struct UnexpectedEofReader;
|
||||
|
||||
impl AsyncRead for UnexpectedEofReader {
|
||||
fn poll_read(self: Pin<&mut Self>, _cx: &mut Context<'_>, _buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockReadable for UnexpectedEofReader {
|
||||
fn read_block<'a>(&'a mut self, _buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
Box::pin(async { Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "synthetic unexpected eof")) })
|
||||
}
|
||||
}
|
||||
|
||||
impl EtagResolvable for UnexpectedEofReader {}
|
||||
impl HashReaderDetector for UnexpectedEofReader {}
|
||||
impl TryGetIndex for UnexpectedEofReader {}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hashreader_wrapping_logic() {
|
||||
@@ -742,6 +818,37 @@ mod tests {
|
||||
assert_eq!(buf, data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hashreader_read_block_reads_full_and_tail_block() {
|
||||
let data = b"hello block reader";
|
||||
let reader = BufReader::new(Cursor::new(&data[..]));
|
||||
let reader = Box::new(WarpReader::new(reader));
|
||||
let mut hash_reader = HashReader::new(reader, data.len() as i64, data.len() as i64, None, None, false).unwrap();
|
||||
|
||||
let mut first = [0_u8; 8];
|
||||
let n1 = hash_reader.read_block(&mut first).await.unwrap();
|
||||
assert_eq!(n1, 8);
|
||||
assert_eq!(&first[..n1], b"hello bl");
|
||||
|
||||
let mut second = [0_u8; 32];
|
||||
let n2 = hash_reader.read_block(&mut second).await.unwrap();
|
||||
assert_eq!(n2, data.len() - n1);
|
||||
assert_eq!(&second[..n2], b"ock reader");
|
||||
|
||||
let n3 = hash_reader.read_block(&mut second).await.unwrap();
|
||||
assert_eq!(n3, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hashreader_read_block_propagates_unexpected_eof() {
|
||||
let mut hash_reader = HashReader::new(Box::new(UnexpectedEofReader), 0, 0, None, None, true).unwrap();
|
||||
let mut buf = [0_u8; 8];
|
||||
|
||||
let err = hash_reader.read_block(&mut buf).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hashreader_new_logic() {
|
||||
let data = b"test data";
|
||||
|
||||
@@ -23,7 +23,6 @@ use rustfs_utils::get_env_opt_str;
|
||||
use std::io::IoSlice;
|
||||
use std::io::{self, Error};
|
||||
use std::net::IpAddr;
|
||||
use std::ops::Not as _;
|
||||
use std::pin::Pin;
|
||||
use std::sync::LazyLock;
|
||||
use std::task::{Context, Poll};
|
||||
@@ -137,6 +136,71 @@ fn get_http_client(url: &str) -> Client {
|
||||
CLIENT.clone()
|
||||
}
|
||||
|
||||
type HttpByteStream = Pin<Box<dyn Stream<Item = std::io::Result<Bytes>> + Send + Sync>>;
|
||||
|
||||
async fn request_http_byte_stream(
|
||||
url: String,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
body: Option<Vec<u8>>,
|
||||
meter_stream_recv_bytes: bool,
|
||||
) -> io::Result<(bool, HttpByteStream)> {
|
||||
let track_internode_metrics = is_internode_rpc_url(&url);
|
||||
let client = get_http_client(&url);
|
||||
let mut request: RequestBuilder = client.request(method, url.clone()).headers(headers);
|
||||
if let Some(body) = body {
|
||||
request = request.body(body);
|
||||
}
|
||||
|
||||
let resp = request.send().await.map_err(|e| {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
Error::other(format!("HttpReader HTTP request error: {e}"))
|
||||
})?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
return Err(Error::other(format!(
|
||||
"HttpReader HTTP request failed with non-200 status {}",
|
||||
resp.status()
|
||||
)));
|
||||
}
|
||||
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_outgoing_request();
|
||||
}
|
||||
|
||||
let stream = resp
|
||||
.bytes_stream()
|
||||
.map_ok(move |bytes| {
|
||||
if track_internode_metrics && meter_stream_recv_bytes {
|
||||
global_internode_metrics().record_recv_bytes(bytes.len());
|
||||
}
|
||||
bytes
|
||||
})
|
||||
.map_err(move |e| {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
Error::other(format!("HttpReader stream error: {e}"))
|
||||
});
|
||||
|
||||
Ok((track_internode_metrics, Box::pin(stream)))
|
||||
}
|
||||
|
||||
pub async fn open_http_byte_stream(
|
||||
url: String,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> io::Result<HttpByteStream> {
|
||||
let (_track_internode_metrics, stream) = request_http_byte_stream(url, method, headers, body, true).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
pub struct HttpReader {
|
||||
url:String,
|
||||
@@ -161,43 +225,11 @@ impl HttpReader {
|
||||
body: Option<Vec<u8>>,
|
||||
_read_buf_size: usize,
|
||||
) -> io::Result<Self> {
|
||||
let track_internode_metrics = is_internode_rpc_url(&url);
|
||||
let client = get_http_client(&url);
|
||||
let mut request: RequestBuilder = client.request(method.clone(), url.clone()).headers(headers.clone());
|
||||
if let Some(body) = body {
|
||||
request = request.body(body);
|
||||
}
|
||||
|
||||
let resp = request.send().await.map_err(|e| {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
Error::other(format!("HttpReader HTTP request error: {e}"))
|
||||
})?;
|
||||
|
||||
if resp.status().is_success().not() {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
return Err(Error::other(format!(
|
||||
"HttpReader HTTP request failed with non-200 status {}",
|
||||
resp.status()
|
||||
)));
|
||||
}
|
||||
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_outgoing_request();
|
||||
}
|
||||
|
||||
let stream = resp.bytes_stream().map_err(move |e| {
|
||||
if track_internode_metrics {
|
||||
global_internode_metrics().record_error();
|
||||
}
|
||||
Error::other(format!("HttpReader stream error: {e}"))
|
||||
});
|
||||
let (track_internode_metrics, stream) =
|
||||
request_http_byte_stream(url.clone(), method.clone(), headers.clone(), body, false).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: StreamReader::new(Box::pin(stream)),
|
||||
inner: StreamReader::new(stream),
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
|
||||
+138
-2
@@ -15,6 +15,10 @@
|
||||
// Default encryption block size - aligned with system default read buffer size (1MB)
|
||||
pub const DEFAULT_ENCRYPTION_BLOCK_SIZE: usize = 1024 * 1024;
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
macro_rules! delegate_reader_capabilities_generic {
|
||||
($name:ident<$inner_ty:ident>, $inner:ident) => {
|
||||
impl<$inner_ty> crate::EtagResolvable for $name<$inner_ty>
|
||||
@@ -114,14 +118,39 @@ pub use compress_index::{Index, TryGetIndex};
|
||||
|
||||
mod etag;
|
||||
|
||||
pub type BoxReadBlockFuture<'a> = Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>>;
|
||||
|
||||
pub trait BlockReadable {
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a>;
|
||||
}
|
||||
|
||||
fn read_block_via_async_read<'a, R>(reader: &'a mut R, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + Sync + 'a,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let mut total = 0;
|
||||
|
||||
while total < buf.len() {
|
||||
match reader.read(&mut buf[total..]).await {
|
||||
Ok(0) => return Ok(total),
|
||||
Ok(n) => total += n,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
})
|
||||
}
|
||||
|
||||
pub trait ReadStream: tokio::io::AsyncRead + Unpin + Send + Sync {}
|
||||
impl<T> ReadStream for T where T: tokio::io::AsyncRead + Unpin + Send + Sync {}
|
||||
|
||||
pub trait ReaderCapabilities: EtagResolvable + HashReaderDetector + TryGetIndex {}
|
||||
impl<T> ReaderCapabilities for T where T: EtagResolvable + HashReaderDetector + TryGetIndex {}
|
||||
|
||||
pub trait Reader: ReadStream + ReaderCapabilities {}
|
||||
impl<T> Reader for T where T: ReadStream + ReaderCapabilities {}
|
||||
pub trait Reader: ReadStream + ReaderCapabilities + BlockReadable {}
|
||||
impl<T> Reader for T where T: ReadStream + ReaderCapabilities + BlockReadable {}
|
||||
|
||||
pub type DynReader = Box<dyn Reader>;
|
||||
|
||||
@@ -154,6 +183,42 @@ pub trait HashReaderDetector {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for crate::WarpReader<R>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + Sync,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
read_block_via_async_read(self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for tokio::io::BufReader<R>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + Sync,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
read_block_via_async_read(self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for crate::LimitReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
read_block_via_async_read(self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> BlockReadable for crate::DecryptReader<R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
read_block_via_async_read(self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boxed_reader<R>(reader: R) -> DynReader
|
||||
where
|
||||
R: Reader + 'static,
|
||||
@@ -198,3 +263,74 @@ where
|
||||
self.as_ref().try_get_index()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BlockReadable for Box<T>
|
||||
where
|
||||
T: BlockReadable + ?Sized,
|
||||
{
|
||||
fn read_block<'a>(&'a mut self, buf: &'a mut [u8]) -> BoxReadBlockFuture<'a> {
|
||||
self.as_mut().read_block(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
|
||||
enum ReadStep {
|
||||
Data(Vec<u8>),
|
||||
Error(ErrorKind),
|
||||
Eof,
|
||||
}
|
||||
|
||||
struct StepReader {
|
||||
steps: VecDeque<ReadStep>,
|
||||
}
|
||||
|
||||
impl StepReader {
|
||||
fn new(steps: impl IntoIterator<Item = ReadStep>) -> Self {
|
||||
Self {
|
||||
steps: steps.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for StepReader {
|
||||
fn poll_read(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
|
||||
match self.steps.pop_front().unwrap_or(ReadStep::Eof) {
|
||||
ReadStep::Data(data) => {
|
||||
buf.put_slice(&data);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
ReadStep::Error(kind) => Poll::Ready(Err(io::Error::new(kind, "synthetic read failure"))),
|
||||
ReadStep::Eof => Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_block_via_async_read_preserves_midstream_error_kind() {
|
||||
let reader = StepReader::new([ReadStep::Data(b"ab".to_vec()), ReadStep::Error(ErrorKind::ConnectionReset)]);
|
||||
let mut reader = WarpReader::new(reader);
|
||||
let mut buf = [0_u8; 4];
|
||||
|
||||
let err = reader.read_block(&mut buf).await.unwrap_err();
|
||||
|
||||
assert_eq!(err.kind(), ErrorKind::ConnectionReset);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_block_via_async_read_returns_zero_on_initial_eof() {
|
||||
let mut reader = WarpReader::new(StepReader::new([ReadStep::Eof]));
|
||||
let mut buf = [0_u8; 4];
|
||||
|
||||
let n = reader.read_block(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user