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:
houseme
2026-04-07 08:33:46 +08:00
committed by GitHub
parent 8d27170ce4
commit 32bf8f5bf3
84 changed files with 15932 additions and 3592 deletions
+34 -6
View File
@@ -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==");
}
}
+94
View File
@@ -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);
}
}
+139 -43
View File
@@ -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;
+51 -1
View File
@@ -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";
+57
View File
@@ -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
View File
@@ -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";
+68 -36
View File
@@ -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
View File
@@ -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);
}
}