Skip to main content

ozzy_proto/
data.rs

1//! Shared bounded data-command fields and multipart records.
2
3use super::{Envelope, EnvelopeError, EnvelopeLimits, Opcode};
4use crate::{
5    GroupId, MessageId,
6    layout::{LayoutError, Reader},
7};
8
9mod batch;
10mod descriptors;
11mod encoding;
12mod records;
13pub use encoding::Encoding;
14mod buffer;
15pub use batch::{BatchIter, BatchRecord, PayloadRef, RecordBatch, TinyRecords};
16mod indexed;
17mod owned;
18mod payload;
19pub use buffer::RecordBuffer;
20pub(super) use descriptors::validate_raw_record_entries;
21pub use descriptors::{
22    RecordDescriptor, RecordDescriptorIter, RecordDescriptors, decode_record_descriptors,
23};
24pub use indexed::{EncodedParts, IndexedRecords};
25pub use owned::{OwnedRecord, OwnedRecordIter, OwnedRecords};
26pub use payload::RecordPayload;
27pub(super) use records::validate_record_entries;
28pub use records::{DecodedRecord, Parts, RecordIter, Records, decode_records};
29
30/// Group membership and primary generation, not proof of serving authority.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Authority {
33    /// Immutable group identity.
34    pub group_id: GroupId,
35    /// Nonzero membership generation.
36    pub config_epoch: u64,
37    /// Primary view; zero is the initial view.
38    pub view: u64,
39}
40
41/// Hard directional bounds, enforced before iterating or copying record bytes.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct DataLimits {
44    /// Maximum complete metadata and payload frame sizes.
45    pub envelope: EnvelopeLimits,
46    /// Maximum records in one data packet.
47    pub max_records: usize,
48    /// Maximum total payload parts, including empty parts.
49    pub max_parts: usize,
50    /// Maximum payload bytes in one record, summed across all parts.
51    pub max_record_bytes: usize,
52}
53
54impl DataLimits {
55    /// Select the smaller bound in each field for a directional data transfer.
56    #[must_use]
57    pub fn intersection(self, other: Self) -> Self {
58        Self {
59            envelope: EnvelopeLimits {
60                max_metadata_bytes: self
61                    .envelope
62                    .max_metadata_bytes
63                    .min(other.envelope.max_metadata_bytes),
64                max_payload_bytes: self
65                    .envelope
66                    .max_payload_bytes
67                    .min(other.envelope.max_payload_bytes),
68            },
69            max_records: self.max_records.min(other.max_records),
70            max_parts: self.max_parts.min(other.max_parts),
71            max_record_bytes: self.max_record_bytes.min(other.max_record_bytes),
72        }
73    }
74}
75
76#[cfg(test)]
77mod record_limit_tests {
78    use super::*;
79
80    #[test]
81    fn record_ceiling_is_independent_of_packet_capacity() {
82        let limits = DataLimits {
83            envelope: EnvelopeLimits {
84                max_payload_bytes: 4 * 1024 * 1024,
85                ..EnvelopeLimits::default()
86            },
87            ..DataLimits::default()
88        };
89        let payload = vec![7; 600 * 1024];
90        let parts = [payload.as_slice()];
91        let records = [Record {
92            encoding: crate::data::Encoding::Raw,
93            message_id: MessageId::new(),
94            parts: &parts,
95        }; 2];
96        assert!(records_size(&records, 0, limits).is_ok());
97        let large_parts = [payload.as_slice(), payload.as_slice()];
98        let large = [Record {
99            encoding: crate::data::Encoding::Raw,
100            message_id: MessageId::new(),
101            parts: &large_parts,
102        }];
103        assert!(matches!(
104            records_size(&large, 0, limits),
105            Err(CodecError::Limit)
106        ));
107        let mut metadata = Vec::new();
108        let mut wire_payload = Vec::new();
109        encode_records(&large, &mut metadata, Some(&mut wire_payload));
110        assert!(matches!(
111            decode_records(&metadata, &wire_payload, 0, limits),
112            Err(CodecError::Limit)
113        ));
114    }
115}
116
117impl Default for DataLimits {
118    fn default() -> Self {
119        Self {
120            envelope: EnvelopeLimits {
121                max_payload_bytes: 16 * 1024 * 1024,
122                ..EnvelopeLimits::default()
123            },
124            max_records: 1000,
125            max_parts: 4000,
126            max_record_bytes: 1024 * 1024,
127        }
128    }
129}
130
131/// Borrowed application record supplied to the encoder.
132#[derive(Debug, Clone, Copy)]
133pub struct Record<'a> {
134    /// Payload representation; brokers preserve it without decoding.
135    pub encoding: Encoding,
136    /// Stable record identity; retries retain it along with multipart bytes.
137    pub message_id: MessageId,
138    /// Ordered parts. At least one part is required; a part may be empty.
139    pub parts: &'a [&'a [u8]],
140}
141
142pub(super) fn records_size(
143    records: &[Record<'_>],
144    first: u64,
145    limits: DataLimits,
146) -> Result<(usize, usize), CodecError> {
147    record_sizes(
148        records
149            .iter()
150            .map(|r| (r.message_id, r.encoding, r.parts.iter().copied())),
151        first,
152        limits,
153    )
154}
155
156pub(super) fn record_sizes<'a, I, P>(
157    records: I,
158    first: u64,
159    limits: DataLimits,
160) -> Result<(usize, usize), CodecError>
161where
162    I: ExactSizeIterator<Item = (MessageId, Encoding, P)>,
163    P: Iterator<Item = &'a [u8]>,
164{
165    record_count(records.len(), first, limits)?;
166    let mut metadata = 4_usize;
167    let mut payload = 0_usize;
168    let mut parts = 0_usize;
169    for (_, encoding, record) in records {
170        metadata = add(metadata, encoding.head_bytes())?;
171        let mut record_parts = 0_usize;
172        let mut record_bytes = 0_usize;
173        for part in record {
174            if encoding
175                .frame_offset(part, limits.max_parts, limits.max_record_bytes)
176                .is_none()
177            {
178                return Err(CodecError::Length);
179            }
180            parts = add(parts, 1)?;
181            record_parts = add(record_parts, 1)?;
182            count(parts)?;
183            if parts > limits.max_parts {
184                return Err(CodecError::Limit);
185            }
186            metadata = add(metadata, 4)?;
187            count(part.len())?;
188            payload = add(payload, part.len())?;
189            record_bytes = add(record_bytes, part.len())?;
190            if record_bytes > limits.max_record_bytes {
191                return Err(CodecError::Limit);
192            }
193        }
194        if !encoding.validate(record_parts, limits.max_parts, limits.max_record_bytes) {
195            return Err(CodecError::Parts);
196        }
197    }
198    Ok((metadata, payload))
199}
200
201/// Caller validated sizes and reserved both complete output frames.
202pub(super) fn encode_records(
203    records: &[Record<'_>],
204    metadata: &mut Vec<u8>,
205    payload: Option<&mut Vec<u8>>,
206) {
207    write_record_frames(
208        records
209            .iter()
210            .map(|r| (r.message_id, r.encoding, r.parts.iter().copied())),
211        metadata,
212        payload,
213    );
214}
215
216/// Same descriptor writer for borrowed slices and owned-runtime iterators.
217pub(super) fn write_records<'a, I, P>(records: I, metadata: &mut Vec<u8>, payload: &mut Vec<u8>)
218where
219    I: ExactSizeIterator<Item = (MessageId, Encoding, P)>,
220    P: Iterator<Item = &'a [u8]>,
221{
222    write_record_frames(records, metadata, Some(payload));
223}
224
225fn write_record_frames<'a, I, P>(records: I, metadata: &mut Vec<u8>, payload: Option<&mut Vec<u8>>)
226where
227    I: ExactSizeIterator<Item = (MessageId, Encoding, P)>,
228    P: Iterator<Item = &'a [u8]>,
229{
230    metadata.extend_from_slice(&(records.len() as u32).to_be_bytes());
231    write_record_entries(records, metadata, payload);
232}
233
234/// Append validated entries without a new collection count.
235#[expect(
236    clippy::arithmetic_side_effects,
237    reason = "callers validated each part count against max_parts, a u32"
238)]
239pub(super) fn write_record_entries<'a, I, P>(
240    records: I,
241    metadata: &mut Vec<u8>,
242    mut payload: Option<&mut Vec<u8>>,
243) where
244    I: Iterator<Item = (MessageId, Encoding, P)>,
245    P: Iterator<Item = &'a [u8]>,
246{
247    for (id, encoding, parts) in records {
248        metadata.extend_from_slice(id.as_bytes());
249        let count_at = metadata.len();
250        metadata.extend_from_slice(&[0; 4]);
251        if let Encoding::Lz4 { decoded_bytes } = encoding {
252            metadata.extend_from_slice(&decoded_bytes.to_be_bytes());
253        }
254        let mut count = 0_u32;
255        for part in parts {
256            count += 1;
257            metadata.extend_from_slice(&(part.len() as u32).to_be_bytes());
258            if let Some(payload) = payload.as_mut() {
259                payload.extend_from_slice(part);
260            }
261        }
262        // The placeholder above is in bounds, so this cannot fail.
263        let _ = crate::layout::patch(metadata, count_at, (count | encoding.tag()).to_be_bytes());
264    }
265}
266
267pub(super) fn command(
268    envelope: Envelope,
269    opcode: Opcode,
270    response: bool,
271) -> Result<(), CodecError> {
272    if envelope.opcode != opcode || envelope.response != response || envelope.request_id.is_none() {
273        return Err(CodecError::Command);
274    }
275    Ok(())
276}
277
278pub(super) fn record_count(
279    records: usize,
280    first: u64,
281    limits: DataLimits,
282) -> Result<(), CodecError> {
283    if records == 0 || records > limits.max_records {
284        return Err(CodecError::Limit);
285    }
286    first
287        .checked_add(u64::from(count(records)?))
288        .ok_or(CodecError::Length)?;
289    Ok(())
290}
291
292pub(super) fn add(left: usize, right: usize) -> Result<usize, CodecError> {
293    left.checked_add(right).ok_or(CodecError::Length)
294}
295/// Checked total of encoded field sizes.
296pub(super) fn sum(terms: &[usize]) -> Result<usize, CodecError> {
297    terms.iter().try_fold(0, |total, &term| add(total, term))
298}
299pub(super) fn count(value: usize) -> Result<u32, CodecError> {
300    u32::try_from(value).map_err(|_| CodecError::Length)
301}
302pub(super) fn capacity(buffer: &Vec<u8>, length: usize) -> Result<(), CodecError> {
303    if buffer.capacity() < length {
304        return Err(CodecError::Capacity);
305    }
306    Ok(())
307}
308
309/// Structural rejection. No error establishes whether an earlier retry committed.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
311pub enum CodecError {
312    /// Envelope, bounds, correlation, or link-session shape is invalid.
313    #[error(transparent)]
314    Envelope(#[from] EnvelopeError),
315    /// Wrong opcode, direction, or correlation shape.
316    #[error("invalid data command envelope")]
317    Command,
318    /// Zero identity or required fencing epoch.
319    #[error("invalid data identity or epoch")]
320    Identity,
321    /// Unknown policy byte.
322    #[error("unknown completion policy")]
323    Policy,
324    /// Empty batch or exceeded record/part bounds.
325    #[error("data command exceeds record or part limits")]
326    Limit,
327    /// A record must contain at least one part, which may itself be empty.
328    #[error("record has no payload parts")]
329    Parts,
330    /// Truncated/trailing bytes, inconsistent packed lengths, or arithmetic overflow.
331    #[error("invalid data command length or sequence range")]
332    Length,
333    /// Caller must reserve both output arenas before encoding.
334    #[error("data output buffer capacity exhausted")]
335    Capacity,
336    /// Invalid confirmation position or progress beyond the received prefix.
337    #[error("invalid data confirmation position")]
338    Position,
339    /// Unsupported reader mode, delivery requirement, or progress evidence.
340    #[error("unsupported reader profile")]
341    Profile,
342}
343
344impl From<LayoutError> for CodecError {
345    fn from(_: LayoutError) -> Self {
346        Self::Length
347    }
348}
349
350fn descriptor(cursor: &mut Reader<'_>) -> Result<(usize, Encoding), CodecError> {
351    let tagged = cursor.u32()?;
352    let encoding = match tagged >> 24 {
353        0 => Encoding::Raw,
354        1 => Encoding::Lz4 {
355            decoded_bytes: cursor.u32()?,
356        },
357        _ => return Err(CodecError::Profile),
358    };
359    Ok(((tagged & 0x00ff_ffff) as usize, encoding))
360}