use std::slice::Iter;
use ozzy_journal::operation::{
CanonicalOperation, OperationKind, canonical_body_digest,
logical_operation_digest_with_body_digest,
};
use ozzy_proto::NodeId;
use ozzy_proto::{Envelope, Opcode};
use super::{
COMMON_BYTES, EncodeContext, Reader, WireError, WireLimits, Writer, validate_prefix,
validate_scope,
};
use super::{ReadFields, WriteFields};
use crate::flow::ReceiveEpoch;
use crate::{Digest, OpNumber, Prefix, Scope};
mod plan;
use plan::Planned;
pub(super) const DESCRIPTOR_BYTES: usize = 86;
pub(super) const BATCH_BYTES: usize = COMMON_BYTES + 8 + 32 + 40 + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Operation<'a> {
canonical: CanonicalOperation<'a>,
body_digest: Digest,
digest: Digest,
}
impl<'a> Operation<'a> {
pub fn from_verified(canonical: CanonicalOperation<'a>, body_digest: Digest) -> Self {
Self {
digest: logical_operation_digest_with_body_digest(&canonical, body_digest),
canonical,
body_digest,
}
}
pub const fn canonical(self) -> CanonicalOperation<'a> {
self.canonical
}
pub const fn body_digest(self) -> Digest {
self.body_digest
}
pub const fn prefix(self) -> Prefix {
Prefix {
op: OpNumber(self.canonical.op_number),
digest: self.digest,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerifiedOperation<'a>(Operation<'a>);
impl<'a> VerifiedOperation<'a> {
pub const fn canonical(self) -> CanonicalOperation<'a> {
self.0.canonical()
}
pub const fn body_digest(self) -> Digest {
self.0.body_digest()
}
pub const fn prefix(self) -> Prefix {
self.0.prefix()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Prepare<'a> {
pub scope: Scope,
pub committed: Prefix,
pub operations: &'a [Operation<'a>],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrepareEncoding {
pub header: [u8; 64],
pub metadata_bytes: usize,
pub payload_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrepareMetadata {
pub metadata_bytes: usize,
pub payload_bytes: usize,
}
pub fn encode_prepare_unbound(
sender: NodeId,
prepare: Prepare<'_>,
metadata: &mut [u8],
limits: WireLimits,
) -> Result<PrepareMetadata, WireError> {
Planned::new(sender, None, prepare, limits)?.write(metadata, None)
}
pub fn encode_prepare(
context: EncodeContext,
prepare: Prepare<'_>,
metadata: &mut [u8],
payload: &mut [u8],
) -> Result<PrepareEncoding, WireError> {
Planned::new(context.sender, None, prepare, context.limits)?.bind(
context,
metadata,
Some(payload),
)
}
pub fn encode_prepare_metadata(
context: EncodeContext,
prepare: Prepare<'_>,
metadata: &mut [u8],
) -> Result<PrepareEncoding, WireError> {
Planned::new(context.sender, None, prepare, context.limits)?.bind(context, metadata, None)
}
pub fn encode_flow_prepare(
context: EncodeContext,
epoch: ReceiveEpoch,
prepare: Prepare<'_>,
metadata: &mut [u8],
payload: &mut [u8],
) -> Result<PrepareEncoding, WireError> {
Planned::new(context.sender, Some(epoch), prepare, context.limits)?.bind(
context,
metadata,
Some(payload),
)
}
pub fn encode_flow_prepare_metadata(
context: EncodeContext,
epoch: ReceiveEpoch,
prepare: Prepare<'_>,
metadata: &mut [u8],
) -> Result<PrepareEncoding, WireError> {
Planned::new(context.sender, Some(epoch), prepare, context.limits)?
.bind(context, metadata, None)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrepareBatch<'a> {
committed: Prefix,
batch: DecodedBatch<'a>,
}
impl<'a> PrepareBatch<'a> {
pub const fn scope(self) -> Scope {
self.batch.scope
}
pub const fn committed(self) -> Prefix {
self.committed
}
pub const fn predecessor(self) -> Prefix {
self.batch.predecessor
}
pub const fn end(self) -> Prefix {
self.batch.accepted
}
pub fn operations(self) -> Operations<'a> {
self.batch.operations()
}
pub fn verified_operations(
self,
) -> impl ExactSizeIterator<Item = VerifiedOperation<'a>> + std::iter::FusedIterator {
self.operations().map(VerifiedOperation)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct DecodedBatch<'a> {
pub scope: Scope,
pub predecessor: Prefix,
pub accepted: Prefix,
descriptors: &'a [u8],
pub payload: &'a [u8],
}
impl<'a> DecodedBatch<'a> {
pub(super) fn operations(self) -> Operations<'a> {
Operations {
descriptors: self.descriptors.as_chunks::<DESCRIPTOR_BYTES>().0.iter(),
payload: self.payload,
scope: self.scope,
previous: self.predecessor,
}
}
}
#[derive(Debug, Clone)]
pub struct Operations<'a> {
descriptors: Iter<'a, [u8; DESCRIPTOR_BYTES]>,
payload: &'a [u8],
scope: Scope,
previous: Prefix,
}
impl<'a> Iterator for Operations<'a> {
type Item = Operation<'a>;
#[expect(
clippy::expect_used,
reason = "decode_batch checked every descriptor and body length"
)]
fn next(&mut self) -> Option<Self::Item> {
let descriptor = self.descriptors.next()?;
let mut reader = Reader::new(descriptor);
let operation = read_operation(&mut reader, &mut self.payload, self.scope, self.previous)
.expect("validated batch");
self.previous = operation.prefix();
Some(operation)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.descriptors.size_hint()
}
}
impl ExactSizeIterator for Operations<'_> {}
impl std::iter::FusedIterator for Operations<'_> {}
pub(super) fn decode_prepare<'a>(
scope: Scope,
mut reader: Reader<'a>,
payload: &'a [u8],
limits: WireLimits,
) -> Result<PrepareBatch<'a>, WireError> {
let first_op = reader.u64()?;
let predecessor = Prefix {
op: OpNumber(first_op.checked_sub(1).ok_or(WireError::Chain)?),
digest: reader.digest()?,
};
validate_prefix(predecessor)?;
let committed = reader.prefix()?;
let chain = Chain {
scope,
predecessor,
committed: Some(committed),
};
let batch = decode_batch(chain, reader, payload, limits)?;
Ok(PrepareBatch { committed, batch })
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Chain {
pub scope: Scope,
pub predecessor: Prefix,
pub committed: Option<Prefix>,
}
impl Chain {
fn check_start(self) -> Result<(), WireError> {
validate_prefix(self.predecessor)?;
self.check_commit(self.predecessor)
}
fn check_commit(self, position: Prefix) -> Result<(), WireError> {
match self.committed {
Some(committed) if committed.op == position.op && committed != position => {
Err(WireError::Chain)
}
_ => Ok(()),
}
}
}
pub(super) fn decode_batch<'a>(
chain: Chain,
mut reader: Reader<'a>,
mut payload: &'a [u8],
limits: WireLimits,
) -> Result<DecodedBatch<'a>, WireError> {
chain.check_start()?;
let Chain {
scope, predecessor, ..
} = chain;
let count = usize::try_from(reader.u32()?).map_err(|_| WireError::Limit)?;
let descriptor_bytes = metadata_length(count, 0, limits)?;
if reader.rest().len() != descriptor_bytes {
return Err(WireError::Length);
}
let descriptors = reader.rest();
let original_payload = payload;
let mut previous = predecessor;
for _ in 0..count {
let operation = read_operation(&mut reader, &mut payload, scope, previous)?;
validate_step(scope, previous, operation)?;
if canonical_body_digest(operation.canonical.body) != operation.body_digest
|| logical_operation_digest_with_body_digest(
&operation.canonical,
operation.body_digest,
) != operation.digest
{
return Err(WireError::Digest);
}
chain.check_commit(operation.prefix())?;
previous = operation.prefix();
}
if !payload.is_empty() {
return Err(WireError::Payload);
}
reader.finish()?;
Ok(DecodedBatch {
scope,
predecessor,
accepted: previous,
descriptors,
payload: original_payload,
})
}
fn read_operation<'a>(
reader: &mut Reader<'_>,
payload: &mut &'a [u8],
scope: Scope,
previous: Prefix,
) -> Result<Operation<'a>, WireError> {
let op_number = reader.u64()?;
let original_view = reader.u64()?;
let kind = u16::from_be_bytes(reader.array()?);
let kind = OperationKind::try_from(kind).map_err(|_| WireError::OperationKind(kind))?;
let bytes = usize::try_from(reader.u32()?).map_err(|_| WireError::Limit)?;
if bytes == 0 {
return Err(WireError::Limit);
}
let body_digest = reader.digest()?;
let digest = reader.digest()?;
let (body, rest) = payload.split_at_checked(bytes).ok_or(WireError::Length)?;
*payload = rest;
Ok(Operation {
canonical: CanonicalOperation {
group_id: scope.group_id,
configuration_epoch: scope.configuration_epoch,
original_view,
op_number,
previous_digest: previous.digest,
kind,
body,
},
body_digest,
digest,
})
}
fn validate_step(
scope: Scope,
previous: Prefix,
operation: Operation<'_>,
) -> Result<(), WireError> {
let canonical = operation.canonical;
validate_prefix(operation.prefix())?;
if canonical.group_id != scope.group_id
|| canonical.configuration_epoch != scope.configuration_epoch
|| canonical.original_view > scope.view
|| previous.op.0.checked_add(1) != Some(canonical.op_number)
|| canonical.previous_digest != previous.digest
{
return Err(WireError::Chain);
}
Ok(())
}
pub(super) fn metadata_length(
count: usize,
fixed_bytes: usize,
limits: WireLimits,
) -> Result<usize, WireError> {
if count == 0 || count > limits.max_operations || u32::try_from(count).is_err() {
return Err(WireError::Limit);
}
let bytes = count
.checked_mul(DESCRIPTOR_BYTES)
.and_then(|bytes| bytes.checked_add(fixed_bytes))
.ok_or(WireError::Limit)?;
if bytes > limits.envelope.max_metadata_bytes {
return Err(WireError::Limit);
}
Ok(bytes)
}
pub(super) fn validate_batch(
chain: Chain,
operations: &[Operation<'_>],
limits: WireLimits,
) -> Result<usize, WireError> {
metadata_length(operations.len(), 0, limits)?;
chain.check_start()?;
let mut previous = chain.predecessor;
let mut bytes = 0usize;
for &operation in operations {
validate_step(chain.scope, previous, operation)?;
chain.check_commit(operation.prefix())?;
bytes = bytes
.checked_add(operation.canonical.body.len())
.ok_or(WireError::Limit)?;
if bytes > limits.envelope.max_payload_bytes || operation.canonical.body.is_empty() {
return Err(WireError::Limit);
}
previous = operation.prefix();
}
Ok(bytes)
}
pub(super) fn write_batch(
writer: &mut Writer<'_>,
mut payload: Option<&mut [u8]>,
operations: &[Operation<'_>],
) -> Result<(), WireError> {
writer.bytes(&(operations.len() as u32).to_be_bytes())?;
let mut offset = 0_usize;
for &operation in operations {
let canonical = operation.canonical;
writer.u64(canonical.op_number)?;
writer.u64(canonical.original_view)?;
writer.bytes(&(canonical.kind as u16).to_be_bytes())?;
writer.bytes(&(canonical.body.len() as u32).to_be_bytes())?;
writer.bytes(operation.body_digest.as_bytes())?;
writer.bytes(operation.digest.as_bytes())?;
let end = offset
.checked_add(canonical.body.len())
.ok_or(WireError::Length)?;
if let Some(payload) = payload.as_deref_mut() {
let body = payload.get_mut(offset..end).ok_or(WireError::Capacity)?;
body.copy_from_slice(canonical.body);
}
offset = end;
}
Ok(())
}