pub(crate) mod binary_index;
pub(crate) mod decoder;
mod encoder;
pub mod hash_index;
pub(crate) mod header;
mod identity;
pub(crate) mod kv_checksum;
mod offset;
mod trailer;
mod transform;
mod r#type;
pub(crate) use decoder::{Decodable, Decoder, DecoderMeta, ParsedItem};
pub(crate) use encoder::{Encodable, Encoder};
pub use header::Header;
pub use identity::BlockIdentity;
pub use offset::BlockOffset;
pub(crate) use trailer::{TRAILER_START_MARKER, Trailer};
pub use transform::{BlockTransform, CompressionContext, EccParams};
pub use r#type::BlockType;
#[cfg(zstd_any)]
use crate::compression::CompressionProvider as _;
use crate::{
Checksum, CompressionType, Slice,
coding::{Decode, Encode},
fs::FsFile,
table::BlockHandle,
};
use alloc::borrow::Cow;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
const MAX_DECOMPRESSION_SIZE: u32 = 256 * 1024 * 1024;
#[inline]
pub(crate) fn expected_parity_len(data_length: u32, params: EccParams) -> u32 {
let (data_shards, parity_shards) = match params {
EccParams::Secded => return data_length.div_ceil(8),
EccParams::Shard {
data_shards,
parity_shards,
} => (u32::from(data_shards), u32::from(parity_shards)),
};
if data_length == 0 || data_shards == 0 || parity_shards == 0 {
return 0;
}
let ceil = (data_length / data_shards) + u32::from(!data_length.is_multiple_of(data_shards));
let shard_bytes = ceil.saturating_add(u32::from(!ceil.is_multiple_of(2)));
shard_bytes.saturating_mul(parity_shards)
}
fn block_has_parity(header: &Header, transform: &BlockTransform<'_>) -> bool {
if Header::has_block_flags(header.block_type) {
header.block_flags & header::block_flags::ECC_PARITY != 0
} else {
transform.page_ecc()
}
}
fn block_ecc_params(header: &Header, transform: &BlockTransform<'_>) -> EccParams {
if Header::has_block_flags(header.block_type) {
EccParams::RS_4_2
} else {
transform.ecc_params().unwrap_or(EccParams::RS_4_2)
}
}
#[cfg(zstd_any)]
fn compression_tag_byte(compression: CompressionType) -> u8 {
match compression {
CompressionType::None => 0,
#[cfg(feature = "lz4")]
CompressionType::Lz4 => 1,
CompressionType::Zstd(_) => 3,
CompressionType::ZstdDict { .. } => 4,
}
}
#[cfg_attr(
zstd_any,
expect(
clippy::needless_pass_by_value,
reason = "owned is consumed by encrypt_vec on the non-zstd path; the AAD \
path only borrows it, so by-value is needed for the other cfg"
)
)]
fn encrypt_block_payload(
enc: &dyn crate::encryption::EncryptionProvider,
owned: Option<Vec<u8>>,
borrow: &[u8],
identity: &BlockIdentity,
compression: CompressionType,
block_flags: u8,
) -> crate::Result<Vec<u8>> {
#[cfg(zstd_any)]
{
let plaintext = owned.as_deref().unwrap_or(borrow);
let aad_block_flags = block_flags & !crate::table::block::header::block_flags::ECC_PARITY;
enc.encrypt_block_aad(
plaintext,
identity,
compression_tag_byte(compression),
aad_block_flags,
)
}
#[cfg(not(zstd_any))]
{
let _ = (identity, compression, block_flags);
match owned {
Some(buf) => enc.encrypt_vec(buf),
None => enc.encrypt(borrow),
}
}
}
fn decrypt_block_payload(
enc: &dyn crate::encryption::EncryptionProvider,
raw: &[u8],
identity: &BlockIdentity,
) -> crate::Result<Vec<u8>> {
#[cfg(zstd_any)]
{
enc.decrypt_block_aad(raw, identity)
}
#[cfg(not(zstd_any))]
{
let _ = identity;
enc.decrypt_vec(raw.to_vec())
}
}
fn classify_block_trailer(
has_recognized_ecc: bool,
actual_payload_plus_ecc: usize,
data_length: usize,
ecc_length: u32,
handle: &BlockHandle,
) -> crate::Result<EccStatus> {
let trailer = actual_payload_plus_ecc
.checked_sub(data_length)
.ok_or(crate::Error::InvalidHeader("Block"))?;
if has_recognized_ecc {
if trailer != ecc_length as usize {
return Err(crate::Error::InvalidHeader("Block"));
}
Ok(EccStatus::Ok)
} else if trailer == 0 {
Ok(EccStatus::Ok)
} else {
log::warn!(
"block {handle:?} carries an unrecognized ECC trailer ({trailer} B); \
payload verified by checksum but recovery is unavailable — recompact \
to re-stamp with a supported scheme",
);
Ok(EccStatus::Unrecognized)
}
}
pub(crate) struct PreparedBlock<'a> {
header: Header,
payload: Cow<'a, [u8]>,
parity: Option<Vec<u8>>,
pub(crate) layout: Vec<u32>,
}
impl PreparedBlock<'_> {
#[cfg(feature = "std")] pub(crate) fn into_owned(self) -> PreparedBlock<'static> {
PreparedBlock {
header: self.header,
payload: Cow::Owned(self.payload.into_owned()),
parity: self.parity,
layout: self.layout,
}
}
pub(crate) fn write_to<W: crate::io::Write>(self, mut writer: &mut W) -> crate::Result<Header> {
self.header.encode_into(&mut writer)?;
writer.write_all(&self.payload)?;
if let Some(parity) = &self.parity {
writer.write_all(parity)?;
}
log::trace!(
"Writing block with size {}B (on-disk: {}B, ecc: {}B) (excluding header of {}B)",
self.header.uncompressed_length,
self.header.data_length,
self.parity.as_ref().map_or(0, Vec::len),
Header::header_len(self.header.block_type),
);
Ok(self.header)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
pub enum EccStatus {
#[default]
Ok,
Unrecognized,
Corrected,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum EccRecoveryKind {
Secded,
Shard,
}
#[derive(Clone)]
pub struct Block {
pub header: Header,
pub data: Slice,
}
impl Block {
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
}
fn read_payload_and_verify<R: crate::io::Read>(
reader: &mut R,
data_length: u32,
ecc_length: u32,
expected: Checksum,
#[cfg_attr(
not(feature = "page_ecc"),
expect(unused_variables, reason = "recovery scheme only used under page_ecc")
)]
ecc_params: EccParams,
) -> crate::Result<(Slice, Option<EccRecoveryKind>)> {
let data = Slice::from_reader(reader, data_length as usize)?;
let computed = Checksum::from_raw(crate::hash::hash128(&data));
if ecc_length == 0 {
computed.check(expected).inspect_err(|_| {
log::error!(
"Checksum mismatch for block payload, got={computed}, expected={expected}",
);
})?;
return Ok((data, None));
}
let parity = Slice::from_reader(reader, ecc_length as usize)?;
if computed == expected {
return Ok((data, None));
}
#[cfg(feature = "page_ecc")]
{
let expected_raw = expected.into_u128();
if matches!(ecc_params, crate::table::block::EccParams::Secded) {
let mut healed = data.to_vec();
if crate::secded::try_correct_block(&mut healed, &parity)
== crate::secded::SecdedOutcome::Corrected
&& crate::hash::hash128(&healed) == expected_raw
{
log::warn!(
"recovered block via SEC-DED single-bit heal after \
checksum mismatch (data_len={}, ecc_len={ecc_length})",
data.len(),
);
return Ok((Slice::from(healed), Some(EccRecoveryKind::Secded)));
}
log::error!(
"Checksum mismatch on SEC-DED block, heal failed, \
got={computed}, expected={expected}",
);
return Err(crate::Error::PageEccUnrecoverable {
got: computed,
expected,
});
}
let (data_shards, parity_shards) = ecc_params.as_shards();
if let Some(recovered) = crate::ecc::try_recover(
&data,
&parity,
data.len(),
data_shards,
parity_shards,
|buf| crate::hash::hash128(buf) == expected_raw,
)? {
log::warn!(
"recovered block from RS parity after checksum mismatch \
(data_len={}, ecc_len={ecc_length})",
data.len(),
);
return Ok((Slice::from(recovered), Some(EccRecoveryKind::Shard)));
}
log::error!(
"Checksum mismatch on ECC-protected block, recovery failed, \
got={computed}, expected={expected}",
);
Err(crate::Error::PageEccUnrecoverable {
got: computed,
expected,
})
}
#[cfg(not(feature = "page_ecc"))]
{
let _ = parity;
log::error!(
"block has ECC trailer (ecc_length={ecc_length}) but this \
build lacks the page_ecc feature — cannot attempt recovery; \
got={computed}, expected={expected}",
);
Err(crate::Error::ChecksumMismatch {
expected,
got: computed,
})
}
}
pub fn write_into<W: crate::io::Write>(
writer: &mut W,
data: &[u8],
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<Header> {
Self::write_into_with_flags(writer, data, identity, transform, 0)
}
pub(crate) fn write_into_with_flags<W: crate::io::Write>(
writer: &mut W,
data: &[u8],
identity: BlockIdentity,
transform: &BlockTransform<'_>,
extra_flags: u8,
) -> crate::Result<Header> {
Self::prepare_with_flags(data, identity, transform, extra_flags)?.write_to(writer)
}
#[expect(
clippy::too_many_lines,
reason = "linear transform pipeline: compress → encrypt → checksum → ecc; \
each step is small but they share state (header, payload, owned buffers) \
so factoring would just hide the data flow"
)]
pub(crate) fn prepare_with_flags<'a>(
data: &'a [u8],
identity: BlockIdentity,
transform: &BlockTransform<'_>,
extra_flags: u8,
) -> crate::Result<PreparedBlock<'a>> {
let compression = transform.compression();
let encryption = transform.encryption();
#[cfg(zstd_any)]
let zstd_dict = transform.zstd_dict();
let block_type = identity.block_type;
if data.len() > MAX_DECOMPRESSION_SIZE as usize {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: data.len() as u64,
limit: u64::from(MAX_DECOMPRESSION_SIZE),
});
}
let block_flags = {
use crate::table::block::header::block_flags;
debug_assert_eq!(
extra_flags & !block_flags::KNOWN,
0,
"extra_flags must contain only defined block_flags bits",
);
let mut f = extra_flags;
if transform.compression() != CompressionType::None {
f |= block_flags::COMPRESSED;
}
if transform.encryption().is_some() {
f |= block_flags::ENCRYPTED;
}
f
};
let mut header = Header {
block_type,
block_flags,
checksum: Checksum::from_raw(0), data_length: 0,
#[expect(clippy::cast_possible_truncation, reason = "blocks are limited to u32")]
uncompressed_length: data.len() as u32,
};
#[cfg(any(feature = "lz4", zstd_any))]
let mut compressed_buf: Option<Vec<u8>> = None;
#[cfg_attr(
not(zstd_any),
expect(unused_mut, reason = "`layout` is only mutated on zstd-enabled builds")
)]
let mut layout: Vec<u32> = Vec::new();
match compression {
CompressionType::None => {}
#[cfg(feature = "lz4")]
CompressionType::Lz4 => {
compressed_buf = Some(lz4_flex::compress(data));
}
#[cfg(zstd_any)]
CompressionType::Zstd(level) => {
if block_type == BlockType::Data {
let (buf, lay) =
crate::compression::ZstdBackend::compress_with_layout(data, level)?;
compressed_buf = Some(buf);
layout = lay;
} else {
compressed_buf = Some(crate::compression::ZstdBackend::compress(data, level)?);
}
}
#[cfg(zstd_any)]
CompressionType::ZstdDict { level, dict_id } => {
let dict = zstd_dict.ok_or(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: None,
})?;
if dict.id() != dict_id {
return Err(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: Some(dict.id()),
});
}
compressed_buf = Some(crate::compression::ZstdBackend::compress_with_dict(
data,
level,
dict.raw(),
)?);
}
}
let encrypted_buf: Option<Vec<u8>>;
#[cfg(any(feature = "lz4", zstd_any))]
{
encrypted_buf = encryption
.map(|enc| {
encrypt_block_payload(
enc,
compressed_buf.take(),
data,
&identity,
compression,
block_flags,
)
})
.transpose()?;
}
#[cfg(not(any(feature = "lz4", zstd_any)))]
{
encrypted_buf = encryption
.map(|enc| {
encrypt_block_payload(enc, None, data, &identity, compression, block_flags)
})
.transpose()?;
}
let payload: Cow<'a, [u8]> = if let Some(enc) = encrypted_buf {
Cow::Owned(enc)
} else {
#[cfg(any(feature = "lz4", zstd_any))]
{
compressed_buf.map_or(Cow::Borrowed(data), Cow::Owned)
}
#[cfg(not(any(feature = "lz4", zstd_any)))]
{
Cow::Borrowed(data)
}
};
let max_payload = (u64::from(MAX_DECOMPRESSION_SIZE)
+ encryption.map_or(0u64, |enc| u64::from(enc.max_overhead())))
.min(u64::from(u32::MAX));
if payload.len() as u64 > max_payload {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: payload.len() as u64,
limit: max_payload,
});
}
#[expect(clippy::cast_possible_truncation, reason = "bounded by check above")]
let payload_len = payload.len() as u32;
header.data_length = payload_len;
header.checksum = Checksum::from_raw(crate::hash::hash128(&payload));
#[cfg(feature = "page_ecc")]
let parity_buf: Option<Vec<u8>> = if let Some(ecc_params) = transform.ecc_params() {
let p = match ecc_params {
crate::table::block::EccParams::Secded => {
crate::secded::encode_block_parity(&payload)
}
crate::table::block::EccParams::Shard { .. } => {
let (data_shards, parity_shards) = ecc_params.as_shards();
crate::ecc::encode_parity(&payload, data_shards, parity_shards)?
}
};
let p_len =
u32::try_from(p.len()).map_err(|_| crate::Error::DecompressedSizeTooLarge {
declared: p.len() as u64,
limit: u64::from(u32::MAX),
})?;
if p_len > 0 {
header.block_flags |= crate::table::block::header::block_flags::ECC_PARITY;
}
Some(p)
} else {
None
};
#[cfg(not(feature = "page_ecc"))]
let parity_buf: Option<Vec<u8>> = None;
Ok(PreparedBlock {
header,
payload,
parity: parity_buf,
layout,
})
}
#[expect(
clippy::too_many_lines,
reason = "encrypt/no-encrypt branches duplicate compression match — see comment above"
)]
pub fn from_reader<R: crate::io::Read>(
reader: &mut R,
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<Self> {
let compression = transform.compression();
let encryption = transform.encryption();
#[cfg(zstd_any)]
let zstd_dict = transform.zstd_dict();
let header = Header::decode_from(reader)?;
let enc_overhead = encryption.map_or(0u64, |e| u64::from(e.max_overhead()));
let max_data_length = u64::from(MAX_DECOMPRESSION_SIZE) + enc_overhead;
if u64::from(header.data_length) > max_data_length {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(header.data_length),
limit: max_data_length,
});
}
if header.uncompressed_length > MAX_DECOMPRESSION_SIZE {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(header.uncompressed_length),
limit: u64::from(MAX_DECOMPRESSION_SIZE),
});
}
let ecc_length = if block_has_parity(&header, transform) {
expected_parity_len(header.data_length, block_ecc_params(&header, transform))
} else {
0
};
let data = if let Some(enc) = encryption {
let (raw_vec, _recovery) = Self::read_payload_and_verify(
reader,
header.data_length,
ecc_length,
header.checksum,
block_ecc_params(&header, transform),
)?;
let decrypted = decrypt_block_payload(enc, &raw_vec, &identity)?;
match compression {
CompressionType::None => {
#[expect(
clippy::cast_possible_truncation,
reason = "values are u32 length max"
)]
let actual_len = decrypted.len() as u32;
if header.uncompressed_length != actual_len {
return Err(crate::Error::InvalidHeader("Block"));
}
Slice::from(decrypted)
}
#[cfg(feature = "lz4")]
CompressionType::Lz4 => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder =
unsafe { Slice::builder_unzeroed(header.uncompressed_length as usize) };
let bytes_written = lz4_flex::decompress_into(&decrypted, &mut builder)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::Zstd(_) => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder =
unsafe { Slice::builder_unzeroed(header.uncompressed_length as usize) };
let bytes_written =
crate::compression::ZstdBackend::decompress_into(&decrypted, &mut builder)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::ZstdDict { dict_id, .. } => {
let dict = zstd_dict.ok_or(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: None,
})?;
if dict.id() != dict_id {
return Err(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: Some(dict.id()),
});
}
let decompressed = crate::compression::ZstdBackend::decompress_with_dict(
&decrypted,
dict,
header.uncompressed_length as usize,
)
.map_err(|_| crate::Error::Decompress(compression))?;
if decompressed.len() != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
Slice::from(decompressed)
}
}
} else {
let raw_data = if ecc_length == 0 {
let s = Slice::from_reader(reader, header.data_length as usize)?;
let checksum = Checksum::from_raw(crate::hash::hash128(&s));
checksum.check(header.checksum).inspect_err(|_| {
log::error!(
"Checksum mismatch for <bufreader>, got={}, expected={}",
checksum,
header.checksum,
);
})?;
s
} else {
let (payload, _recovery) = Self::read_payload_and_verify(
reader,
header.data_length,
ecc_length,
header.checksum,
block_ecc_params(&header, transform),
)?;
payload
};
match compression {
CompressionType::None => {
#[expect(
clippy::cast_possible_truncation,
reason = "values are u32 length max"
)]
let actual_len = raw_data.len() as u32;
if header.uncompressed_length != actual_len {
return Err(crate::Error::InvalidHeader("Block"));
}
raw_data
}
#[cfg(feature = "lz4")]
CompressionType::Lz4 => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder =
unsafe { Slice::builder_unzeroed(header.uncompressed_length as usize) };
let bytes_written = lz4_flex::decompress_into(&raw_data, &mut builder)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::Zstd(_) => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder =
unsafe { Slice::builder_unzeroed(header.uncompressed_length as usize) };
let bytes_written =
crate::compression::ZstdBackend::decompress_into(&raw_data, &mut builder)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::ZstdDict { dict_id, .. } => {
let dict = zstd_dict.ok_or(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: None,
})?;
if dict.id() != dict_id {
return Err(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: Some(dict.id()),
});
}
let decompressed = crate::compression::ZstdBackend::decompress_with_dict(
&raw_data,
dict,
header.uncompressed_length as usize,
)
.map_err(|_| crate::Error::Decompress(compression))?;
if decompressed.len() != header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
Slice::from(decompressed)
}
}
};
Ok(Self { header, data })
}
pub fn from_file(
file: &dyn FsFile,
handle: BlockHandle,
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<Self> {
let (block, _status) = Self::from_file_with_status(file, handle, identity, transform)?;
Ok(block)
}
pub fn from_file_with_status(
file: &dyn FsFile,
handle: BlockHandle,
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<(Self, EccStatus)> {
let (block, status, _recovery) =
Self::from_file_with_recovery(file, handle, identity, transform)?;
Ok((block, status))
}
#[expect(
clippy::too_many_lines,
reason = "encrypt/no-encrypt branches duplicate compression match — see from_reader"
)]
pub(crate) fn from_file_with_recovery(
file: &dyn FsFile,
handle: BlockHandle,
identity: BlockIdentity,
transform: &BlockTransform<'_>,
) -> crate::Result<(Self, EccStatus, Option<EccRecoveryKind>)> {
let compression = transform.compression();
let encryption = transform.encryption();
#[cfg(zstd_any)]
let zstd_dict = transform.zstd_dict();
let enc_overhead = encryption.map_or(0u64, |e| u64::from(e.max_overhead()));
let max_payload = u64::from(MAX_DECOMPRESSION_SIZE) + enc_overhead;
let max_ecc_overhead = match transform.ecc_params() {
Some(params) => {
#[expect(
clippy::cast_possible_truncation,
reason = "max_payload is MAX_DECOMPRESSION_SIZE (+ enc overhead), well below u32::MAX"
)]
let max_payload_u32 = max_payload.min(u64::from(u32::MAX)) as u32;
u64::from(expected_parity_len(max_payload_u32, params))
}
None => 0,
};
let max_on_disk_size = max_payload + max_ecc_overhead + Header::MAX_LEN as u64;
if u64::from(handle.size()) > max_on_disk_size {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(handle.size()),
limit: max_on_disk_size,
});
}
let (header, data, ecc_status, recovery) = if let Some(enc) = encryption {
let block_size = handle.size() as usize;
if block_size < Header::MIN_LEN {
return Err(crate::Error::InvalidHeader("Block"));
}
let mut buf = vec![0u8; block_size];
let n = file.read_at(&mut buf, *handle.offset())?;
if n != block_size {
return Err(crate::Error::Io(crate::io::Error::new(
crate::io::ErrorKind::UnexpectedEof,
format!(
"block read_at: expected {block_size} bytes, got {n} at offset {}",
*handle.offset(),
),
)));
}
let parsed_header = Header::decode_from(&mut &buf[..])?;
let header_len = Header::header_len(parsed_header.block_type);
let has_ecc = block_has_parity(&parsed_header, transform);
let ecc_length = if has_ecc {
expected_parity_len(
parsed_header.data_length,
block_ecc_params(&parsed_header, transform),
)
} else {
0
};
let actual_payload_plus_ecc = block_size.saturating_sub(header_len);
let actual_data_len = parsed_header.data_length as usize;
let ecc_status = classify_block_trailer(
has_ecc,
actual_payload_plus_ecc,
actual_data_len,
ecc_length,
&handle,
)?;
let max_data_length = u64::from(MAX_DECOMPRESSION_SIZE) + enc_overhead;
if u64::from(parsed_header.data_length) > max_data_length {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(parsed_header.data_length),
limit: max_data_length,
});
}
if parsed_header.uncompressed_length > MAX_DECOMPRESSION_SIZE {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(parsed_header.uncompressed_length),
limit: u64::from(MAX_DECOMPRESSION_SIZE),
});
}
let (buf, payload_corrected) = if ecc_length == 0 {
#[expect(
clippy::indexing_slicing,
reason = "actual_data_len <= post-header len"
)]
let checksum = Checksum::from_raw(crate::hash::hash128(
&buf[header_len..header_len + actual_data_len],
));
checksum.check(parsed_header.checksum).inspect_err(|_| {
log::error!(
"Checksum mismatch for block {handle:?}, got={}, expected={}",
checksum,
parsed_header.checksum,
);
})?;
buf.copy_within(header_len..header_len + actual_data_len, 0);
buf.truncate(actual_data_len);
(Slice::from(buf), None)
} else {
#[expect(clippy::indexing_slicing, reason = "header was decoded from buf")]
let mut cursor = crate::io::Cursor::new(&buf[header_len..]);
Self::read_payload_and_verify(
&mut cursor,
parsed_header.data_length,
ecc_length,
parsed_header.checksum,
block_ecc_params(&parsed_header, transform),
)?
};
let ecc_status = if payload_corrected.is_some() {
EccStatus::Corrected
} else {
ecc_status
};
let decrypted = decrypt_block_payload(enc, &buf, &identity)?;
let data = match compression {
CompressionType::None => {
#[expect(
clippy::cast_possible_truncation,
reason = "values are u32 length max"
)]
let actual_len = decrypted.len() as u32;
if parsed_header.uncompressed_length != actual_len {
return Err(crate::Error::InvalidHeader("Block"));
}
Slice::from(decrypted)
}
#[cfg(feature = "lz4")]
CompressionType::Lz4 => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut decompressed = unsafe {
Slice::builder_unzeroed(parsed_header.uncompressed_length as usize)
};
let bytes_written = lz4_flex::decompress_into(&decrypted, &mut decompressed)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
decompressed.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::Zstd(_) => {
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder = unsafe {
Slice::builder_unzeroed(parsed_header.uncompressed_length as usize)
};
let bytes_written =
crate::compression::ZstdBackend::decompress_into(&decrypted, &mut builder)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::ZstdDict { dict_id, .. } => {
let dict = zstd_dict.ok_or(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: None,
})?;
if dict.id() != dict_id {
return Err(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: Some(dict.id()),
});
}
let decompressed = crate::compression::ZstdBackend::decompress_with_dict(
&decrypted,
dict,
parsed_header.uncompressed_length as usize,
)
.map_err(|_| crate::Error::Decompress(compression))?;
if decompressed.len() != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
Slice::from(decompressed)
}
};
(parsed_header, data, ecc_status, payload_corrected)
} else {
let buf = crate::file::read_exact(file, *handle.offset(), handle.size() as usize)?;
let parsed_header = Header::decode_from(&mut &buf[..])?;
let header_len = Header::header_len(parsed_header.block_type);
let has_ecc = block_has_parity(&parsed_header, transform);
let ecc_length = if has_ecc {
expected_parity_len(
parsed_header.data_length,
block_ecc_params(&parsed_header, transform),
)
} else {
0
};
let actual_payload_plus_ecc = buf.len().saturating_sub(header_len);
let actual_data_len = parsed_header.data_length as usize;
let ecc_status = classify_block_trailer(
has_ecc,
actual_payload_plus_ecc,
actual_data_len,
ecc_length,
&handle,
)?;
if parsed_header.uncompressed_length > MAX_DECOMPRESSION_SIZE {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(parsed_header.uncompressed_length),
limit: u64::from(MAX_DECOMPRESSION_SIZE),
});
}
let (payload_slice, payload_corrected): (Slice, Option<EccRecoveryKind>) =
if ecc_length == 0 {
#[expect(
clippy::indexing_slicing,
reason = "actual_data_len <= post-header len"
)]
let checksum = Checksum::from_raw(crate::hash::hash128(
&buf[header_len..header_len + actual_data_len],
));
checksum.check(parsed_header.checksum).inspect_err(|_| {
log::error!(
"Checksum mismatch for block {handle:?}, got={}, expected={}",
checksum,
parsed_header.checksum,
);
})?;
(buf.slice(header_len..header_len + actual_data_len), None)
} else {
#[expect(clippy::indexing_slicing, reason = "header was decoded from buf")]
let mut cursor = crate::io::Cursor::new(&buf[header_len..]);
let (payload, recovery) = Self::read_payload_and_verify(
&mut cursor,
parsed_header.data_length,
ecc_length,
parsed_header.checksum,
block_ecc_params(&parsed_header, transform),
)?;
(payload, recovery)
};
let ecc_status = if payload_corrected.is_some() {
EccStatus::Corrected
} else {
ecc_status
};
let data = match compression {
CompressionType::None => {
#[expect(
clippy::cast_possible_truncation,
reason = "values are u32 length max"
)]
let actual_len = payload_slice.len() as u32;
if parsed_header.uncompressed_length != actual_len {
return Err(crate::Error::InvalidHeader("Block"));
}
payload_slice
}
#[cfg(feature = "lz4")]
CompressionType::Lz4 => {
let compressed_data: &[u8] = &payload_slice;
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut decompressed = unsafe {
Slice::builder_unzeroed(parsed_header.uncompressed_length as usize)
};
let bytes_written =
lz4_flex::decompress_into(compressed_data, &mut decompressed)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
decompressed.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::Zstd(_) => {
let compressed_data: &[u8] = &payload_slice;
#[expect(unsafe_code, reason = "fill an uninitialized Slice via decompress")]
let mut builder = unsafe {
Slice::builder_unzeroed(parsed_header.uncompressed_length as usize)
};
let bytes_written = crate::compression::ZstdBackend::decompress_into(
compressed_data,
&mut builder,
)
.map_err(|_| crate::Error::Decompress(compression))?;
if bytes_written != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
builder.freeze().into()
}
#[cfg(zstd_any)]
CompressionType::ZstdDict { dict_id, .. } => {
let compressed_data: &[u8] = &payload_slice;
let dict = zstd_dict.ok_or(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: None,
})?;
if dict.id() != dict_id {
return Err(crate::Error::ZstdDictMismatch {
expected: dict_id,
got: Some(dict.id()),
});
}
let decompressed = crate::compression::ZstdBackend::decompress_with_dict(
compressed_data,
dict,
parsed_header.uncompressed_length as usize,
)
.map_err(|_| crate::Error::Decompress(compression))?;
if decompressed.len() != parsed_header.uncompressed_length as usize {
return Err(crate::Error::Decompress(compression));
}
Slice::from(decompressed)
}
};
(parsed_header, data, ecc_status, payload_corrected)
};
Ok((Self { header, data }, ecc_status, recovery))
}
#[cfg(feature = "zstd")]
pub(crate) fn read_data_frame(
file: &dyn FsFile,
handle: BlockHandle,
transform: &BlockTransform<'_>,
) -> crate::Result<(Header, Slice, Option<EccRecoveryKind>)> {
if transform.encryption().is_some() {
return Err(crate::Error::Io(crate::io::Error::other(
"read_data_frame: encrypted blocks are not supported on the lazy path",
)));
}
let max_ecc_overhead = match transform.ecc_params() {
Some(params) => u64::from(expected_parity_len(MAX_DECOMPRESSION_SIZE, params)),
None => 0,
};
let max_on_disk_size =
u64::from(MAX_DECOMPRESSION_SIZE) + max_ecc_overhead + Header::MAX_LEN as u64;
if u64::from(handle.size()) > max_on_disk_size {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(handle.size()),
limit: max_on_disk_size,
});
}
let buf = crate::file::read_exact(file, *handle.offset(), handle.size() as usize)?;
let parsed_header = Header::decode_from(&mut &buf[..])?;
if parsed_header.uncompressed_length > MAX_DECOMPRESSION_SIZE {
return Err(crate::Error::DecompressedSizeTooLarge {
declared: u64::from(parsed_header.uncompressed_length),
limit: u64::from(MAX_DECOMPRESSION_SIZE),
});
}
let header_len = Header::header_len(parsed_header.block_type);
let has_ecc = block_has_parity(&parsed_header, transform);
let ecc_length = if has_ecc {
expected_parity_len(
parsed_header.data_length,
block_ecc_params(&parsed_header, transform),
)
} else {
0
};
let actual_payload_plus_ecc = buf.len().saturating_sub(header_len);
let actual_data_len = parsed_header.data_length as usize;
let _ecc_status = classify_block_trailer(
has_ecc,
actual_payload_plus_ecc,
actual_data_len,
ecc_length,
&handle,
)?;
let (payload, recovery): (Slice, Option<EccRecoveryKind>) = if ecc_length == 0 {
#[expect(
clippy::indexing_slicing,
reason = "actual_data_len <= post-header len, checked via classify_block_trailer"
)]
let checksum = Checksum::from_raw(crate::hash::hash128(
&buf[header_len..header_len + actual_data_len],
));
checksum.check(parsed_header.checksum)?;
(buf.slice(header_len..header_len + actual_data_len), None)
} else {
#[expect(clippy::indexing_slicing, reason = "header was decoded from buf")]
let mut cursor = crate::io::Cursor::new(&buf[header_len..]);
let (frame, recovery) = Self::read_payload_and_verify(
&mut cursor,
parsed_header.data_length,
ecc_length,
parsed_header.checksum,
block_ecc_params(&parsed_header, transform),
)?;
(frame, recovery)
};
Ok((parsed_header, payload, recovery))
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::useless_vec,
clippy::cast_possible_truncation,
clippy::expect_used,
reason = "test code"
)]
mod tests;