use super::*;
use test_log::test;
#[test]
fn expected_parity_len_saturates_on_huge_parity_product() {
let data_length = 100 * 1024 * 1024; let params = EccParams::Shard {
data_shards: 1,
parity_shards: 255,
};
assert_eq!(expected_parity_len(data_length, params), u32::MAX);
}
#[test]
fn expected_parity_len_saturates_on_max_data_length_even_rounding() {
let params = EccParams::Shard {
data_shards: 1,
parity_shards: 2,
};
assert_eq!(expected_parity_len(u32::MAX, params), u32::MAX);
}
struct TempBlock {
file: std::fs::File,
handle: crate::table::BlockHandle,
#[cfg_attr(
not(feature = "page_ecc"),
expect(dead_code, reason = "drop guard; only read by page_ecc-gated tests")
)]
dir: tempfile::TempDir,
}
fn write_block_to_tempfile(
data: &[u8],
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<TempBlock> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("block");
let header = {
let mut file = std::fs::File::create(&path)?;
let header = Block::write_into(&mut file, data, identity, transform)?;
file.sync_all()?;
header
};
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(
BlockOffset(0),
header.on_disk_size_with(transform.ecc_params()),
);
Ok(TempBlock { file, handle, dir })
}
#[test]
fn block_from_file_roundtrip_uncompressed() -> crate::Result<()> {
let data = b"abcdefabcdefabcdef";
let tmp = write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[cfg(feature = "zstd")]
#[test]
fn read_data_frame_returns_decompressible_zstd_frame() -> crate::Result<()> {
let data: Vec<u8> = (0..40_000u32).map(|i| (i % 64) as u8).collect();
let transform =
crate::table::block::BlockTransform::from_parts(CompressionType::Zstd(3), None, None)?;
let tmp = write_block_to_tempfile(
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
let (header, frame, _corrected) = Block::read_data_frame(&tmp.file, tmp.handle, &transform)?;
assert!(
frame.len() < data.len(),
"frame must be compressed (got {} for {} bytes)",
frame.len(),
data.len(),
);
let decompressed = crate::compression::ZstdBackend::decompress(
&frame,
header.uncompressed_length as usize + 1,
)?;
assert_eq!(decompressed, data, "frame must decompress to the original");
Ok(())
}
#[cfg(feature = "zstd")]
#[test]
fn read_data_frame_rejects_oversized_handle() -> crate::Result<()> {
let data: Vec<u8> = (0..1_000u32).map(|i| (i % 64) as u8).collect();
let transform =
crate::table::block::BlockTransform::from_parts(CompressionType::Zstd(3), None, None)?;
let tmp = write_block_to_tempfile(
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
let oversized = BlockHandle::new(tmp.handle.offset(), u32::MAX);
let err = Block::read_data_frame(&tmp.file, oversized, &transform)
.expect_err("oversized handle must be rejected");
assert!(
matches!(err, crate::Error::DecompressedSizeTooLarge { .. }),
"expected DecompressedSizeTooLarge, got {err:?}",
);
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_from_file_roundtrip_lz4() -> crate::Result<()> {
let data = b"abcdefabcdefabcdef";
let tmp = write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(zstd_any)]
fn block_from_file_roundtrip_zstd() -> crate::Result<()> {
let data = b"abcdefabcdefabcdef";
let tmp = write_block_to_tempfile(
data,
BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
fn block_roundtrip_uncompressed() -> crate::Result<()> {
let mut writer = vec![];
Block::write_into(
&mut writer,
b"abcdefabcdefabcdef",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
{
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(b"abcdefabcdefabcdef", &*block.data);
}
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_roundtrip_lz4() -> crate::Result<()> {
let mut writer = vec![];
Block::write_into(
&mut writer,
b"abcdefabcdefabcdef",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
{
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(b"abcdefabcdefabcdef", &*block.data);
}
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_reject_absurd_uncompressed_length() {
use crate::coding::Encode;
let mut buf = vec![];
Block::write_into(
&mut buf,
b"hello",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
)
.unwrap();
let mut reader = &buf[..];
let mut header = Header::decode_from(&mut reader).unwrap();
let compressed_payload: Vec<u8> = reader.to_vec();
header.uncompressed_length = u32::MAX;
let mut tampered = header.encode_into_vec();
tampered.extend_from_slice(&compressed_payload);
let mut r = &tampered[..];
let result = Block::from_reader(
&mut r,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"expected DecompressedSizeTooLarge, got: {:?}",
result.err(),
);
}
#[test]
#[cfg(feature = "lz4")]
fn block_zero_uncompressed_length_with_data_fails_decompress() {
use crate::coding::Encode;
let mut buf = vec![];
Block::write_into(
&mut buf,
b"hello",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
)
.unwrap();
let mut reader = &buf[..];
let mut header = Header::decode_from(&mut reader).unwrap();
let compressed_payload: Vec<u8> = reader.to_vec();
header.uncompressed_length = 0;
let mut tampered = header.encode_into_vec();
tampered.extend_from_slice(&compressed_payload);
let mut r = &tampered[..];
let result = Block::from_reader(
&mut r,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::Decompress(_))),
"expected Decompress error, got: {:?}",
result.err(),
);
}
#[test]
#[cfg(feature = "lz4")]
fn lz4_corrupted_uncompressed_length_triggers_decompress_error() {
use crate::coding::Encode;
use std::io::Cursor;
let payload: &[u8] = b"hello world";
let compressed = lz4_flex::compress(payload);
let data_length = compressed.len() as u32;
let uncompressed_length_correct = payload.len() as u32;
let uncompressed_length_corrupted = uncompressed_length_correct + 1;
let checksum = Checksum::from_raw(crate::hash::hash128(&compressed));
let header = Header {
data_length,
uncompressed_length: uncompressed_length_corrupted,
checksum,
..Header::test_dummy(BlockType::Data)
};
let mut buf = header.encode_into_vec();
buf.extend_from_slice(&compressed);
let mut cursor = Cursor::new(buf);
let result = Block::from_reader(
&mut cursor,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
match result {
Err(crate::Error::Decompress(CompressionType::Lz4)) => { }
Ok(_) => panic!("expected Error::Decompress, but got Ok(Block)"),
Err(other) => panic!("expected Error::Decompress, got different error: {other:?}"),
}
}
#[test]
#[cfg(feature = "lz4")]
fn block_from_file_reject_absurd_uncompressed_length() {
use crate::coding::Encode;
use std::io::Write;
let mut buf = vec![];
Block::write_into(
&mut buf,
b"hello",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
)
.unwrap();
let mut reader = &buf[..];
let mut header = Header::decode_from(&mut reader).unwrap();
let compressed_payload: Vec<u8> = reader.to_vec();
header.uncompressed_length = u32::MAX;
let mut tampered = header.encode_into_vec();
tampered.extend_from_slice(&compressed_payload);
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&tampered).unwrap();
tmp.flush().unwrap();
let file = std::fs::File::open(tmp.path()).unwrap();
let handle = crate::table::BlockHandle::new(BlockOffset(0), tampered.len() as u32);
let result = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"expected DecompressedSizeTooLarge, got: {:?}",
result.err(),
);
}
#[test]
#[cfg(feature = "lz4")]
fn block_from_file_zero_uncompressed_length_with_data_fails_decompress() {
use crate::coding::Encode;
use std::io::Write;
let mut buf = vec![];
Block::write_into(
&mut buf,
b"hello",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
)
.unwrap();
let mut reader = &buf[..];
let mut header = Header::decode_from(&mut reader).unwrap();
let compressed_payload: Vec<u8> = reader.to_vec();
header.uncompressed_length = 0;
let mut tampered = header.encode_into_vec();
tampered.extend_from_slice(&compressed_payload);
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&tampered).unwrap();
tmp.flush().unwrap();
let file = std::fs::File::open(tmp.path()).unwrap();
let handle = crate::table::BlockHandle::new(BlockOffset(0), tampered.len() as u32);
let result = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::Decompress(_))),
"expected Decompress error, got: {:?}",
result.err(),
);
}
#[test]
fn block_from_reader_reject_absurd_data_length() {
use crate::coding::Encode;
let mut buf = vec![];
Block::write_into(
&mut buf,
b"hello",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
)
.unwrap();
let mut reader = &buf[..];
let mut header = Header::decode_from(&mut reader).unwrap();
let payload: Vec<u8> = reader.to_vec();
header.data_length = MAX_DECOMPRESSION_SIZE + 1;
let mut tampered = header.encode_into_vec();
tampered.extend_from_slice(&payload);
let mut r = &tampered[..];
let result = Block::from_reader(
&mut r,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"expected DecompressedSizeTooLarge, got: {:?}",
result.err(),
);
}
#[test]
fn block_from_file_reject_oversized_handle() {
use std::io::Write;
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"dummy").unwrap();
tmp.flush().unwrap();
let file = std::fs::File::open(tmp.path()).unwrap();
let handle = crate::table::BlockHandle::new(BlockOffset(0), u32::MAX);
let result = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(&result, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"expected DecompressedSizeTooLarge, got: {:?}",
result.err(),
);
}
#[test]
#[cfg(zstd_any)]
fn zstd_corrupted_uncompressed_length_triggers_decompress_error() {
use crate::coding::Encode;
use std::io::Cursor;
let payload: &[u8] = b"hello world";
let compressed =
crate::compression::ZstdBackend::compress(payload, 3).expect("zstd compress failed");
let data_length = compressed.len() as u32;
let uncompressed_length_corrupted = payload.len() as u32 + 1;
let checksum = Checksum::from_raw(crate::hash::hash128(&compressed));
let header = Header {
data_length,
uncompressed_length: uncompressed_length_corrupted,
checksum,
..Header::test_dummy(BlockType::Data)
};
let mut buf = header.encode_into_vec();
buf.extend_from_slice(&compressed);
let mut cursor = Cursor::new(buf);
let result = Block::from_reader(
&mut cursor,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
match result {
Err(crate::Error::Decompress(CompressionType::Zstd(_))) => { }
Ok(_) => panic!("expected Error::Decompress, but got Ok(Block)"),
Err(other) => panic!("expected Error::Decompress, got different error: {other:?}"),
}
}
#[test]
#[cfg(zstd_any)]
fn zstd_decreased_uncompressed_length_triggers_decompress_error() {
use crate::coding::Encode;
use std::io::Cursor;
let payload: &[u8] = b"hello world hello world hello world";
let compressed =
crate::compression::ZstdBackend::compress(payload, 3).expect("zstd compress failed");
let data_length = compressed.len() as u32;
let uncompressed_length_too_small = payload.len() as u32 - 1;
let checksum = Checksum::from_raw(crate::hash::hash128(&compressed));
let header = Header {
data_length,
uncompressed_length: uncompressed_length_too_small,
checksum,
..Header::test_dummy(BlockType::Data)
};
let mut buf = header.encode_into_vec();
buf.extend_from_slice(&compressed);
let mut cursor = Cursor::new(buf);
let result = Block::from_reader(
&mut cursor,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
match result {
Err(crate::Error::Decompress(CompressionType::Zstd(_))) => { }
Ok(_) => panic!("expected Error::Decompress, but got Ok(Block)"),
Err(other) => panic!("expected Error::Decompress, got different error: {other:?}"),
}
}
#[test]
#[cfg(zstd_any)]
fn block_roundtrip_zstd() -> crate::Result<()> {
let mut writer = vec![];
Block::write_into(
&mut writer,
b"abcdefabcdefabcdef",
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
{
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(b"abcdefabcdefabcdef", &*block.data);
}
Ok(())
}
#[test]
fn block_write_rejects_oversized_payload() {
let oversized = vec![0u8; MAX_DECOMPRESSION_SIZE as usize + 1];
let mut sink = std::io::sink();
let result = Block::write_into(
&mut sink,
&oversized,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
None,
#[cfg(zstd_any)]
None,
)
.unwrap(),
);
assert!(
matches!(result, Err(crate::Error::DecompressedSizeTooLarge { .. })),
"expected DecompressedSizeTooLarge, got: {result:?}",
);
}
#[test]
#[cfg(zstd_any)]
fn block_roundtrip_zstd_large_data() -> crate::Result<()> {
let data = vec![0xABu8; 64 * 1024]; let mut writer = vec![];
Block::write_into(
&mut writer,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert!(
writer.len() < data.len(),
"zstd should compress repeated data"
);
{
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
None,
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(&*block.data, &data[..]);
}
Ok(())
}
#[cfg(feature = "encryption")]
mod encrypted {
use crate::table::block::*;
fn test_provider() -> crate::encryption::Aes256GcmProvider {
crate::encryption::Aes256GcmProvider::new(&[0x42; 32])
}
#[test]
fn block_roundtrip_encrypted_uncompressed() -> crate::Result<()> {
let enc = test_provider();
let data = b"plaintext block data for encryption test";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_roundtrip_encrypted_lz4() -> crate::Result<()> {
let enc = test_provider();
let data = b"abcdefabcdefabcdef";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(zstd_any)]
fn block_roundtrip_encrypted_zstd() -> crate::Result<()> {
let enc = test_provider();
let data = b"abcdefabcdefabcdef";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
fn block_from_file_encrypted_uncompressed() -> crate::Result<()> {
let enc = test_provider();
let data = b"plaintext block data for from_file encryption test";
let tmp = super::write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_from_file_encrypted_lz4() -> crate::Result<()> {
let enc = test_provider();
let data = b"abcdefabcdefabcdef";
let tmp = super::write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(zstd_any)]
fn block_from_file_encrypted_zstd() -> crate::Result<()> {
let enc = test_provider();
let data = b"abcdefabcdefabcdef";
let tmp = super::write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
fn block_from_file_encrypted_wrong_key_fails() -> crate::Result<()> {
let enc_write = test_provider();
let enc_read = crate::encryption::Aes256GcmProvider::new(&[0x99; 32]);
let data = b"encrypted block data";
let tmp = super::write_block_to_tempfile(
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc_write),
#[cfg(zstd_any)]
None,
)?,
)?;
let result = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc_read),
#[cfg(zstd_any)]
None,
)?,
);
assert!(
matches!(result, Err(crate::Error::Decrypt(_))),
"expected Decrypt error for wrong key, got: {:?}",
result.err(),
);
Ok(())
}
#[test]
fn block_from_reader_encrypted_wrong_key_fails() -> crate::Result<()> {
let enc_write = test_provider();
let enc_read = crate::encryption::Aes256GcmProvider::new(&[0x99; 32]);
let data = b"encrypted block data";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc_write),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let result = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc_read),
#[cfg(zstd_any)]
None,
)?,
);
assert!(
matches!(result, Err(crate::Error::Decrypt(_))),
"expected Decrypt error for wrong key, got: {:?}",
result.err(),
);
Ok(())
}
#[test]
fn block_from_file_encrypted_checksum_tamper_detected() -> crate::Result<()> {
use std::io::Write;
let enc = test_provider();
let data = b"data for tamper test";
let mut buf = vec![];
let header = Block::write_into(
&mut buf,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mid = Header::MIN_LEN + 1;
if mid < buf.len() {
#[expect(clippy::indexing_slicing, reason = "mid < buf.len() checked above")]
{
buf[mid] ^= 0xFF;
}
}
let dir = tempfile::tempdir()?;
let path = dir.path().join("block");
let mut file = std::fs::File::create(&path)?;
file.write_all(&buf)?;
file.sync_all()?;
drop(file);
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(BlockOffset(0), header.on_disk_size());
let result = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
);
assert!(
matches!(result, Err(crate::Error::ChecksumMismatch { .. })),
"expected ChecksumMismatch for tampered data, got: {:?}",
result.err(),
);
Ok(())
}
#[test]
fn block_from_file_encrypted_undersized_handle_rejected() -> crate::Result<()> {
use std::io::Write;
let enc = test_provider();
let dir = tempfile::tempdir()?;
let path = dir.path().join("block");
let mut file = std::fs::File::create(&path)?;
file.write_all(b"tiny")?;
file.sync_all()?;
drop(file);
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(BlockOffset(0), 2);
let result = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
);
assert!(
matches!(result, Err(crate::Error::InvalidHeader(_))),
"expected InvalidHeader for undersized handle, got: {:?}",
result.err(),
);
Ok(())
}
#[test]
fn block_from_file_encrypted_uncompressed_large_payload() -> crate::Result<()> {
let enc = test_provider();
let data = vec![0xBB_u8; 32 * 1024]; let tmp = super::write_block_to_tempfile(
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let block = Block::from_file(
&tmp.file,
tmp.handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
#[test]
fn block_roundtrip_encrypted_uncompressed_large() -> crate::Result<()> {
let enc = test_provider();
let data = vec![0xCC_u8; 32 * 1024]; let mut writer = vec![];
Block::write_into(
&mut writer,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::None,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
#[test]
#[cfg(feature = "lz4")]
fn block_roundtrip_encrypted_lz4_large() -> crate::Result<()> {
let enc = test_provider();
let data = vec![0xDD_u8; 32 * 1024]; let mut writer = vec![];
Block::write_into(
&mut writer,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Lz4,
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
#[test]
#[cfg(zstd_any)]
fn block_roundtrip_encrypted_zstd_large() -> crate::Result<()> {
let enc = test_provider();
let data = vec![0xEE_u8; 32 * 1024]; let mut writer = vec![];
Block::write_into(
&mut writer,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
CompressionType::Zstd(3),
Some(&enc),
#[cfg(zstd_any)]
None,
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
}
#[cfg(feature = "zstd")]
mod zstd_dict {
use super::*;
use crate::compression::ZstdDictionary;
use test_log::test;
fn test_dict() -> ZstdDictionary {
let mut samples = Vec::new();
for i in 0u32..500 {
samples.extend_from_slice(format!("key-{i:05}val-{i:05}").as_bytes());
}
ZstdDictionary::new(&samples)
}
fn test_compression(dict: &ZstdDictionary) -> CompressionType {
CompressionType::ZstdDict {
level: 3,
dict_id: dict.id(),
}
}
#[test]
fn block_roundtrip_zstd_dict_reader() -> crate::Result<()> {
let dict = test_dict();
let compression = test_compression(&dict);
let data = b"abcdefabcdefabcdef";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
fn block_roundtrip_zstd_dict_file() -> crate::Result<()> {
use std::io::Write;
let dict = test_dict();
let compression = test_compression(&dict);
let data = b"abcdefabcdefabcdef";
let mut buf = vec![];
let header = Block::write_into(
&mut buf,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
let dir = tempfile::tempdir()?;
let path = dir.path().join("block");
let mut file = std::fs::File::create(&path)?;
file.write_all(&buf)?;
file.sync_all()?;
drop(file);
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(BlockOffset(0), header.on_disk_size());
let block = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
fn block_roundtrip_zstd_dict_large_data() -> crate::Result<()> {
let dict = test_dict();
let compression = test_compression(&dict);
let data = vec![0xAB_u8; 64 * 1024]; let mut writer = vec![];
Block::write_into(
&mut writer,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert!(
writer.len() < data.len(),
"dict compression should reduce size"
);
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
None,
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
#[test]
fn block_zstd_dict_wrong_dict_returns_error() {
let dict = test_dict();
let compression = test_compression(&dict);
let wrong_dict = ZstdDictionary::new(b"completely different dictionary bytes");
let result =
crate::table::block::BlockTransform::from_parts(compression, None, Some(&wrong_dict));
assert!(
matches!(
result,
Err(crate::Error::ZstdDictMismatch { got: Some(_), .. })
),
"expected ZstdDictMismatch with got=Some",
);
}
#[test]
fn block_transform_from_parts_zstd_dict_missing_returns_error() {
let dict = test_dict();
let compression = test_compression(&dict);
let result = crate::table::block::BlockTransform::from_parts(compression, None, None);
assert!(
matches!(
&result,
Err(crate::Error::ZstdDictMismatch { got: None, .. })
),
"expected ZstdDictMismatch, got: {:?}",
result.as_ref().err(),
);
}
#[test]
#[cfg(feature = "encryption")]
fn block_roundtrip_zstd_dict_encrypted_reader() -> crate::Result<()> {
let enc = crate::Aes256GcmProvider::new(&[0x42; 32]);
let dict = test_dict();
let compression = test_compression(&dict);
let data = b"encrypted-dict-compressed-data-for-test";
let mut writer = vec![];
Block::write_into(
&mut writer,
data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
Some(&enc),
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
Some(&enc),
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert_eq!(data, &*block.data);
Ok(())
}
#[test]
#[cfg(feature = "encryption")]
fn block_roundtrip_zstd_dict_encrypted_file() -> crate::Result<()> {
use std::io::Write;
let enc = crate::Aes256GcmProvider::new(&[0x42; 32]);
let dict = test_dict();
let compression = test_compression(&dict);
let data = vec![0xCC_u8; 16 * 1024]; let mut buf = vec![];
let header = Block::write_into(
&mut buf,
&data,
crate::table::block::BlockIdentity::for_test(0, BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
Some(&enc),
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
let dir = tempfile::tempdir()?;
let path = dir.path().join("block");
let mut file = std::fs::File::create(&path)?;
file.write_all(&buf)?;
file.sync_all()?;
drop(file);
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(BlockOffset(0), header.on_disk_size());
let block = Block::from_file(
&file,
handle,
crate::table::block::BlockIdentity::for_test(0, crate::table::block::BlockType::Data),
&crate::table::block::BlockTransform::from_parts(
compression,
Some(&enc),
#[cfg(zstd_any)]
Some(&dict),
)?,
)?;
assert_eq!(&*block.data, &data[..]);
Ok(())
}
}
#[cfg(feature = "page_ecc")]
mod page_ecc {
use super::*;
use test_log::test;
const PAYLOAD: &[u8] = b"the quick brown fox jumps over the lazy dog \
0123456789 the quick brown fox jumps over \
the lazy dog 0123456789";
#[test]
fn block_roundtrip_plain_ecc_clean_read() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert!(
header.block_flags & crate::table::block::header::block_flags::ECC_PARITY != 0,
"PlainEcc writer must set the ECC_PARITY flag",
);
assert_eq!(
writer.len(),
header.on_disk_size() as usize,
"on-disk size must equal header + payload + derived parity length",
);
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert_eq!(&*block.data, PAYLOAD);
Ok(())
}
#[test]
fn block_roundtrip_plain_ecc_recovers_from_single_byte_flip() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
let header_len = Header::MIN_LEN;
let flip_at = header_len + (header.data_length as usize) / 2;
writer[flip_at] ^= 0xFF;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert_eq!(
&*block.data, PAYLOAD,
"Reed-Solomon recovery must reconstruct the original \
payload from a single-byte data-shard flip",
);
Ok(())
}
#[test]
fn block_roundtrip_secded_clean_read() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
)?;
assert!(
header.block_flags & crate::table::block::header::block_flags::ECC_PARITY != 0,
"SECDED writer must set the ECC_PARITY flag",
);
assert_eq!(
writer.len(),
header.on_disk_size_with(Some(EccParams::SECDED)) as usize,
"on-disk size must equal header + payload + SECDED parity length",
);
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
)?;
assert_eq!(&*block.data, PAYLOAD);
Ok(())
}
#[test]
fn block_roundtrip_secded_recovers_from_single_bit_flip() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
)?;
let flip_at = Header::MIN_LEN + (header.data_length as usize) / 2;
writer[flip_at] ^= 0x01;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
)?;
assert_eq!(
&*block.data, PAYLOAD,
"SECDED must heal a single-bit payload flip",
);
Ok(())
}
#[test]
fn block_roundtrip_secded_unrecoverable_on_double_bit_flip() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
)?;
let flip_at = Header::MIN_LEN + (header.data_length as usize) / 2;
writer[flip_at] ^= 0x03;
let mut reader = &writer[..];
let result = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::SECDED),
);
assert!(
matches!(&result, Err(crate::Error::PageEccUnrecoverable { .. })),
"a double-bit error in one word must be detected as unrecoverable \
(got ok={})",
result.is_ok(),
);
Ok(())
}
#[test]
fn block_from_file_plain_ecc_recovers_from_single_byte_flip() -> crate::Result<()> {
let tmp = super::write_block_to_tempfile(
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
let path = tmp.dir.path().join("block");
let mut bytes = std::fs::read(&path)?;
let payload_start = Header::MIN_LEN;
bytes[payload_start + 3] ^= 0x80;
std::fs::write(&path, &bytes)?;
let file = std::fs::File::open(&path)?;
let block = Block::from_file(
&file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert_eq!(&*block.data, PAYLOAD);
Ok(())
}
#[test]
fn from_file_with_status_reports_corrected_after_ecc_repair() -> crate::Result<()> {
let transform = BlockTransform::PlainEcc(EccParams::RS_4_2);
let tmp = super::write_block_to_tempfile(
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
let path = tmp.dir.path().join("block");
{
let file = std::fs::File::open(&path)?;
let (block, status) = Block::from_file_with_status(
&file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
assert_eq!(&*block.data, PAYLOAD);
assert_eq!(status, EccStatus::Ok, "clean read must not flag a repair");
}
let mut bytes = std::fs::read(&path)?;
bytes[Header::MIN_LEN + 3] ^= 0x80;
std::fs::write(&path, &bytes)?;
let file = std::fs::File::open(&path)?;
let (block, status, recovery) = Block::from_file_with_recovery(
&file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
assert_eq!(&*block.data, PAYLOAD, "repaired bytes must equal original");
assert_eq!(
status,
EccStatus::Corrected,
"a repaired read reports Corrected"
);
assert_eq!(
recovery,
Some(EccRecoveryKind::Shard),
"an RS repair is attributed to the shard mechanism",
);
Ok(())
}
#[test]
fn from_file_with_status_reports_secded_kind_after_single_bit_heal() -> crate::Result<()> {
let transform = BlockTransform::PlainEcc(EccParams::SECDED);
let tmp = super::write_block_to_tempfile(
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
let path = tmp.dir.path().join("block");
let mut bytes = std::fs::read(&path)?;
bytes[Header::MIN_LEN + 3] ^= 0x01;
std::fs::write(&path, &bytes)?;
let file = std::fs::File::open(&path)?;
let (block, status, recovery) = Block::from_file_with_recovery(
&file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&transform,
)?;
assert_eq!(
&*block.data, PAYLOAD,
"SEC-DED must heal the single-bit flip"
);
assert_eq!(
status,
EccStatus::Corrected,
"a repaired read reports Corrected"
);
assert_eq!(
recovery,
Some(EccRecoveryKind::Secded),
"a SEC-DED single-bit heal is attributed to the SEC-DED mechanism",
);
Ok(())
}
#[test]
fn from_file_with_status_soft_warns_on_unrecognized_trailer() -> crate::Result<()> {
let scheme = EccParams::try_new(8, 2).expect("valid shards");
let tmp = super::write_block_to_tempfile(
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(scheme),
)?;
let (block, status) = Block::from_file_with_status(
&tmp.file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(scheme),
)?;
assert_eq!(&*block.data, PAYLOAD);
assert_eq!(status, EccStatus::Ok);
let (block, status) = Block::from_file_with_status(
&tmp.file,
tmp.handle,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PLAIN,
)?;
assert_eq!(
&*block.data, PAYLOAD,
"payload reads despite unknown trailer"
);
assert_eq!(status, EccStatus::Unrecognized);
Ok(())
}
#[test]
fn from_file_recognized_empty_block_rejects_extra_trailer() -> crate::Result<()> {
let scheme = EccParams::try_new(8, 2).expect("valid shards");
let tmp = super::write_block_to_tempfile(
b"",
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(scheme),
)?;
let path = tmp.dir.path().join("block");
let base = tmp.handle.size();
let mut bytes = std::fs::read(&path)?;
bytes.extend_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]);
std::fs::write(&path, &bytes)?;
let file = std::fs::File::open(&path)?;
let handle = crate::table::BlockHandle::new(crate::table::BlockOffset(0), base + 4);
let err = Block::from_file_with_status(
&file,
handle,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(scheme),
)
.err();
assert!(
matches!(err, Some(crate::Error::InvalidHeader("Block"))),
"recognized zero-parity layout + extra trailer must fail, got {err:?}",
);
Ok(())
}
#[cfg(feature = "lz4")]
#[test]
fn block_roundtrip_compressed_ecc_recovers_from_byte_flip() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::CompressedEcc(
CompressionContext::new(CompressionType::Lz4)?,
EccParams::RS_4_2,
),
)?;
assert!(header.block_flags & crate::table::block::header::block_flags::ECC_PARITY != 0);
let header_len = Header::MIN_LEN;
let flip_at = header_len + (header.data_length as usize) / 2;
writer[flip_at] ^= 0x55;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::CompressedEcc(
CompressionContext::new(CompressionType::Lz4)?,
EccParams::RS_4_2,
),
)?;
assert_eq!(
&*block.data, PAYLOAD,
"ECC must recover the compressed bytes BEFORE lz4 \
decompression, otherwise lz4 would fail on corrupt input",
);
Ok(())
}
#[cfg(feature = "encryption")]
#[test]
fn block_roundtrip_encrypted_ecc_recovers_from_byte_flip() -> crate::Result<()> {
let enc = crate::encryption::Aes256GcmProvider::new(&[0x42; 32]);
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::EncryptedEcc(&enc, EccParams::RS_4_2),
)?;
assert!(header.block_flags & crate::table::block::header::block_flags::ECC_PARITY != 0);
let header_len = Header::MIN_LEN;
let flip_at = header_len + (header.data_length as usize) / 2;
writer[flip_at] ^= 0x21;
let mut reader = &writer[..];
let block = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::EncryptedEcc(&enc, EccParams::RS_4_2),
)?;
assert_eq!(
&*block.data, PAYLOAD,
"ECC must reconstruct ciphertext byte-exactly so AEAD \
authentication succeeds on the recovered bytes",
);
Ok(())
}
#[test]
fn block_roundtrip_plain_ecc_unrecoverable_when_too_many_shards_corrupt() -> crate::Result<()> {
let mut writer = vec![];
let header = Block::write_into(
&mut writer,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
let payload_len = header.data_length as usize;
let shard_bytes = ((payload_len.div_ceil(4)) + 1) & !1;
let payload_start = Header::MIN_LEN;
for shard_idx in 0..3 {
let pos = payload_start + shard_idx * shard_bytes;
if pos < writer.len() {
writer[pos] ^= 0xFF;
}
}
let mut reader = &writer[..];
let result = Block::from_reader(
&mut reader,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
);
match result {
Ok(_) => panic!(
"3-shard corruption must exceed RS(4,2) recovery capacity, \
but from_reader returned Ok"
),
Err(crate::Error::PageEccUnrecoverable { .. }) => {}
Err(e) => panic!("expected PageEccUnrecoverable, got {e:?}"),
}
Ok(())
}
#[test]
fn ecc_parity_bit_agrees_with_emitted_parity_length() -> crate::Result<()> {
use crate::table::block::header::block_flags;
let mut empty_buf = vec![];
let empty = Block::write_into(
&mut empty_buf,
&[],
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert_eq!(
empty.block_flags & block_flags::ECC_PARITY,
0,
"ECC_PARITY must be clear when no parity trailer is emitted",
);
assert_eq!(
empty_buf.len(),
empty.on_disk_size() as usize,
"on-disk size matches the derived (zero) parity length",
);
assert_eq!(
empty.on_disk_size() as usize,
Header::MIN_LEN,
"empty payload emits no parity, so on-disk size is just the header",
);
let mut full_buf = vec![];
let full = Block::write_into(
&mut full_buf,
PAYLOAD,
BlockIdentity::for_test(0, BlockType::Data),
&BlockTransform::PlainEcc(EccParams::RS_4_2),
)?;
assert_ne!(
full.block_flags & block_flags::ECC_PARITY,
0,
"ECC_PARITY must be set when a parity trailer is emitted",
);
assert_eq!(
full_buf.len(),
full.on_disk_size() as usize,
"on-disk size matches header + payload + derived parity",
);
assert!(
full.on_disk_size() as usize > Header::MIN_LEN + full.data_length as usize,
"non-empty payload emits a parity trailer beyond header + payload",
);
Ok(())
}
}