ozzy-proto 0.7.0

Sans-I/O application protocol and message types for Ozzy.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
//! Incremental canonical-reader output, independent of stored batch boundaries.

use super::{
    CodecError, DataLimits, ENVELOPE_BYTES, Envelope, PublicationHeader, RecordHeader, data,
};

use crate::layout;

mod shared;
pub use shared::PreparedBlock;

/// Encode available records into one bounded reader message. This controls the
/// application record table, not OMQ transport framing or write coalescing.
#[derive(Debug)]
pub struct RecordsEncoder<'a> {
    source: super::Source,
    first_offset: u64,
    envelope: Envelope,
    metadata: &'a mut Vec<u8>,
    payload: &'a mut Vec<u8>,
    limits: DataLimits,
    records: usize,
    parts: usize,
    decoded_payload_bytes: usize,
    encoding_at: usize,
    count_at: usize,
    shared_enabled: bool,
    shared: Option<shared::Payload>,
}

impl<'a> RecordsEncoder<'a> {
    /// Begin a response using caller-reserved storage. No allocation growth.
    pub fn new(
        envelope: Envelope,
        header: RecordHeader,
        metadata: &'a mut Vec<u8>,
        payload: &'a mut Vec<u8>,
        limits: DataLimits,
    ) -> Result<Self, CodecError> {
        super::command(envelope, super::Opcode::Records)?;
        header.subscription.validate()?;
        header.source.validate()?;
        let count_at = data::add(45, header.source.size())?;
        let header_bytes = data::add(count_at, 4)?;
        envelope.validate_frames(header_bytes, 0, limits.envelope)?;
        data::capacity(metadata, header_bytes)?;
        metadata.clear();
        header.subscription.encode(metadata);
        Ok(Self::begin(
            envelope,
            header.source,
            header.first_offset,
            (metadata, payload),
            limits,
        ))
    }

    /// Begin one message shared by every live reader of a group partition. It
    /// names no link, request, or subscription; receivers fence it by source.
    pub fn publication(
        envelope: Envelope,
        header: PublicationHeader,
        metadata: &'a mut Vec<u8>,
        payload: &'a mut Vec<u8>,
        limits: DataLimits,
    ) -> Result<Self, CodecError> {
        super::command(envelope, super::Opcode::RecordsPub)?;
        super::publication_topic(header.source)?;
        let count_at = data::add(13, header.source.size())?;
        let header_bytes = data::add(count_at, 4)?;
        envelope.validate_frames(header_bytes, 0, limits.envelope)?;
        data::capacity(metadata, header_bytes)?;
        metadata.clear();
        Ok(Self::begin(
            envelope,
            header.source,
            header.first_offset,
            (metadata, payload),
            limits,
        ))
    }

    /// Append the fields common to both messages after any subscription identity.
    fn begin(
        envelope: Envelope,
        source: super::Source,
        first_offset: u64,
        (metadata, payload): (&'a mut Vec<u8>, &'a mut Vec<u8>),
        limits: DataLimits,
    ) -> Self {
        payload.clear();
        source.encode(metadata);
        metadata.extend_from_slice(&first_offset.to_be_bytes());
        let encoding_at = metadata.len();
        metadata.push(crate::append::PayloadEncoding::Raw as u8);
        metadata.extend_from_slice(&[0; 4]);
        let count_at = metadata.len();
        metadata.extend_from_slice(&[0; 4]);
        Self {
            source,
            first_offset,
            envelope,
            metadata,
            payload,
            limits,
            records: 0,
            parts: 0,
            decoded_payload_bytes: 0,
            encoding_at,
            count_at,
            shared_enabled: false,
            shared: None,
        }
    }

    /// The log whose records this message carries.
    pub const fn source(&self) -> super::Source {
        self.source
    }

    /// The bounds this message was started with.
    pub const fn limits(&self) -> DataLimits {
        self.limits
    }

    /// Remaining record, part and payload capacity for a storage read.
    pub fn remaining(&self) -> DataLimits {
        let encoded = self.has_encoded_payload();
        DataLimits {
            max_record_bytes: self.limits.max_record_bytes,
            max_records: if encoded {
                0
            } else {
                self.limits.max_records.saturating_sub(self.records)
            },
            max_parts: if encoded {
                0
            } else {
                self.limits.max_parts.saturating_sub(self.parts)
            },
            envelope: super::EnvelopeLimits {
                max_metadata_bytes: self
                    .limits
                    .envelope
                    .max_metadata_bytes
                    .saturating_sub(self.metadata.len()),
                max_payload_bytes: if encoded {
                    0
                } else {
                    self.limits
                        .envelope
                        .max_payload_bytes
                        .saturating_sub(self.payload_bytes())
                },
            },
        }
    }

    /// Number of records already encoded.
    pub const fn len(&self) -> usize {
        self.records
    }

    /// Whether a nonempty response is available.
    pub const fn is_empty(&self) -> bool {
        self.records == 0
    }

    /// Decoded record bytes charged against local output limits.
    pub const fn decoded_payload_bytes(&self) -> usize {
        self.decoded_payload_bytes
    }

    /// Append one raw, single-part record. Check all bounds before copying its
    /// descriptor and payload; failure preserves the previous response.
    pub fn push_raw(&mut self, id: crate::MessageId, payload: &[u8]) -> Result<(), CodecError> {
        if self.has_encoded_payload() {
            return Err(CodecError::Limit);
        }
        self.materialize()?;
        let records = data::add(self.records, 1)?;
        data::record_count(records, self.first_offset, self.limits)?;
        let parts = data::add(self.parts, 1)?;
        if parts > self.limits.max_parts || payload.len() > self.limits.max_record_bytes {
            return Err(CodecError::Limit);
        }
        let length = data::count(payload.len())?;
        frame_capacity(
            self.metadata,
            data::add(self.metadata.len(), 24)?,
            self.limits.envelope.max_metadata_bytes,
        )?;
        frame_capacity(
            self.payload,
            data::add(self.payload.len(), payload.len())?,
            self.limits.envelope.max_payload_bytes,
        )?;
        let mut descriptor = [0; 24];
        descriptor[..16].copy_from_slice(id.as_bytes());
        descriptor[19] = 1;
        descriptor[20..].copy_from_slice(&length.to_be_bytes());
        self.metadata.extend_from_slice(&descriptor);
        self.payload.extend_from_slice(payload);
        self.records = records;
        self.parts = parts;
        layout::patch(self.metadata, self.count_at, (records as u32).to_be_bytes())?;
        self.set_raw_payload_bytes()?;
        Ok(())
    }

    /// Append an encoded descriptor table (without a collection count) and its
    /// exact payload. Validate every descriptor and all destination limits before
    /// copying either span. Failure leaves output and capacity unchanged.
    pub fn extend_packed(
        &mut self,
        descriptors: &[u8],
        payload: &[u8],
        count: usize,
    ) -> Result<(), CodecError> {
        if self.has_encoded_payload() {
            return Err(CodecError::Limit);
        }
        self.materialize()?;
        let first = self
            .first_offset
            .checked_add(self.records as u64)
            .ok_or(CodecError::Length)?;
        let records = data::add(self.records, count)?;
        data::count(records)?;
        let size = data::add(self.metadata.len(), descriptors.len())?;
        let bytes = data::add(self.payload.len(), payload.len())?;
        self.envelope
            .validate_frames(size, bytes, self.limits.envelope)?;
        data::capacity(self.metadata, size)?;
        data::capacity(self.payload, bytes)?;
        let parts =
            data::validate_record_entries(descriptors, payload, count, first, self.remaining())?.0;
        let parts = data::add(self.parts, parts)?;
        self.metadata.extend_from_slice(descriptors);
        self.payload.extend_from_slice(payload);
        self.records = records;
        self.parts = parts;
        layout::patch(self.metadata, self.count_at, (records as u32).to_be_bytes())?;
        self.set_raw_payload_bytes()?;
        Ok(())
    }

    /// Validate and append a chunk in one pass, without allocation growth.
    /// On failure, this chunk leaves the output unchanged.
    pub fn extend<'b, I, P>(&mut self, records: I) -> Result<(), CodecError>
    where
        I: ExactSizeIterator<Item = (crate::MessageId, data::Encoding, P)>,
        P: Iterator<Item = &'b [u8]>,
    {
        if self.has_encoded_payload() {
            return Err(CodecError::Limit);
        }
        self.materialize()?;
        let remaining = self.remaining();
        let first = self
            .first_offset
            .checked_add(self.records as u64)
            .ok_or(CodecError::Length)?;
        let count = records.len();
        data::record_count(count, first, remaining)?;
        let total = data::add(self.records, count)?;
        data::count(total)?;
        let before = (self.metadata.len(), self.payload.len());
        let written = self
            .write_entries(records)
            .and_then(|parts| data::add(self.parts, parts));
        let parts = match written {
            Ok(parts) => parts,
            Err(error) => {
                self.metadata.truncate(before.0);
                self.payload.truncate(before.1);
                return Err(error);
            }
        };
        self.records = total;
        self.parts = parts;
        layout::patch(self.metadata, self.count_at, (total as u32).to_be_bytes())?;
        self.set_raw_payload_bytes()?;
        Ok(())
    }

    fn write_entries<'b, P>(
        &mut self,
        records: impl Iterator<Item = (crate::MessageId, data::Encoding, P)>,
    ) -> Result<usize, CodecError>
    where
        P: Iterator<Item = &'b [u8]>,
    {
        let mut total_parts = 0;
        for (id, encoding, parts) in records {
            let count_at = self.begin_entry(id, encoding)?;
            let mut record_bytes = 0_usize;
            let mut count = 0_u32;
            for part in parts {
                if encoding
                    .frame_offset(part, self.limits.max_parts, self.limits.max_record_bytes)
                    .is_none()
                {
                    return Err(CodecError::Length);
                }
                total_parts = data::add(total_parts, 1)?;
                data::count(total_parts)?;
                if data::add(self.parts, total_parts)? > self.limits.max_parts {
                    return Err(CodecError::Limit);
                }
                self.push_part(part, &mut record_bytes)?;
                count = count.checked_add(1).ok_or(CodecError::Length)?;
            }
            if !encoding.validate(
                count as usize,
                self.limits.max_parts,
                self.limits.max_record_bytes,
            ) {
                return Err(CodecError::Parts);
            }
            layout::patch(
                self.metadata,
                count_at,
                (count | encoding.tag()).to_be_bytes(),
            )?;
        }
        Ok(total_parts)
    }

    /// Write one descriptor head within the frame limit. Returns where its
    /// part count goes once the parts are known.
    fn begin_entry(
        &mut self,
        id: crate::MessageId,
        encoding: data::Encoding,
    ) -> Result<usize, CodecError> {
        let header_end = data::add(self.metadata.len(), encoding.head_bytes())?;
        frame_capacity(
            self.metadata,
            header_end,
            self.limits.envelope.max_metadata_bytes,
        )?;
        self.metadata.extend_from_slice(id.as_bytes());
        let count_at = self.metadata.len();
        self.metadata.extend_from_slice(&[0; 4]);
        if let data::Encoding::Lz4 { decoded_bytes } = encoding {
            self.metadata
                .extend_from_slice(&decoded_bytes.to_be_bytes());
        }
        Ok(count_at)
    }

    /// Append one part's length and bytes, charging `record_bytes` against
    /// the record limit and both frames against theirs.
    fn push_part(&mut self, part: &[u8], record_bytes: &mut usize) -> Result<(), CodecError> {
        let part_bytes = data::count(part.len())?;
        let metadata_end = data::add(self.metadata.len(), 4)?;
        let payload_end = data::add(self.payload.len(), part.len())?;
        *record_bytes = data::add(*record_bytes, part.len())?;
        if *record_bytes > self.limits.max_record_bytes {
            return Err(CodecError::Limit);
        }
        frame_capacity(
            self.metadata,
            metadata_end,
            self.limits.envelope.max_metadata_bytes,
        )?;
        frame_capacity(
            self.payload,
            payload_end,
            self.limits.envelope.max_payload_bytes,
        )?;
        self.metadata.extend_from_slice(&part_bytes.to_be_bytes());
        self.payload.extend_from_slice(part);
        Ok(())
    }

    /// Adopt a complete packed batch into an empty response. Validate all bounds
    /// before swapping equal-capacity payload allocations; no payload copy or
    /// allocation growth. The emptied batch receives the previous output buffer.
    pub fn take_buffer(&mut self, buffer: &mut data::RecordBuffer) -> Result<(), CodecError> {
        if !self.is_empty() || buffer.is_empty() {
            return Err(CodecError::Limit);
        }
        // RecordBuffer's private table was validated on insertion. Only the
        // destination's aggregate limits and offset range need checking here.
        data::record_count(buffer.count, self.first_offset, self.limits)?;
        if buffer.parts > self.limits.max_parts {
            return Err(CodecError::Limit);
        }
        let payload = buffer.payload.len();
        let size = data::add(self.metadata.len(), buffer.metadata.len())?;
        self.envelope
            .validate_frames(size, payload, self.limits.envelope)?;
        data::capacity(self.metadata, size)?;
        // Swapping must not grow the transport slot's backing-memory bound.
        if buffer.payload.capacity() != self.payload.capacity() {
            return Err(CodecError::Capacity);
        }
        self.metadata.extend_from_slice(&buffer.metadata);
        std::mem::swap(self.payload, &mut buffer.payload);
        self.records = buffer.count;
        self.parts = buffer.parts;
        layout::patch(
            self.metadata,
            self.count_at,
            (self.records as u32).to_be_bytes(),
        )?;
        self.set_raw_payload_bytes()?;
        buffer.clear();
        Ok(())
    }

    /// Finish a nonempty response and return its envelope.
    pub fn finish(self) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
        if self.shared.is_some() {
            return Err(CodecError::Parts);
        }
        self.finish_with_payload().map(|(header, _)| header)
    }

    /// Finish and return any shared payload owner. Otherwise the caller's
    /// original payload buffer holds the encoded bytes.
    pub fn finish_with_payload(
        self,
    ) -> Result<([u8; ENVELOPE_BYTES], Option<bytes::Bytes>), CodecError> {
        if self.is_empty() {
            return Err(CodecError::Limit);
        }
        let header = self.envelope.encode_header(
            self.metadata.len(),
            self.payload_bytes(),
            self.limits.envelope,
        )?;
        Ok((header, self.shared.map(shared::Payload::finish)))
    }

    fn set_raw_payload_bytes(&mut self) -> Result<(), CodecError> {
        let payload_bytes = self.payload_bytes();
        self.set_payload_encoding(crate::append::PayloadEncoding::Raw, payload_bytes)?;
        self.decoded_payload_bytes = payload_bytes;
        Ok(())
    }

    fn set_payload_encoding(
        &mut self,
        encoding: crate::append::PayloadEncoding,
        decoded_bytes: usize,
    ) -> Result<(), CodecError> {
        let [a, b, c, d] = (decoded_bytes as u32).to_be_bytes();
        layout::patch(
            self.metadata,
            self.encoding_at,
            [encoding as u8, a, b, c, d],
        )?;
        Ok(())
    }

    fn has_encoded_payload(&self) -> bool {
        self.metadata
            .get(self.encoding_at)
            .is_some_and(|&tag| tag != crate::append::PayloadEncoding::Raw as u8)
    }
}

/// Check before each write so even a late error cannot grow either allocation.
fn frame_capacity(buffer: &Vec<u8>, end: usize, limit: usize) -> Result<(), CodecError> {
    if end > limit {
        return Err(crate::EnvelopeError::Limit.into());
    }
    u32::try_from(end).map_err(|_| crate::EnvelopeError::Length)?;
    data::capacity(buffer, end)
}