#[cfg(zstd_any)]
use crate::compression::ZstdDictionary;
use crate::{CompressionType, encryption::EncryptionProvider};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct EccParams {
data_shards: u8,
parity_shards: u8,
}
impl EccParams {
pub const RS_4_2: Self = Self {
data_shards: 4,
parity_shards: 2,
};
pub fn try_new(data_shards: u8, parity_shards: u8) -> crate::Result<Self> {
if data_shards == 0 {
return Err(crate::Error::FeatureUnsupported("ecc_scheme data_shards=0"));
}
if parity_shards == 0 {
return Err(crate::Error::FeatureUnsupported(
"ecc_scheme parity_shards=0",
));
}
Ok(Self {
data_shards,
parity_shards,
})
}
#[must_use]
pub const fn data_shards(self) -> u8 {
self.data_shards
}
#[must_use]
pub const fn parity_shards(self) -> u8 {
self.parity_shards
}
#[must_use]
pub const fn as_shards(self) -> (usize, usize) {
(self.data_shards as usize, self.parity_shards as usize)
}
}
pub struct CompressionContext<'a> {
kind: CompressionType,
#[cfg(zstd_any)]
zstd_dict: Option<&'a ZstdDictionary>,
#[cfg(not(zstd_any))]
_lifetime: core::marker::PhantomData<&'a ()>,
}
#[cfg_attr(
not(zstd_any),
expect(
clippy::elidable_lifetime_names,
reason = "'a kept for cross-feature-matrix signature stability; \
used by with_dict under any zstd feature"
)
)]
impl<'a> CompressionContext<'a> {
pub fn new(kind: CompressionType) -> crate::Result<Self> {
if kind == CompressionType::None {
return Err(crate::Error::FeatureUnsupported("compression-context-none"));
}
#[cfg(zstd_any)]
if matches!(kind, CompressionType::ZstdDict { .. }) {
return Err(crate::Error::FeatureUnsupported(
"compression-context-zstd-dict-via-new",
));
}
Ok(Self {
kind,
#[cfg(zstd_any)]
zstd_dict: None,
#[cfg(not(zstd_any))]
_lifetime: core::marker::PhantomData,
})
}
#[cfg(zstd_any)]
#[must_use]
pub fn with_dict(level: i32, dict: &'a ZstdDictionary) -> Self {
Self {
kind: CompressionType::ZstdDict {
level,
dict_id: dict.id(),
},
zstd_dict: Some(dict),
}
}
#[must_use]
pub fn kind(&self) -> CompressionType {
self.kind
}
#[cfg(zstd_any)]
#[must_use]
pub fn zstd_dict(&self) -> Option<&ZstdDictionary> {
self.zstd_dict
}
}
pub enum BlockTransform<'a> {
Plain,
Compressed(CompressionContext<'a>),
Encrypted(&'a dyn EncryptionProvider),
CompressedAndEncrypted(CompressionContext<'a>, &'a dyn EncryptionProvider),
#[cfg(feature = "page_ecc")]
PlainEcc(EccParams),
#[cfg(feature = "page_ecc")]
CompressedEcc(CompressionContext<'a>, EccParams),
#[cfg(feature = "page_ecc")]
EncryptedEcc(&'a dyn EncryptionProvider, EccParams),
#[cfg(feature = "page_ecc")]
CompressedAndEncryptedEcc(
CompressionContext<'a>,
&'a dyn EncryptionProvider,
EccParams,
),
}
impl BlockTransform<'_> {
pub const PLAIN: Self = Self::Plain;
#[must_use]
pub fn compression(&self) -> CompressionType {
match self {
Self::Plain | Self::Encrypted(_) => CompressionType::None,
Self::Compressed(ctx) | Self::CompressedAndEncrypted(ctx, _) => ctx.kind(),
#[cfg(feature = "page_ecc")]
Self::PlainEcc(_) | Self::EncryptedEcc(_, _) => CompressionType::None,
#[cfg(feature = "page_ecc")]
Self::CompressedEcc(ctx, _) | Self::CompressedAndEncryptedEcc(ctx, _, _) => ctx.kind(),
}
}
#[cfg(zstd_any)]
#[must_use]
pub fn zstd_dict(&self) -> Option<&ZstdDictionary> {
match self {
Self::Plain | Self::Encrypted(_) => None,
Self::Compressed(ctx) | Self::CompressedAndEncrypted(ctx, _) => ctx.zstd_dict(),
#[cfg(feature = "page_ecc")]
Self::PlainEcc(_) | Self::EncryptedEcc(_, _) => None,
#[cfg(feature = "page_ecc")]
Self::CompressedEcc(ctx, _) | Self::CompressedAndEncryptedEcc(ctx, _, _) => {
ctx.zstd_dict()
}
}
}
#[must_use]
pub fn encryption(&self) -> Option<&dyn EncryptionProvider> {
match self {
Self::Plain | Self::Compressed(_) => None,
Self::Encrypted(enc) | Self::CompressedAndEncrypted(_, enc) => Some(*enc),
#[cfg(feature = "page_ecc")]
Self::PlainEcc(_) | Self::CompressedEcc(_, _) => None,
#[cfg(feature = "page_ecc")]
Self::EncryptedEcc(enc, _) | Self::CompressedAndEncryptedEcc(_, enc, _) => Some(*enc),
}
}
#[must_use]
pub fn page_ecc(&self) -> bool {
match self {
Self::Plain
| Self::Compressed(_)
| Self::Encrypted(_)
| Self::CompressedAndEncrypted(_, _) => false,
#[cfg(feature = "page_ecc")]
Self::PlainEcc(_)
| Self::CompressedEcc(_, _)
| Self::EncryptedEcc(_, _)
| Self::CompressedAndEncryptedEcc(_, _, _) => true,
}
}
#[must_use]
pub fn ecc_params(&self) -> Option<EccParams> {
match self {
Self::Plain
| Self::Compressed(_)
| Self::Encrypted(_)
| Self::CompressedAndEncrypted(_, _) => None,
#[cfg(feature = "page_ecc")]
Self::PlainEcc(p)
| Self::CompressedEcc(_, p)
| Self::EncryptedEcc(_, p)
| Self::CompressedAndEncryptedEcc(_, _, p) => Some(*p),
}
}
#[cfg(feature = "page_ecc")]
#[must_use]
pub fn with_ecc(self, params: EccParams) -> Self {
match self {
Self::Plain | Self::PlainEcc(_) => Self::PlainEcc(params),
Self::Compressed(ctx) | Self::CompressedEcc(ctx, _) => Self::CompressedEcc(ctx, params),
Self::Encrypted(enc) | Self::EncryptedEcc(enc, _) => Self::EncryptedEcc(enc, params),
Self::CompressedAndEncrypted(ctx, enc)
| Self::CompressedAndEncryptedEcc(ctx, enc, _) => {
Self::CompressedAndEncryptedEcc(ctx, enc, params)
}
}
}
#[cfg(not(feature = "page_ecc"))]
#[must_use]
pub fn with_ecc(self, _params: EccParams) -> Self {
self
}
}
impl<'a> BlockTransform<'a> {
pub fn from_parts(
compression: CompressionType,
encryption: Option<&'a dyn EncryptionProvider>,
#[cfg(zstd_any)] zstd_dict: Option<&'a ZstdDictionary>,
) -> crate::Result<Self> {
if compression == CompressionType::None {
return Ok(match encryption {
Some(enc) => Self::Encrypted(enc),
None => Self::Plain,
});
}
#[cfg(zstd_any)]
let ctx = if let CompressionType::ZstdDict { level, dict_id } = compression {
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()),
});
}
CompressionContext::with_dict(level, dict)
} else {
let _ = zstd_dict;
CompressionContext::new(compression)?
};
#[cfg(not(zstd_any))]
let ctx = CompressionContext::new(compression)?;
Ok(match encryption {
Some(enc) => Self::CompressedAndEncrypted(ctx, enc),
None => Self::Compressed(ctx),
})
}
}
#[cfg(test)]
#[expect(
clippy::expect_used,
reason = "tests panic on the unhappy paths to surface failures loudly"
)]
mod tests {
use super::*;
#[test]
fn plain_transform_reports_no_compression_no_encryption_no_ecc() {
let t = BlockTransform::Plain;
assert_eq!(t.compression(), CompressionType::None);
assert!(t.encryption().is_none());
assert!(!t.page_ecc());
}
#[test]
fn plain_constant_matches_plain_variant() {
let t = BlockTransform::PLAIN;
assert!(matches!(t, BlockTransform::Plain));
assert!(!t.page_ecc());
}
#[cfg(feature = "page_ecc")]
#[test]
fn plain_ecc_variant_reports_ecc_enabled_no_other_transform() {
let t = BlockTransform::PlainEcc(EccParams::RS_4_2);
assert_eq!(t.compression(), CompressionType::None);
assert!(t.encryption().is_none());
assert!(t.page_ecc());
}
#[cfg(all(feature = "page_ecc", feature = "lz4"))]
#[test]
fn compressed_ecc_carries_compression_kind_and_reports_ecc() {
let Ok(ctx) = CompressionContext::new(CompressionType::Lz4) else {
panic!("Lz4 ctx construction is total");
};
let t = BlockTransform::CompressedEcc(ctx, EccParams::RS_4_2);
assert_eq!(t.compression(), CompressionType::Lz4);
assert!(t.encryption().is_none());
assert!(t.page_ecc());
}
#[test]
fn eccparams_try_new_rejects_zero_shards() {
assert!(matches!(
EccParams::try_new(0, 2),
Err(crate::Error::FeatureUnsupported(_))
));
assert!(matches!(
EccParams::try_new(8, 0),
Err(crate::Error::FeatureUnsupported(_))
));
let ok = EccParams::try_new(8, 2).expect("non-zero shards are accepted");
assert_eq!((ok.data_shards(), ok.parity_shards()), (8, 2));
assert_eq!(ok.as_shards(), (8, 2));
}
#[cfg(feature = "page_ecc")]
#[test]
fn with_ecc_upgrades_plain_to_plain_ecc() {
let p = EccParams::try_new(8, 2).expect("valid shards");
let t = BlockTransform::Plain.with_ecc(p);
assert!(matches!(t, BlockTransform::PlainEcc(_)));
assert_eq!(t.ecc_params(), Some(p));
assert_eq!(t.compression(), CompressionType::None);
assert!(t.encryption().is_none());
let p2 = EccParams::try_new(4, 2).expect("valid shards");
assert_eq!(t.with_ecc(p2).ecc_params(), Some(p2));
}
#[cfg(all(feature = "page_ecc", feature = "encryption"))]
#[test]
fn with_ecc_upgrades_encrypted_variants() {
let p = EccParams::try_new(8, 2).expect("valid shards");
let enc = crate::encryption::Aes256GcmProvider::new(&[0x11; 32]);
let t = BlockTransform::Encrypted(&enc).with_ecc(p);
assert!(matches!(t, BlockTransform::EncryptedEcc(_, _)));
assert_eq!(t.ecc_params(), Some(p));
assert!(t.encryption().is_some());
assert_eq!(t.compression(), CompressionType::None);
#[cfg(feature = "lz4")]
{
let ctx = CompressionContext::new(CompressionType::Lz4).expect("lz4 ctx");
let t = BlockTransform::CompressedAndEncrypted(ctx, &enc).with_ecc(p);
assert!(matches!(
t,
BlockTransform::CompressedAndEncryptedEcc(_, _, _)
));
assert_eq!(t.ecc_params(), Some(p));
assert!(t.encryption().is_some());
assert_eq!(t.compression(), CompressionType::Lz4);
}
}
}