use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio::sync::Mutex;
use super::crc::CrcDb;
use super::header::{detect_ascii_header_corruption, is_ascii_corruption_value};
use super::source::BlockSource;
use super::types::SSTableReaderConfig;
use crate::{Error, Result};
const UNCOMPRESSED_READ_PIECE_BYTES: usize = 64 * 1024;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn read_next_block(
file: &Arc<Mutex<BlockSource>>,
cassandra_version: &crate::parser::header::CassandraVersion,
config: &SSTableReaderConfig,
compression_info: &Option<Arc<crate::storage::sstable::compression_info::CompressionInfo>>,
crc_reader: Option<&CrcDb>,
current_chunk_index: &std::sync::atomic::AtomicUsize,
header_offset: u64,
scratch: &mut Vec<u8>,
) -> Result<Option<Vec<u8>>> {
let original_pos = {
let mut guard = file.lock().await;
guard.stream_position().await.map_err(Error::Io)?
};
let mut attempt = 0;
loop {
if attempt == 1 {
let mut guard = file.lock().await;
guard
.seek(std::io::SeekFrom::Start(original_pos))
.await
.map_err(Error::Io)?;
}
let r = read_next_block_impl(
file,
cassandra_version,
config,
compression_info,
crc_reader,
current_chunk_index,
header_offset,
scratch,
)
.await;
match r {
Err(e) if attempt == 0 && is_transient_io(&e) => {
tracing::warn!(
"transient I/O fault ({e}); re-seeking to offset {original_pos} and retrying once"
);
attempt = 1;
}
other => return other,
}
}
}
pub(super) fn is_transient_io(e: &Error) -> bool {
match e {
Error::Io(io) => matches!(
io.kind(),
std::io::ErrorKind::Interrupted
| std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
),
_ => false,
}
}
fn io_error_with_context(context: impl std::fmt::Display, source: std::io::Error) -> Error {
let kind = source.kind();
Error::Io(std::io::Error::new(kind, format!("{context}: {source}")))
}
#[allow(clippy::too_many_arguments)]
async fn read_next_block_impl(
file: &Arc<Mutex<BlockSource>>,
cassandra_version: &crate::parser::header::CassandraVersion,
config: &SSTableReaderConfig,
compression_info: &Option<Arc<crate::storage::sstable::compression_info::CompressionInfo>>,
crc_reader: Option<&CrcDb>,
current_chunk_index: &std::sync::atomic::AtomicUsize,
_header_offset: u64, scratch: &mut Vec<u8>,
) -> Result<Option<Vec<u8>>> {
tracing::debug!("block_io::read_next_block_impl: Starting block read");
tracing::debug!(
"block_io::read_next_block_impl: Cassandra version: {:?}",
cassandra_version
);
if matches!(
cassandra_version,
crate::parser::header::CassandraVersion::V5_0Uncompressed
) {
tracing::debug!("block_io::read_next_block_impl: Using uncompressed direct read");
return read_uncompressed_data_block(file, config, false, crc_reader).await;
}
let is_bti = matches!(
cassandra_version,
crate::parser::header::CassandraVersion::V5_0Bti
);
if is_bti && compression_info.is_none() {
tracing::debug!("block_io::read_next_block_impl: BTI without CompressionInfo, direct read");
return read_uncompressed_data_block(file, config, false, crc_reader).await;
}
if cassandra_version.is_nb_format() || is_bti {
tracing::debug!("block_io::read_next_block_impl: Using NB/BTI format chunk reader");
let file_size = {
let mut file_guard = file.lock().await;
file_guard.len().await?
};
return read_nb_format_chunk_data(
file,
config,
compression_info,
crc_reader,
current_chunk_index,
file_size,
0, scratch,
)
.await;
}
let block_header = match cassandra_version {
crate::parser::header::CassandraVersion::V5_0Bti => {
tracing::debug!("block_io::read_next_block_impl: Using BTI format block header reader");
read_bti_format_block_header(file).await?
}
_ => {
tracing::debug!(
"block_io::read_next_block_impl: Using legacy format block header reader"
);
read_legacy_format_block_header(file).await?
}
};
let Some((compressed_size, checksum, current_pos)) = block_header else {
tracing::debug!("block_io::read_next_block_impl: Block header returned None (EOF)");
return Ok(None); };
tracing::debug!(
"block_io::read_next_block_impl: Block header: compressed_size={}, checksum={}, pos={}",
compressed_size,
checksum,
current_pos
);
if compressed_size > 64 * 1024 * 1024 {
return Err(Error::corruption(format!(
"Block size too large: {} bytes (limit: 64MB)",
compressed_size
)));
}
if is_ascii_corruption_value(compressed_size) {
return Err(Error::corruption(format!(
"Block size appears to be ASCII corruption: {} (0x{:08x}) - likely misaligned file reading",
compressed_size, compressed_size
)));
}
if compressed_size == 0 {
tracing::info!("Encountered empty block at position {}", current_pos);
return Ok(Some(Vec::new()));
}
let block_data = if compressed_size > config.read_buffer_size as u32 {
read_large_block_streaming(file, compressed_size as usize, config).await?
} else {
read_block_direct(file, compressed_size as usize).await?
};
if config.validate_checksums && checksum != 0 {
let computed_checksum = crc32fast::hash(&block_data);
if computed_checksum != checksum {
return Err(Error::corruption(format!(
"Block checksum mismatch at position {}: expected 0x{:08x}, got 0x{:08x}",
current_pos, checksum, computed_checksum
)));
}
tracing::debug!("Block checksum validated: 0x{:08x}", checksum);
}
tracing::debug!(
"Successfully read block: {} bytes at position {}",
block_data.len(),
current_pos
);
Ok(Some(block_data))
}
#[allow(clippy::too_many_arguments)]
async fn read_nb_format_chunk_data(
file: &Arc<Mutex<BlockSource>>,
config: &SSTableReaderConfig,
compression_info: &Option<Arc<crate::storage::sstable::compression_info::CompressionInfo>>,
crc_reader: Option<&CrcDb>,
current_chunk_index: &std::sync::atomic::AtomicUsize,
file_size: u64,
header_offset: u64,
scratch: &mut Vec<u8>,
) -> Result<Option<Vec<u8>>> {
tracing::debug!("read_nb_format_chunk_data: Starting chunk read");
let Some(comp_info) = compression_info else {
tracing::debug!(
"read_nb_format_chunk_data: No CompressionInfo.db, falling back to raw data read"
);
return read_uncompressed_data_block(file, config, true, crc_reader).await;
};
let chunk_idx = current_chunk_index.load(std::sync::atomic::Ordering::Relaxed);
if chunk_idx >= comp_info.chunk_offsets.len() {
tracing::debug!(
"read_nb_format_chunk_data: All chunks read ({}/{})",
chunk_idx,
comp_info.chunk_offsets.len()
);
return Ok(None); }
let chunk_length = comp_info.chunk_length as u64;
if chunk_length > 0 {
let logical_start = (chunk_idx as u64).saturating_mul(chunk_length);
if logical_start >= comp_info.data_length {
tracing::debug!(
"read_nb_format_chunk_data: chunk {} is a degenerate empty trailing chunk \
(logical_start={} >= data_length={}); treating as EOF (issue #2225)",
chunk_idx,
logical_start,
comp_info.data_length
);
return Ok(None);
}
}
tracing::debug!(
"read_nb_format_chunk_data: Reading chunk {}/{}",
chunk_idx,
comp_info.chunk_offsets.len()
);
let chunk_offset = comp_info
.compressed_chunk_offset(chunk_idx)
.ok_or_else(|| Error::InvalidFormat(format!("No offset for chunk {}", chunk_idx)))?;
tracing::debug!(
"read_nb_format_chunk_data: Chunk {} offset: 0x{:x}",
chunk_idx,
chunk_offset
);
let total_chunk_size = comp_info
.compressed_chunk_size(chunk_idx, file_size)
.ok_or_else(|| {
Error::InvalidFormat(format!(
"Cannot determine size for chunk {} (file_size={})",
chunk_idx, file_size
))
})?;
if total_chunk_size < 4 {
return Err(Error::InvalidFormat(format!(
"Chunk {} size too small: {} bytes (minimum 4 for CRC)",
chunk_idx, total_chunk_size
)));
}
let chunk_end = chunk_offset
.checked_add(header_offset)
.and_then(|abs| abs.checked_add(total_chunk_size));
match chunk_end {
Some(end) if end <= file_size => {}
_ => {
return Err(Error::InvalidFormat(format!(
"Chunk {} at offset 0x{:x} with size {} exceeds Data.db length {} \
— corrupt CompressionInfo.db chunk offset",
chunk_idx, chunk_offset, total_chunk_size, file_size
)));
}
}
let chunk_data_size = (total_chunk_size - 4) as usize;
tracing::debug!(
"read_nb_format_chunk_data: Chunk {} total_size={}, data_size={}, offset=0x{:x}",
chunk_idx,
total_chunk_size,
chunk_data_size,
chunk_offset
);
let (chunk_data, expected_crc) = {
let mut file_guard = file.lock().await;
let absolute_offset = chunk_offset + header_offset;
crate::storage::sstable::read_work_counters::record_seek();
file_guard
.seek(std::io::SeekFrom::Start(absolute_offset))
.await
.map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!(
"Failed to seek to chunk {} at offset 0x{:x} (header_offset={}): {}",
chunk_idx, absolute_offset, header_offset, e
),
))
})?;
crate::storage::sstable::read_work_counters::record_read();
let mut chunk_data = std::mem::take(scratch);
chunk_data.clear();
let cap_before = chunk_data.capacity();
chunk_data.resize(total_chunk_size as usize, 0u8);
if chunk_data.capacity() > cap_before {
crate::storage::sstable::read_work_counters::record_chunk_path_alloc();
}
file_guard.read_exact(&mut chunk_data).await.map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!(
"Failed to read chunk {} data+CRC ({} bytes at offset 0x{:x}): {}",
chunk_idx, total_chunk_size, chunk_offset, e
),
))
})?;
let expected_crc = u32::from_be_bytes([
chunk_data[chunk_data_size],
chunk_data[chunk_data_size + 1],
chunk_data[chunk_data_size + 2],
chunk_data[chunk_data_size + 3],
]);
chunk_data.truncate(chunk_data_size);
(chunk_data, expected_crc)
};
let computed_crc = crc32fast::hash(&chunk_data);
if computed_crc != expected_crc {
return Err(Error::InvalidFormat(format!(
"CRC32 mismatch for chunk {} at offset 0x{:x}: expected=0x{:08x}, computed=0x{:08x}, chunk_size={}",
chunk_idx, chunk_offset, expected_crc, computed_crc, chunk_data_size
)));
}
tracing::debug!(
"read_nb_format_chunk_data: CRC32 validated for chunk {}: 0x{:08x}",
chunk_idx,
expected_crc
);
tracing::debug!(
"read_nb_format_chunk_data: Successfully read chunk {}: {} bytes (compressed)",
chunk_idx,
chunk_data.len()
);
current_chunk_index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(Some(chunk_data))
}
pub(crate) fn read_compressed_chunk_at(
source: &dyn super::read_at::ReadAt,
comp_info: &crate::storage::sstable::compression_info::CompressionInfo,
chunk_idx: usize,
file_size: u64,
header_offset: u64,
) -> Result<Option<Vec<u8>>> {
if chunk_idx >= comp_info.chunk_offsets.len() {
return Ok(None); }
let chunk_offset = comp_info
.compressed_chunk_offset(chunk_idx)
.ok_or_else(|| Error::InvalidFormat(format!("No offset for chunk {}", chunk_idx)))?;
let total_chunk_size = comp_info
.compressed_chunk_size(chunk_idx, file_size)
.ok_or_else(|| {
Error::InvalidFormat(format!(
"Cannot determine size for chunk {} (file_size={})",
chunk_idx, file_size
))
})?;
if total_chunk_size < 4 {
return Err(Error::InvalidFormat(format!(
"Chunk {} size too small: {} bytes (minimum 4 for CRC)",
chunk_idx, total_chunk_size
)));
}
let chunk_end = chunk_offset
.checked_add(header_offset)
.and_then(|abs| abs.checked_add(total_chunk_size));
match chunk_end {
Some(end) if end <= file_size => {}
_ => {
return Err(Error::InvalidFormat(format!(
"Chunk {} at offset 0x{:x} with size {} exceeds Data.db length {} \
— corrupt CompressionInfo.db chunk offset",
chunk_idx, chunk_offset, total_chunk_size, file_size
)));
}
}
let chunk_data_size = (total_chunk_size - 4) as usize;
let absolute_offset = chunk_offset + header_offset;
crate::storage::sstable::read_work_counters::record_seek();
crate::storage::sstable::read_work_counters::record_read();
let mut buf = vec![0u8; chunk_data_size + 4];
source
.read_exact_at(absolute_offset, &mut buf)
.map_err(|e| {
Error::Io(std::io::Error::other(format!(
"Failed to read chunk {} ({} bytes at offset 0x{:x}): {}",
chunk_idx,
chunk_data_size + 4,
absolute_offset,
e
)))
})?;
let expected_crc = u32::from_be_bytes([
buf[chunk_data_size],
buf[chunk_data_size + 1],
buf[chunk_data_size + 2],
buf[chunk_data_size + 3],
]);
buf.truncate(chunk_data_size);
let computed_crc = crc32fast::hash(&buf);
if computed_crc != expected_crc {
return Err(Error::InvalidFormat(format!(
"CRC32 mismatch for chunk {} at offset 0x{:x}: expected=0x{:08x}, \
computed=0x{:08x}, chunk_size={}",
chunk_idx, chunk_offset, expected_crc, computed_crc, chunk_data_size
)));
}
Ok(Some(buf))
}
async fn read_bti_format_block_header(
file: &Arc<Mutex<BlockSource>>,
) -> Result<Option<(u32, u32, u64)>> {
let mut header_buffer = [0u8; 12]; let current_pos = {
let mut file_guard = file.lock().await;
let pos = file_guard.stream_position().await.unwrap_or(0);
match file_guard.read_exact(&mut header_buffer).await {
Ok(_) => pos,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
Err(e) => {
return Err(io_error_with_context("Failed to read BTI block header", e));
}
}
};
if detect_ascii_header_corruption(&header_buffer) {
return Err(Error::corruption(format!(
"BTI block header appears to contain ASCII corruption at position {}: {:?}",
current_pos,
String::from_utf8_lossy(&header_buffer[0..4])
)));
}
let compressed_size = u32::from_be_bytes([
header_buffer[0],
header_buffer[1],
header_buffer[2],
header_buffer[3],
]);
let checksum = u32::from_be_bytes([
header_buffer[8],
header_buffer[9],
header_buffer[10],
header_buffer[11],
]);
Ok(Some((compressed_size, checksum, current_pos)))
}
async fn read_legacy_format_block_header(
file: &Arc<Mutex<BlockSource>>,
) -> Result<Option<(u32, u32, u64)>> {
let mut header_buffer = [0u8; 8]; let current_pos = {
let mut file_guard = file.lock().await;
let pos = file_guard.stream_position().await.unwrap_or(0);
match file_guard.read_exact(&mut header_buffer).await {
Ok(_) => pos,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
Err(e) => {
return Err(io_error_with_context(
"Failed to read legacy block header",
e,
));
}
}
};
let compressed_size = u32::from_be_bytes([
header_buffer[0],
header_buffer[1],
header_buffer[2],
header_buffer[3],
]);
let checksum = u32::from_be_bytes([
header_buffer[4],
header_buffer[5],
header_buffer[6],
header_buffer[7],
]);
Ok(Some((compressed_size, checksum, current_pos)))
}
async fn read_block_direct(file: &Arc<Mutex<BlockSource>>, size: usize) -> Result<Vec<u8>> {
let mut block_data = vec![0u8; size];
{
let mut file_guard = file.lock().await;
file_guard
.read_exact(&mut block_data)
.await
.map_err(|e| io_error_with_context(format!("Failed to read block data ({size})"), e))?;
}
Ok(block_data)
}
async fn read_into_vec_capped<R>(
reader: &mut R,
size: usize,
buffer_size: usize,
) -> std::io::Result<Vec<u8>>
where
R: AsyncReadExt + Unpin,
{
let mut out = Vec::with_capacity(size);
if size == 0 {
return Ok(out);
}
let cap = buffer_size.clamp(1, size);
let mut scratch = vec![0u8; cap];
let mut remaining = size;
while remaining > 0 {
let to_read = remaining.min(cap);
reader.read_exact(&mut scratch[..to_read]).await?;
out.extend_from_slice(&scratch[..to_read]);
remaining -= to_read;
if remaining > 0 && out.len() % (1024 * 1024) == 0 {
tokio::task::yield_now().await;
}
}
Ok(out)
}
async fn read_large_block_streaming(
file: &Arc<Mutex<BlockSource>>,
size: usize,
config: &SSTableReaderConfig,
) -> Result<Vec<u8>> {
let buffer_size = config.read_buffer_size.min(size.max(1));
tracing::info!(
"Reading large block ({} bytes) using streaming with {} byte buffer",
size,
buffer_size
);
let mut file_guard = file.lock().await;
read_into_vec_capped(&mut *file_guard, size, config.read_buffer_size)
.await
.map_err(|e| io_error_with_context("Failed to read block chunk", e))
}
async fn read_uncompressed_data_block(
file: &Arc<Mutex<BlockSource>>,
config: &SSTableReaderConfig,
piecewise: bool,
crc_reader: Option<&CrcDb>,
) -> Result<Option<Vec<u8>>> {
let (current_pos, file_size) = {
let mut file_guard = file.lock().await;
let current = file_guard
.stream_position()
.await
.map_err(|e| io_error_with_context("Failed to get stream position", e))?;
let size = file_guard
.len()
.await
.map_err(|e| io_error_with_context("Failed to get file size", e))?;
(current, size)
};
let remaining = file_size.saturating_sub(current_pos) as usize;
if remaining == 0 {
tracing::debug!(
"read_uncompressed_data_block: EOF reached at position {}",
current_pos
);
return Ok(None);
}
let to_read = if piecewise {
match crc_reader {
Some(crc) => {
let cs = crc.chunk_size() as u64; let want = (UNCOMPRESSED_READ_PIECE_BYTES as u64).max(cs);
let target_end = current_pos.saturating_add(want);
let aligned_end = target_end.div_ceil(cs).saturating_mul(cs).min(file_size);
(aligned_end - current_pos) as usize
}
None => remaining.min(UNCOMPRESSED_READ_PIECE_BYTES),
}
} else {
remaining
};
tracing::debug!(
"read_uncompressed_data_block: Reading {} of {} remaining bytes from position {}",
to_read,
remaining,
current_pos
);
let data = {
let mut file_guard = file.lock().await;
read_into_vec_capped(&mut *file_guard, to_read, config.read_buffer_size)
.await
.map_err(|e| {
io_error_with_context(
format!("Failed to read uncompressed data block ({to_read} bytes)"),
e,
)
})?
};
tracing::debug!(
"read_uncompressed_data_block: Successfully read {} bytes",
data.len()
);
if let Some(crc) = crc_reader {
verify_uncompressed_chunks(file, crc, &data, current_pos, file_size).await?;
}
Ok(Some(data))
}
async fn verify_uncompressed_chunks(
file: &Arc<Mutex<BlockSource>>,
crc: &CrcDb,
data: &[u8],
start_offset: u64,
file_size: u64,
) -> Result<()> {
let cs = crc.chunk_size() as u64;
if cs == 0 {
return Err(Error::corruption(
"CRC.db chunk size is zero; cannot verify uncompressed chunks",
));
}
if data.is_empty() {
return Ok(());
}
let end = start_offset.saturating_add(data.len() as u64);
let first = start_offset / cs;
let last = (end - 1) / cs;
let mut did_seek = false;
for chunk in first..=last {
let lo = chunk.saturating_mul(cs);
let hi = ((chunk + 1).saturating_mul(cs)).min(file_size);
if hi <= lo {
break; }
let mut whole = Vec::with_capacity((hi - lo) as usize);
let pre_hi = start_offset.min(hi);
if lo < pre_hi {
read_range_into(file, lo, pre_hi, &mut whole).await?;
did_seek = true;
}
let mid_lo = lo.max(start_offset);
let mid_hi = hi.min(end);
if mid_lo < mid_hi {
whole.extend_from_slice(
&data[(mid_lo - start_offset) as usize..(mid_hi - start_offset) as usize],
);
}
let suf_lo = end.max(lo);
if suf_lo < hi {
read_range_into(file, suf_lo, hi, &mut whole).await?;
did_seek = true;
}
debug_assert_eq!(whole.len() as u64, hi - lo);
let computed = crc32fast::hash(&whole);
let expected = crc.crc_for_chunk(chunk as usize)?;
if computed != expected {
return Err(Error::corruption(format!(
"uncompressed CRC32 mismatch for chunk {} at Data.db offset 0x{:x} \
({} bytes): expected=0x{:08x}, computed=0x{:08x} (CRC.db)",
chunk,
lo,
hi - lo,
expected,
computed
)));
}
}
if did_seek {
let mut guard = file.lock().await;
guard
.seek(std::io::SeekFrom::Start(end))
.await
.map_err(|e| {
io_error_with_context(
"failed to restore Data.db position after CRC verification",
e,
)
})?;
}
Ok(())
}
async fn read_range_into(
file: &Arc<Mutex<BlockSource>>,
lo: u64,
hi: u64,
out: &mut Vec<u8>,
) -> Result<()> {
let mut buf = vec![0u8; (hi - lo) as usize];
let mut guard = file.lock().await;
guard
.seek(std::io::SeekFrom::Start(lo))
.await
.map_err(|e| {
io_error_with_context(
format!("failed to seek Data.db to 0x{lo:x} for CRC chunk completion"),
e,
)
})?;
guard.read_exact(&mut buf).await.map_err(|e| {
Error::corruption(format!(
"failed to read Data.db bytes [0x{lo:x}, 0x{hi:x}) for CRC verification: {e}"
))
})?;
out.extend_from_slice(&buf);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::AtomicUsize;
use tempfile::TempDir;
struct MemReadAt(Vec<u8>);
impl crate::storage::sstable::reader::read_at::ReadAt for MemReadAt {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let start = offset as usize;
if start >= self.0.len() {
return Ok(0);
}
let avail = &self.0[start..];
let n = avail.len().min(buf.len());
buf[..n].copy_from_slice(&avail[..n]);
Ok(n)
}
fn len(&self) -> u64 {
self.0.len() as u64
}
}
#[test]
#[serial_test::serial]
fn read_compressed_chunk_at_verifies_crc_before_returning() {
use crate::storage::sstable::compression_info::CompressionInfo;
let payload0 = b"first-chunk-bytes".to_vec();
let payload1 = b"second-chunk-bytes".to_vec();
let crc0 = crc32fast::hash(&payload0);
let good_crc1 = crc32fast::hash(&payload1);
let wrong_crc1 = good_crc1 ^ 0xffff_ffff;
let mut file = Vec::new();
file.extend_from_slice(&payload0);
file.extend_from_slice(&crc0.to_be_bytes());
let off1 = file.len() as u64;
file.extend_from_slice(&payload1);
file.extend_from_slice(&wrong_crc1.to_be_bytes());
let file_size = file.len() as u64;
let ci = CompressionInfo {
algorithm: "LZ4Compressor".to_string(),
option_pairs: vec![],
chunk_length: 64 * 1024,
max_compressed_length: i32::MAX as u32,
data_length: (payload0.len() + payload1.len()) as u64,
chunk_offsets: vec![0, off1],
};
let src = MemReadAt(file);
let got = read_compressed_chunk_at(&src, &ci, 0, file_size, 0)
.expect("chunk 0 read")
.expect("chunk 0 present");
assert_eq!(got, payload0, "verified chunk returns its exact payload");
let err = read_compressed_chunk_at(&src, &ci, 1, file_size, 0)
.expect_err("corrupt CRC must error before decompress");
match err {
Error::InvalidFormat(m) => {
assert!(m.contains("CRC32 mismatch"), "unexpected error text: {m}")
}
other => panic!("expected InvalidFormat CRC mismatch, got {other:?}"),
}
assert!(read_compressed_chunk_at(&src, &ci, 2, file_size, 0)
.expect("EOF read ok")
.is_none());
}
#[test]
#[serial_test::serial]
fn read_compressed_chunk_at_records_one_read_per_chunk() {
use crate::storage::sstable::compression_info::CompressionInfo;
use crate::storage::sstable::read_work_counters as rwc;
let payload0 = b"first-chunk-bytes".to_vec();
let payload1 = b"second-chunk-bytes".to_vec();
let crc0 = crc32fast::hash(&payload0);
let crc1 = crc32fast::hash(&payload1);
let mut file = Vec::new();
file.extend_from_slice(&payload0);
file.extend_from_slice(&crc0.to_be_bytes());
let off1 = file.len() as u64;
file.extend_from_slice(&payload1);
file.extend_from_slice(&crc1.to_be_bytes());
let file_size = file.len() as u64;
let ci = CompressionInfo {
algorithm: "LZ4Compressor".to_string(),
option_pairs: vec![],
chunk_length: 64 * 1024,
max_compressed_length: i32::MAX as u32,
data_length: (payload0.len() + payload1.len()) as u64,
chunk_offsets: vec![0, off1],
};
let src = MemReadAt(file);
rwc::reset();
assert_eq!(rwc::read_calls(), 0, "reset must zero READ_CALLS");
read_compressed_chunk_at(&src, &ci, 0, file_size, 0)
.expect("chunk 0 read")
.expect("chunk 0 present");
assert_eq!(
rwc::read_calls(),
1,
"point-read of chunk 0 must record exactly one READ_CALL"
);
read_compressed_chunk_at(&src, &ci, 1, file_size, 0)
.expect("chunk 1 read")
.expect("chunk 1 present");
assert_eq!(
rwc::read_calls(),
2,
"point-read of chunk 1 must record exactly one more READ_CALL (total 2)"
);
assert!(read_compressed_chunk_at(&src, &ci, 2, file_size, 0)
.expect("EOF read ok")
.is_none());
assert_eq!(
rwc::read_calls(),
2,
"EOF (no chunk fetched) must NOT record a READ_CALL"
);
}
#[test]
fn test_is_ascii_corruption_value_known_patterns() {
assert!(is_ascii_corruption_value(2959239534)); assert!(is_ascii_corruption_value(1684108385)); }
#[test]
fn test_is_ascii_corruption_value_normal_values() {
assert!(!is_ascii_corruption_value(4096));
assert!(!is_ascii_corruption_value(65536));
assert!(!is_ascii_corruption_value(1048576));
}
#[test]
fn test_detect_ascii_header_corruption_ascii_text() {
let header = b"DATA1234";
assert!(detect_ascii_header_corruption(header));
let header2 = b"bindata!";
assert!(detect_ascii_header_corruption(header2));
}
#[test]
fn test_detect_ascii_header_corruption_binary() {
let header = [0x00, 0x00, 0x10, 0x00, 0x12, 0x34, 0x56, 0x78]; assert!(!detect_ascii_header_corruption(&header));
}
#[test]
fn test_block_size_limit() {
let limit = 64 * 1024 * 1024;
assert!(4096 <= limit);
assert!(64 * 1024 * 1024 <= limit);
assert!(65 * 1024 * 1024 > limit);
}
#[test]
fn test_empty_block_handling() {
let size = 0u32;
assert_eq!(size, 0);
}
#[test]
fn test_crc32_calculation() {
let data = b"test data for CRC";
let crc = crc32fast::hash(data);
assert_eq!(crc, crc32fast::hash(data));
let data2 = b"different test data";
assert_ne!(crc, crc32fast::hash(data2));
}
#[test]
fn test_crc32_empty_data() {
let data: &[u8] = b"";
let crc = crc32fast::hash(data);
assert_eq!(crc, 0); }
#[test]
fn test_block_header_parsing_big_endian() {
let header_buffer = [0x00, 0x00, 0x10, 0x00, 0x12, 0x34, 0x56, 0x78];
let compressed_size = u32::from_be_bytes([
header_buffer[0],
header_buffer[1],
header_buffer[2],
header_buffer[3],
]);
let checksum = u32::from_be_bytes([
header_buffer[4],
header_buffer[5],
header_buffer[6],
header_buffer[7],
]);
assert_eq!(compressed_size, 4096); assert_eq!(checksum, 0x12345678);
}
#[test]
fn test_bti_header_parsing() {
let header_buffer = [
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0xAB, 0xCD, 0xEF, 0x12, ];
let compressed_size = u32::from_be_bytes([
header_buffer[0],
header_buffer[1],
header_buffer[2],
header_buffer[3],
]);
let checksum = u32::from_be_bytes([
header_buffer[8],
header_buffer[9],
header_buffer[10],
header_buffer[11],
]);
assert_eq!(compressed_size, 2048);
assert_eq!(checksum, 0xABCDEF12);
}
#[test]
fn test_atomic_chunk_index_increment() {
let index = AtomicUsize::new(0);
assert_eq!(index.load(std::sync::atomic::Ordering::Relaxed), 0);
index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
assert_eq!(index.load(std::sync::atomic::Ordering::Relaxed), 1);
index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
assert_eq!(index.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_read_block_direct_empty() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_empty_block.bin");
tokio::fs::write(&temp_file, b"").await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let result = read_block_direct(&file, 0).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[tokio::test]
async fn test_read_block_direct_small() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_small_block.bin");
let test_data = b"Hello, World! This is test data.";
tokio::fs::write(&temp_file, test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let result = read_block_direct(&file, test_data.len()).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), test_data);
}
#[tokio::test]
async fn test_read_uncompressed_data_block() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_uncompressed_block.bin");
let test_data = b"Uncompressed test data block content";
tokio::fs::write(&temp_file, test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig::default();
let result = read_uncompressed_data_block(&file, &config, false, None).await;
assert!(result.is_ok());
let data = result.unwrap();
assert!(data.is_some());
assert_eq!(data.unwrap(), test_data);
}
#[tokio::test]
async fn test_read_uncompressed_data_block_eof() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_uncompressed_eof.bin");
tokio::fs::write(&temp_file, b"").await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig::default();
let result = read_uncompressed_data_block(&file, &config, false, None).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
fn synth_crc_db(chunk_size: u32, crcs: &[u32]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&(chunk_size as i32).to_be_bytes());
for c in crcs {
v.extend_from_slice(&c.to_be_bytes());
}
v
}
async fn blocksource_from(bytes: &[u8]) -> (TempDir, Arc<Mutex<BlockSource>>) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("data.bin");
tokio::fs::write(&path, bytes).await.expect("write data");
let file = tokio::fs::File::open(&path).await.expect("open data");
(dir, Arc::new(Mutex::new(BlockSource::buffered(file))))
}
#[tokio::test]
async fn verify_uncompressed_chunks_clean_multichunk_passes() {
let cs = 4096u32;
let csz = cs as usize;
let size = csz * 2 + csz / 2; let data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let crcs = [
crc32fast::hash(&data[0..csz]),
crc32fast::hash(&data[csz..2 * csz]),
crc32fast::hash(&data[2 * csz..size]),
];
let crc = CrcDb::parse(&synth_crc_db(cs, &crcs)).expect("parse");
let (_dir, file) = blocksource_from(&data).await;
verify_uncompressed_chunks(&file, &crc, &data, 0, data.len() as u64)
.await
.expect("clean data verifies");
}
#[tokio::test]
async fn verify_uncompressed_chunks_flip_in_later_chunk_attributed_to_that_chunk() {
let cs = 4096u32;
let csz = cs as usize;
let size = csz * 2 + csz / 2;
let mut data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let crcs = [
crc32fast::hash(&data[0..csz]),
crc32fast::hash(&data[csz..2 * csz]),
crc32fast::hash(&data[2 * csz..size]),
];
let crc = CrcDb::parse(&synth_crc_db(cs, &crcs)).expect("parse");
data[csz + 100] ^= 0xFF;
let (_dir, file) = blocksource_from(&data).await;
let err = verify_uncompressed_chunks(&file, &crc, &data, 0, data.len() as u64)
.await
.expect_err("corrupt chunk must error");
let msg = err.to_string();
assert!(
matches!(err, Error::Corruption(_)),
"typed corruption: {msg}"
);
assert!(msg.contains("chunk 1"), "must name chunk 1: {msg}");
assert!(
msg.contains("0x1000"),
"must name the Data.db offset 0x1000: {msg}"
);
}
#[tokio::test]
async fn verify_uncompressed_chunks_truncated_crc_db_is_typed_error() {
let cs = 4096u32;
let csz = cs as usize;
let size = csz * 2 + csz / 2;
let data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let crc =
CrcDb::parse(&synth_crc_db(cs, &[crc32fast::hash(&data[0..csz])])).expect("parse");
let (_dir, file) = blocksource_from(&data).await;
let err = verify_uncompressed_chunks(&file, &crc, &data, 0, data.len() as u64)
.await
.expect_err("truncated CRC.db must error");
assert!(matches!(err, Error::Corruption(_)), "typed: {err}");
}
#[tokio::test]
async fn header_offset_read_still_verifies_chunk_0_prefix() {
let cs = 4096u32;
let csz = cs as usize;
let size = csz * 2 + csz / 2;
let clean: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let crcs = [
crc32fast::hash(&clean[0..csz]),
crc32fast::hash(&clean[csz..2 * csz]),
crc32fast::hash(&clean[2 * csz..size]),
];
let crc = CrcDb::parse(&synth_crc_db(cs, &crcs)).expect("parse");
let config = SSTableReaderConfig::default();
let header_size = 3u64;
let (dir, file) = blocksource_from(&clean).await;
{
let mut g = file.lock().await;
g.seek(std::io::SeekFrom::Start(header_size)).await.unwrap();
}
let piece = read_uncompressed_data_block(&file, &config, false, Some(&crc))
.await
.expect("clean header-offset read verifies")
.expect("non-empty section");
assert_eq!(piece, clean[3..], "returned bytes are the post-header data");
drop(dir);
let mut corrupt = clean.clone();
corrupt[1] ^= 0xFF; let (dir, file) = blocksource_from(&corrupt).await;
{
let mut g = file.lock().await;
g.seek(std::io::SeekFrom::Start(header_size)).await.unwrap();
}
let err = read_uncompressed_data_block(&file, &config, false, Some(&crc))
.await
.expect_err("corruption in chunk 0's header prefix must be caught, not returned");
let msg = err.to_string();
assert!(matches!(err, Error::Corruption(_)), "typed: {msg}");
assert!(
msg.contains("chunk 0"),
"must name chunk 0 (proving it is no longer skipped): {msg}"
);
drop(dir);
}
#[tokio::test]
async fn test_read_legacy_format_block_header_eof() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_legacy_header_eof.bin");
tokio::fs::write(&temp_file, &[0x00, 0x00, 0x10, 0x00])
.await
.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let result = read_legacy_format_block_header(&file).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_read_legacy_format_block_header_valid() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_legacy_header_valid.bin");
let header = [0x00, 0x00, 0x10, 0x00, 0x12, 0x34, 0x56, 0x78];
tokio::fs::write(&temp_file, &header).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let result = read_legacy_format_block_header(&file).await;
assert!(result.is_ok());
let (size, checksum, pos) = result.unwrap().unwrap();
assert_eq!(size, 4096);
assert_eq!(checksum, 0x12345678);
assert_eq!(pos, 0);
}
#[tokio::test]
async fn test_read_bti_format_block_header_valid() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_bti_header_valid.bin");
let header = [
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0xAB, 0xCD, 0xEF, 0x12, ];
tokio::fs::write(&temp_file, &header).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let result = read_bti_format_block_header(&file).await;
assert!(result.is_ok());
let (size, checksum, pos) = result.unwrap().unwrap();
assert_eq!(size, 2048);
assert_eq!(checksum, 0xABCDEF12);
assert_eq!(pos, 0);
}
#[tokio::test]
async fn test_read_large_block_streaming() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("test_large_block.bin");
let size = 128 * 1024;
let test_data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
tokio::fs::write(&temp_file, &test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig {
read_buffer_size: 4096, validate_checksums: true,
..Default::default()
};
let result = read_large_block_streaming(&file, size, &config).await;
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.len(), size);
assert_eq!(data, test_data);
}
#[tokio::test]
async fn read_into_vec_capped_bounds_scratch_buffer() {
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::task::{Context, Poll};
use tokio::io::ReadBuf;
struct MaxReadRecorder {
data: std::io::Cursor<Vec<u8>>,
max_request: Arc<AtomicUsize>,
}
impl tokio::io::AsyncRead for MaxReadRecorder {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
self.max_request
.fetch_max(buf.remaining(), Ordering::Relaxed);
let pos = self.data.position() as usize;
let inner = self.data.get_ref();
let avail = &inner[pos.min(inner.len())..];
let n = avail.len().min(buf.remaining());
buf.put_slice(&avail[..n]);
self.data.set_position((pos + n) as u64);
Poll::Ready(Ok(()))
}
}
let size = 4 * 1024 * 1024; let buffer_size = 64 * 1024; let data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let max_request = Arc::new(AtomicUsize::new(0));
let mut reader = MaxReadRecorder {
data: std::io::Cursor::new(data.clone()),
max_request: Arc::clone(&max_request),
};
let out = read_into_vec_capped(&mut reader, size, buffer_size)
.await
.expect("capped read should succeed");
assert_eq!(out.len(), size);
assert_eq!(out, data);
let observed = max_request.load(Ordering::Relaxed);
assert!(
observed <= buffer_size,
"scratch read request {} exceeded cap {} — allocation is scaling with block size",
observed,
buffer_size
);
}
#[tokio::test]
async fn uncompressed_data_block_streams_large_block_byte_identical() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("issue_592_uncompressed_large.bin");
let size = UNCOMPRESSED_READ_PIECE_BYTES * 3 + UNCOMPRESSED_READ_PIECE_BYTES / 2;
let test_data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
tokio::fs::write(&temp_file, &test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig {
read_buffer_size: 8 * 1024, ..Default::default()
};
let mut assembled = Vec::new();
let mut pieces = 0;
while let Some(piece) = read_uncompressed_data_block(&file, &config, true, None)
.await
.expect("read should succeed")
{
assert!(
piece.len() <= UNCOMPRESSED_READ_PIECE_BYTES,
"piece {} bytes exceeds the {} byte cap — read is not bounded",
piece.len(),
UNCOMPRESSED_READ_PIECE_BYTES
);
assembled.extend_from_slice(&piece);
pieces += 1;
}
assert_eq!(assembled.len(), size);
assert_eq!(assembled, test_data);
assert!(
pieces >= 4,
"expected the section to be split into multiple bounded pieces, got {pieces}"
);
}
#[tokio::test]
async fn piecewise_uncompressed_read_verifies_chunks_larger_than_piece_size() {
let cs: usize = 128 * 1024; assert!(cs > UNCOMPRESSED_READ_PIECE_BYTES);
let size = cs * 2 + cs / 2;
let clean: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let crcs = [
crc32fast::hash(&clean[0..cs]),
crc32fast::hash(&clean[cs..2 * cs]),
crc32fast::hash(&clean[2 * cs..size]),
];
let crc = CrcDb::parse(&synth_crc_db(cs as u32, &crcs)).expect("parse synthetic CRC.db");
let config = SSTableReaderConfig::default();
let temp_dir = TempDir::new().expect("create temp dir");
let clean_path = temp_dir.path().join("issue_1396_clean.bin");
tokio::fs::write(&clean_path, &clean).await.unwrap();
let file = tokio::fs::File::open(&clean_path).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let mut assembled = Vec::new();
while let Some(piece) = read_uncompressed_data_block(&file, &config, true, Some(&crc))
.await
.expect("clean piecewise read verifies")
{
assembled.extend_from_slice(&piece);
}
assert_eq!(
assembled, clean,
"clean data must reassemble byte-identical"
);
let mut corrupt = clean.clone();
corrupt[2 * cs + 5] ^= 0xFF;
let corrupt_path = temp_dir.path().join("issue_1396_corrupt.bin");
tokio::fs::write(&corrupt_path, &corrupt).await.unwrap();
let file = tokio::fs::File::open(&corrupt_path).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let mut caught: Option<Error> = None;
loop {
match read_uncompressed_data_block(&file, &config, true, Some(&crc)).await {
Ok(Some(_)) => continue,
Ok(None) => break,
Err(e) => {
caught = Some(e);
break;
}
}
}
let err = caught.expect("flipped byte in a >64 KiB chunk must be caught, not returned");
assert!(
matches!(err, Error::Corruption(_)),
"typed corruption: {err}"
);
assert!(
err.to_string().contains("chunk 2"),
"must name the corrupt chunk 2: {err}"
);
}
#[tokio::test]
async fn uncompressed_data_block_contiguous_returns_whole_section_in_one_call() {
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir
.path()
.join("issue_827_uncompressed_contiguous.bin");
let size = UNCOMPRESSED_READ_PIECE_BYTES * 3 + 7;
let test_data: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
tokio::fs::write(&temp_file, &test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig {
read_buffer_size: 8 * 1024, ..Default::default()
};
let first = read_uncompressed_data_block(&file, &config, false, None)
.await
.expect("read should succeed")
.expect("a non-empty section");
assert_eq!(
first.len(),
size,
"Finding 2: contiguous read must return the whole {size}-byte section \
in one call, got {} bytes (it was split into pieces)",
first.len()
);
assert_eq!(first, test_data, "contiguous read must be byte-identical");
let next = read_uncompressed_data_block(&file, &config, false, None)
.await
.expect("read should succeed");
assert!(
next.is_none(),
"Finding 2: after a contiguous full-section read the next call must be EOF"
);
}
#[tokio::test]
async fn read_next_block_v5_0_uncompressed_returns_contiguous_section() {
use crate::parser::header::CassandraVersion;
let temp_dir = TempDir::new().expect("create temp dir");
let temp_file = temp_dir.path().join("issue_827_v5_uncompressed_block.bin");
let size = UNCOMPRESSED_READ_PIECE_BYTES * 2 + 123;
let test_data: Vec<u8> = (0..size).map(|i| (i % 199) as u8).collect();
tokio::fs::write(&temp_file, &test_data).await.unwrap();
let file = tokio::fs::File::open(&temp_file).await.unwrap();
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
let config = SSTableReaderConfig {
read_buffer_size: 8 * 1024,
..Default::default()
};
let chunk_index = AtomicUsize::new(0);
let block = read_next_block(
&file,
&CassandraVersion::V5_0Uncompressed,
&config,
&None, None, &chunk_index,
0,
&mut Vec::new(),
)
.await
.expect("read_next_block should succeed")
.expect("a non-empty block");
assert_eq!(
block.len(),
size,
"Finding 2: a normal V5_0Uncompressed read must return the whole \
{size}-byte section as one block, got {} (truncated to a piece)",
block.len()
);
assert_eq!(
block, test_data,
"block must be byte-identical to the section"
);
let next = read_next_block(
&file,
&CassandraVersion::V5_0Uncompressed,
&config,
&None,
None,
&chunk_index,
0,
&mut Vec::new(),
)
.await
.expect("read_next_block should succeed");
assert!(
next.is_none(),
"Finding 2: second V5_0Uncompressed read is EOF"
);
}
#[tokio::test]
async fn test_read_with_real_sstable_data() {
let datasets_root = match std::env::var("CQLITE_DATASETS_ROOT") {
Ok(root) => PathBuf::from(root),
Err(_) => {
eprintln!("CQLITE_DATASETS_ROOT not set, skipping real data test");
return;
}
};
let simple_table_dir = datasets_root.join("sstables/test_basic");
if !simple_table_dir.exists() {
eprintln!("test_basic not found, skipping real data test");
return;
}
let table_dir = std::fs::read_dir(&simple_table_dir)
.ok()
.and_then(|entries| {
entries
.filter_map(|e| e.ok())
.find(|e| {
e.file_name()
.to_str()
.map(|n| n.starts_with("simple_table"))
.unwrap_or(false)
})
.map(|e| e.path())
});
let Some(table_path) = table_dir else {
eprintln!("simple_table not found, skipping");
return;
};
let data_file = std::fs::read_dir(&table_path).ok().and_then(|entries| {
entries
.filter_map(|e| e.ok())
.find(|e| {
e.file_name()
.to_str()
.map(|n| n.ends_with("-Data.db"))
.unwrap_or(false)
})
.map(|e| e.path())
});
let Some(data_path) = data_file else {
eprintln!("Data.db not found, skipping");
return;
};
let file = tokio::fs::File::open(&data_path).await.unwrap();
let metadata = file.metadata().await.unwrap();
eprintln!(
"Opened real SSTable Data.db: {} ({} bytes)",
data_path.display(),
metadata.len()
);
let file = Arc::new(Mutex::new(BlockSource::buffered(file)));
if metadata.len() > 100 {
let result = read_block_direct(&file, 100).await;
assert!(result.is_ok(), "Should read first 100 bytes of real file");
let data = result.unwrap();
assert_eq!(data.len(), 100);
eprintln!("Successfully read first 100 bytes from real SSTable");
}
}
}
#[cfg(test)]
#[path = "block_io_retry_tests.rs"]
mod retry_tests;