Skip to main content

active_infinity/
stream.rs

1//! Streaming / Infinity mode: ordered frames that reassemble into exactly the
2//! canonical image.
3//!
4//! A stream is a sequence of self-delimiting frames:
5//!
6//! ```text
7//! Begin  { format version, flags, dialect }
8//! Batch  { section id, bytes }        (one or more per section, in order)
9//! End    { content hash, total size }
10//! ```
11//!
12//! followed, for incremental updates, by any number of
13//!
14//! ```text
15//! Delta  { operations }               (applied to the reassembled module)
16//! End    { new content hash }
17//! ```
18//!
19//! Reassembly is byte-exact: a stream produced from a module yields the same
20//! bytes and the same Blake3 content hash as [`crate::encode::encode`] of that module,
21//! because each batch carries a slice of the canonical section image. Deltas are
22//! semantic (root, node content, bindings, annotations, provenance) and are
23//! re-canonicalized after application, so an incremental update also lands on
24//! the identical whole-module encoding.
25//!
26//! Every frame carries its own length and a truncated Blake3 checksum, so
27//! corruption is detected at the frame boundary instead of surfacing as a
28//! confusing parse error later.
29
30// Module-level: item docs for this layer live in docs/spec/layout.md and
31// docs/spec/streaming.md; the crate root keeps deny(missing_docs) for the
32// hand-written public surface.
33#![allow(missing_docs)]
34
35use std::collections::VecDeque;
36
37use crate::canonical::remap_node;
38use crate::encode::{encode_with, hash_image};
39use crate::error::{Error, Result};
40use crate::ir::{Annotation, AnnotationKind, InfinityModule, NodeId, Provenance};
41use crate::layout::{
42    align8, read_u32, read_u64, EncodeOpts, Section, HEADER_SIZE, SECTION_COUNT, STREAM_MAGIC,
43};
44use crate::validate::Validated;
45
46/// Stream frame magic is `AINS`; version 1.
47pub const STREAM_VERSION: u8 = 1;
48/// Fixed frame header size.
49pub const FRAME_HEADER_SIZE: usize = 32;
50/// Default batch chunk size (bytes of section payload per frame).
51pub const DEFAULT_CHUNK: usize = 64 << 10;
52/// Largest frame payload this reader accepts.
53pub const MAX_FRAME_PAYLOAD: u32 = 64 << 20;
54
55/// Frame kinds.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum FrameKind {
58    /// Opens a stream and declares format metadata.
59    Begin,
60    /// Carries bytes of one section.
61    Batch,
62    /// Carries semantic delta operations.
63    Delta,
64    /// Closes a generation with the expected content hash.
65    End,
66}
67
68impl FrameKind {
69    pub const fn code(self) -> u8 {
70        match self {
71            FrameKind::Begin => 1,
72            FrameKind::Batch => 2,
73            FrameKind::Delta => 3,
74            FrameKind::End => 4,
75        }
76    }
77
78    pub const fn from_code(code: u8) -> Option<Self> {
79        match code {
80            1 => Some(FrameKind::Begin),
81            2 => Some(FrameKind::Batch),
82            3 => Some(FrameKind::Delta),
83            4 => Some(FrameKind::End),
84            _ => None,
85        }
86    }
87}
88
89/// One stream frame.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Frame {
92    pub kind: FrameKind,
93    pub sequence: u64,
94    pub payload: Vec<u8>,
95}
96
97impl Frame {
98    pub fn new(kind: FrameKind, sequence: u64, payload: Vec<u8>) -> Self {
99        Frame {
100            kind,
101            sequence,
102            payload,
103        }
104    }
105
106    /// Serialize the frame with its header and checksum.
107    pub fn to_bytes(&self) -> Vec<u8> {
108        let mut out = Vec::with_capacity(FRAME_HEADER_SIZE + self.payload.len());
109        out.extend_from_slice(&STREAM_MAGIC);
110        out.push(STREAM_VERSION);
111        out.push(self.kind.code());
112        out.extend_from_slice(&0u16.to_le_bytes());
113        out.extend_from_slice(&self.sequence.to_le_bytes());
114        out.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
115        out.extend_from_slice(&0u32.to_le_bytes());
116        let checksum = frame_checksum(&self.payload);
117        out.extend_from_slice(&checksum[..8]);
118        out.extend_from_slice(&self.payload);
119        out
120    }
121
122    /// Parse one frame, returning it and the number of bytes consumed.
123    pub fn parse(buf: &[u8]) -> Result<(Frame, usize)> {
124        if buf.len() < 4 || buf[0..4] != STREAM_MAGIC {
125            return Err(Error::StreamMagic);
126        }
127        if buf.len() < FRAME_HEADER_SIZE {
128            return Err(Error::Truncated {
129                needed: FRAME_HEADER_SIZE as u64,
130                found: buf.len() as u64,
131            });
132        }
133        let version = buf[4];
134        if version != STREAM_VERSION {
135            return Err(Error::StreamVersion(version));
136        }
137        let kind = FrameKind::from_code(buf[5]).ok_or(Error::StreamFrameKind(buf[5]))?;
138        let flags = u16::from_le_bytes([buf[6], buf[7]]);
139        if flags != 0 {
140            return Err(Error::UnknownFlags(flags));
141        }
142        let sequence = u64::from_le_bytes(buf[8..16].try_into().expect("8 bytes"));
143        let len = u32::from_le_bytes(buf[16..20].try_into().expect("4 bytes"));
144        if len > MAX_FRAME_PAYLOAD {
145            return Err(Error::StreamFrameLen {
146                declared: len,
147                available: buf.len() as u64,
148            });
149        }
150        let reserved = u32::from_le_bytes(buf[20..24].try_into().expect("4 bytes"));
151        if reserved != 0 {
152            return Err(Error::ReservedNonZero {
153                section: "stream-frame",
154                offset: 20,
155            });
156        }
157        let payload_start = FRAME_HEADER_SIZE;
158        let payload_end = payload_start + len as usize;
159        if payload_end > buf.len() {
160            return Err(Error::StreamFrameLen {
161                declared: len,
162                available: (buf.len() - payload_start) as u64,
163            });
164        }
165        let payload = &buf[payload_start..payload_end];
166        let expected = frame_checksum(payload);
167        if buf[24..32] != expected[..8] {
168            return Err(Error::HashMismatch);
169        }
170        Ok((
171            Frame {
172                kind,
173                sequence,
174                payload: payload.to_vec(),
175            },
176            payload_end,
177        ))
178    }
179}
180
181/// Truncated Blake3 checksum of a frame payload.
182pub fn frame_checksum(payload: &[u8]) -> [u8; 32] {
183    *blake3::hash(payload).as_bytes()
184}
185
186/// Encode a module into stream frames.
187///
188/// The frames carry the exact bytes of the module's canonical image, so
189/// reassembly reproduces `encode(module)` byte for byte and therefore the same
190/// content hash as the whole-module encoding.
191pub fn encode_stream(module: &InfinityModule, chunk: usize) -> Result<Vec<Frame>> {
192    encode_stream_with(module, chunk, EncodeOpts::default())
193}
194
195/// Encode a module into stream frames with explicit encoder options.
196pub fn encode_stream_with(
197    module: &InfinityModule,
198    chunk: usize,
199    opts: EncodeOpts,
200) -> Result<Vec<Frame>> {
201    if chunk == 0 {
202        return Err(Error::InvalidArgument("chunk size must be non-zero"));
203    }
204    let image = encode_with(module, opts)?;
205    let header = crate::layout::Header::parse(&image)?;
206    let mut frames = Vec::new();
207    let mut sequence = 0u64;
208    let mut begin = Vec::with_capacity(24);
209    begin.extend_from_slice(&header.major.to_le_bytes());
210    begin.extend_from_slice(&header.minor.to_le_bytes());
211    begin.extend_from_slice(&header.flags.to_le_bytes());
212    begin.extend_from_slice(&0u16.to_le_bytes());
213    begin.extend_from_slice(&header.dialect_hash.to_le_bytes());
214    begin.extend_from_slice(&header.total_size.to_le_bytes());
215    frames.push(Frame::new(FrameKind::Begin, sequence, begin));
216    sequence += 1;
217
218    // Split the image into section bodies in canonical order. The header is
219    // regenerated on reassembly, so only section content travels.
220    for section in Section::ALL {
221        let span = header.span(section);
222        let start = span.offset as usize;
223        let len = section_body_len(&image, &header, section);
224        let body = &image[start..start + len];
225        if body.is_empty() {
226            let mut payload = Vec::with_capacity(9);
227            payload.push(section as u8);
228            payload.extend_from_slice(&span.count.to_le_bytes());
229            payload.extend_from_slice(&0u32.to_le_bytes());
230            frames.push(Frame::new(FrameKind::Batch, sequence, payload));
231            sequence += 1;
232            continue;
233        }
234        for piece in body.chunks(chunk) {
235            let mut payload = Vec::with_capacity(9 + piece.len());
236            payload.push(section as u8);
237            payload.extend_from_slice(&span.count.to_le_bytes());
238            payload.extend_from_slice(&(piece.len() as u32).to_le_bytes());
239            payload.extend_from_slice(piece);
240            frames.push(Frame::new(FrameKind::Batch, sequence, payload));
241            sequence += 1;
242        }
243    }
244
245    let mut end = Vec::with_capacity(40);
246    let hash = hash_image(&image)?;
247    end.extend_from_slice(&hash);
248    end.extend_from_slice(&(image.len() as u64).to_le_bytes());
249    frames.push(Frame::new(FrameKind::End, sequence, end));
250    Ok(frames)
251}
252
253fn section_body_len(image: &[u8], header: &crate::layout::Header, section: Section) -> usize {
254    let span = header.span(section);
255    let start = span.offset as usize;
256    let end = match section {
257        Section::Strings => {
258            if span.count == 0 {
259                start
260            } else {
261                let heap = read_u32(image, start + span.count as usize * 4).unwrap_or(0) as usize;
262                start + (span.count as usize + 1) * 4 + heap
263            }
264        }
265        Section::Blobs => {
266            let mut heap = 0usize;
267            for index in 0..span.count {
268                let len = read_u32(image, start + index as usize * 40 + 36).unwrap_or(0) as usize;
269                heap += len;
270            }
271            start + span.count as usize * 40 + heap
272        }
273        Section::Provenance => start + span.count as usize,
274        Section::Secrets => {
275            let mut heap = 0usize;
276            for index in 0..span.count {
277                let len = read_u32(image, start + index as usize * 8 + 4).unwrap_or(0) as usize;
278                heap += len;
279            }
280            start + span.count as usize * 8 + heap
281        }
282        Section::Extensions => {
283            let mut end = start + span.count as usize * 16;
284            for index in 0..span.count {
285                let dir = start + index as usize * 16;
286                let offset = read_u32(image, dir + 8).unwrap_or(0) as usize;
287                let len = read_u32(image, dir + 12).unwrap_or(0) as usize;
288                end = end.max(offset + len);
289            }
290            end
291        }
292        Section::Nodes => start + span.count as usize * 32,
293        Section::Children => start + span.count as usize * 4,
294        Section::Fields => start + span.count as usize * 16,
295        Section::Parts => start + span.count as usize * 8,
296        Section::Edges => start + span.count as usize * 8,
297        Section::Annotations => start + span.count as usize * 16,
298        Section::Units => start + span.count as usize * 8,
299        Section::Types => start + span.count as usize * 16,
300        Section::Aux => start + span.count as usize * 8,
301        Section::Numbers => start + span.count as usize * 8,
302        Section::Hashes => start + span.count as usize * 32,
303        Section::Bindings => start + span.count as usize * 8,
304    };
305    end.saturating_sub(start)
306}
307
308/// A semantic delta operation.
309#[derive(Debug, Clone, PartialEq)]
310pub enum DeltaOp {
311    /// Replace the module root.
312    SetRoot(Option<NodeId>),
313    /// Replace the content of `target` with a fragment module's node 0.
314    PutNode {
315        target: NodeId,
316        fragment: Box<InfinityModule>,
317    },
318    /// Append a fragment module's node 0 and report the new id.
319    PushNode { fragment: Box<InfinityModule> },
320    /// Bind (or rebind) a name.
321    SetBinding { name: String, node: NodeId },
322    /// Remove a binding by name (no-op when absent).
323    RemoveBinding { name: String },
324    /// Append an annotation.
325    SetAnnotation { node: NodeId, kind: AnnotationKind },
326    /// Drop every annotation.
327    ClearAnnotations,
328    /// Replace provenance.
329    SetProvenance(Option<Provenance>),
330}
331
332impl DeltaOp {
333    fn encode(&self, out: &mut Vec<u8>) -> Result<()> {
334        match self {
335            DeltaOp::SetRoot(root) => {
336                out.push(0);
337                out.extend_from_slice(&root.map(|id| id.0).unwrap_or(u32::MAX).to_le_bytes());
338            }
339            DeltaOp::PutNode { target, fragment } => {
340                out.push(1);
341                out.extend_from_slice(&target.0.to_le_bytes());
342                let image = encode_with(fragment, EncodeOpts::default())?;
343                out.extend_from_slice(&(image.len() as u32).to_le_bytes());
344                out.extend_from_slice(&image);
345            }
346            DeltaOp::PushNode { fragment } => {
347                out.push(2);
348                let image = encode_with(fragment, EncodeOpts::default())?;
349                out.extend_from_slice(&(image.len() as u32).to_le_bytes());
350                out.extend_from_slice(&image);
351            }
352            DeltaOp::SetBinding { name, node } => {
353                out.push(3);
354                let bytes = name.as_bytes();
355                out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
356                out.extend_from_slice(bytes);
357                out.extend_from_slice(&node.0.to_le_bytes());
358            }
359            DeltaOp::RemoveBinding { name } => {
360                out.push(4);
361                let bytes = name.as_bytes();
362                out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
363                out.extend_from_slice(bytes);
364            }
365            DeltaOp::SetAnnotation { node, kind } => {
366                out.push(5);
367                out.extend_from_slice(&node.0.to_le_bytes());
368                match kind {
369                    AnnotationKind::Intent(text) => {
370                        out.push(0);
371                        write_string(out, text)?;
372                    }
373                    AnnotationKind::Constraint { source, expr } => {
374                        out.push(1);
375                        write_string(out, source)?;
376                        out.extend_from_slice(&expr.0.to_le_bytes());
377                    }
378                    AnnotationKind::AiImmutable(flag) => {
379                        out.push(2);
380                        out.push(u8::from(*flag));
381                    }
382                }
383            }
384            DeltaOp::ClearAnnotations => out.push(6),
385            DeltaOp::SetProvenance(prov) => {
386                out.push(7);
387                match prov {
388                    None => out.push(0),
389                    Some(prov) => {
390                        out.push(1);
391                        write_opt_string(out, prov.producer.as_deref())?;
392                        write_opt_string(out, prov.model.as_deref())?;
393                        write_opt_string(out, prov.tool.as_deref())?;
394                        out.extend_from_slice(&prov.timestamp_ms.to_le_bytes());
395                        match prov.parent {
396                            None => out.push(0),
397                            Some(hash) => {
398                                out.push(1);
399                                out.extend_from_slice(&hash);
400                            }
401                        }
402                        out.extend_from_slice(&(prov.entries.len() as u32).to_le_bytes());
403                        for entry in &prov.entries {
404                            write_string(out, &entry.key)?;
405                            match &entry.value {
406                                crate::ir::ProvValue::Text(text) => {
407                                    out.push(0);
408                                    write_string(out, text)?;
409                                }
410                                crate::ir::ProvValue::Int(value) => {
411                                    out.push(1);
412                                    out.extend_from_slice(&value.to_le_bytes());
413                                }
414                                crate::ir::ProvValue::Flag(flag) => {
415                                    out.push(2);
416                                    out.push(u8::from(*flag));
417                                }
418                                crate::ir::ProvValue::Hash(hash) => {
419                                    out.push(3);
420                                    out.extend_from_slice(hash);
421                                }
422                            }
423                        }
424                    }
425                }
426            }
427        }
428        Ok(())
429    }
430
431    fn decode(buf: &[u8], cursor: &mut usize) -> Result<DeltaOp> {
432        let tag = take_u8(buf, cursor)?;
433        let op = match tag {
434            0 => {
435                let id = take_u32(buf, cursor)?;
436                DeltaOp::SetRoot(if id == u32::MAX {
437                    None
438                } else {
439                    Some(NodeId(id))
440                })
441            }
442            1 => {
443                let target = NodeId(take_u32(buf, cursor)?);
444                let fragment = Box::new(decode_fragment(buf, cursor)?);
445                DeltaOp::PutNode { target, fragment }
446            }
447            2 => {
448                let fragment = Box::new(decode_fragment(buf, cursor)?);
449                DeltaOp::PushNode { fragment }
450            }
451            3 => {
452                let name = take_string(buf, cursor)?;
453                let node = NodeId(take_u32(buf, cursor)?);
454                DeltaOp::SetBinding { name, node }
455            }
456            4 => {
457                let name = take_string(buf, cursor)?;
458                DeltaOp::RemoveBinding { name }
459            }
460            5 => {
461                let node = NodeId(take_u32(buf, cursor)?);
462                let kind_tag = take_u8(buf, cursor)?;
463                let kind = match kind_tag {
464                    0 => AnnotationKind::Intent(take_string(buf, cursor)?),
465                    1 => {
466                        let source = take_string(buf, cursor)?;
467                        let expr = NodeId(take_u32(buf, cursor)?);
468                        AnnotationKind::Constraint { source, expr }
469                    }
470                    2 => AnnotationKind::AiImmutable(take_u8(buf, cursor)? != 0),
471                    _ => return Err(Error::StreamDelta("unknown annotation kind")),
472                };
473                DeltaOp::SetAnnotation { node, kind }
474            }
475            6 => DeltaOp::ClearAnnotations,
476            7 => {
477                if take_u8(buf, cursor)? == 0 {
478                    DeltaOp::SetProvenance(None)
479                } else {
480                    let producer = take_opt_string(buf, cursor)?;
481                    let model = take_opt_string(buf, cursor)?;
482                    let tool = take_opt_string(buf, cursor)?;
483                    let timestamp_ms = take_u64(buf, cursor)?;
484                    let parent = if take_u8(buf, cursor)? == 0 {
485                        None
486                    } else {
487                        Some(take_hash(buf, cursor)?)
488                    };
489                    let count = take_u32(buf, cursor)?;
490                    let mut entries = Vec::with_capacity(count.min(4096) as usize);
491                    for _ in 0..count {
492                        let key = take_string(buf, cursor)?;
493                        let value = match take_u8(buf, cursor)? {
494                            0 => crate::ir::ProvValue::Text(take_string(buf, cursor)?),
495                            1 => crate::ir::ProvValue::Int(take_i64(buf, cursor)?),
496                            2 => crate::ir::ProvValue::Flag(take_u8(buf, cursor)? != 0),
497                            3 => crate::ir::ProvValue::Hash(take_hash(buf, cursor)?),
498                            _ => return Err(Error::StreamDelta("unknown provenance value kind")),
499                        };
500                        entries.push(crate::ir::ProvEntry { key, value });
501                    }
502                    DeltaOp::SetProvenance(Some(Provenance {
503                        producer,
504                        timestamp_ms,
505                        model,
506                        tool,
507                        parent,
508                        entries,
509                    }))
510                }
511            }
512            _ => return Err(Error::StreamDelta("unknown delta operation")),
513        };
514        Ok(op)
515    }
516}
517
518/// Apply delta operations to a module, returning the updated module.
519pub fn apply_delta(module: &InfinityModule, ops: &[DeltaOp]) -> Result<InfinityModule> {
520    let mut out = module.clone();
521    for op in ops {
522        match op {
523            DeltaOp::SetRoot(root) => {
524                if let Some(root) = root {
525                    if out.get(*root).is_none() {
526                        return Err(Error::StreamDelta("root node does not exist"));
527                    }
528                }
529                out.set_root(*root);
530            }
531            DeltaOp::PutNode { target, fragment } => {
532                if out.get(*target).is_none() {
533                    return Err(Error::StreamDelta("target node does not exist"));
534                }
535                if fragment.node_count() == 0 {
536                    return Err(Error::StreamDelta("fragment is empty"));
537                }
538                let replacement = merge_fragment(&mut out, fragment)?;
539                let content = out
540                    .get(replacement)
541                    .cloned()
542                    .ok_or(Error::StreamDelta("fragment root missing"))?;
543                *out.node_mut(*target)? = content;
544            }
545            DeltaOp::PushNode { fragment } => {
546                if fragment.node_count() == 0 {
547                    return Err(Error::StreamDelta("fragment is empty"));
548                }
549                let _ = merge_fragment(&mut out, fragment)?;
550            }
551            DeltaOp::SetBinding { name, node } => {
552                if out.get(*node).is_none() {
553                    return Err(Error::StreamDelta("binding target does not exist"));
554                }
555                out.bind(name.clone(), *node);
556            }
557            DeltaOp::RemoveBinding { name } => {
558                out.remove_binding(name);
559            }
560            DeltaOp::SetAnnotation { node, kind } => {
561                if out.get(*node).is_none() {
562                    return Err(Error::StreamDelta("annotation subject does not exist"));
563                }
564                out.annotate(*node, kind.clone());
565            }
566            DeltaOp::ClearAnnotations => out.clear_annotations(),
567            DeltaOp::SetProvenance(prov) => out.set_provenance(prov.clone()),
568        }
569    }
570    Ok(out)
571}
572
573fn merge_fragment(base: &mut InfinityModule, fragment: &InfinityModule) -> Result<NodeId> {
574    let base_count = base.node_count();
575    let map: Vec<NodeId> = (0..fragment.node_count())
576        .map(|index| NodeId(base_count as u32 + index as u32))
577        .collect();
578    for node in fragment.nodes() {
579        base.push(remap_node(node, &map)?);
580    }
581    Ok(map[fragment.root().unwrap_or(NodeId(0)).index()])
582}
583
584/// A reassembled stream generation.
585#[derive(Debug, Clone, PartialEq)]
586pub struct StreamedModule {
587    pub module: InfinityModule,
588    pub image: Vec<u8>,
589    pub content_hash: [u8; 32],
590}
591
592/// Streaming decoder state.
593#[derive(Debug, Default)]
594pub struct StreamDecoder {
595    sections: [Vec<u8>; SECTION_COUNT],
596    counts: [u32; SECTION_COUNT],
597    seen: [bool; SECTION_COUNT],
598    begun: bool,
599    ended: bool,
600    expected: u64,
601    flags: u16,
602    total_size: Option<u64>,
603    module: Option<InfinityModule>,
604    image: Option<Vec<u8>>,
605    pending_deltas: Vec<DeltaOp>,
606    frames: u64,
607    partial: Vec<u8>,
608    cursor: usize,
609}
610
611impl StreamDecoder {
612    pub fn new() -> Self {
613        Self {
614            sections: Default::default(),
615            counts: [0; SECTION_COUNT],
616            seen: [false; SECTION_COUNT],
617            begun: false,
618            ended: false,
619            expected: 0,
620            flags: 0,
621            total_size: None,
622            module: None,
623            image: None,
624            pending_deltas: Vec::new(),
625            frames: 0,
626            partial: Vec::new(),
627            cursor: 0,
628        }
629    }
630
631    /// Feed raw frame bytes. Frames may be split across calls and several may
632    /// arrive in one call; partial frames are buffered.
633    pub fn push_bytes(&mut self, buf: &[u8]) -> Result<()> {
634        self.partial.extend_from_slice(buf);
635        loop {
636            if self.partial.len() < FRAME_HEADER_SIZE {
637                return Ok(());
638            }
639            match Frame::parse(&self.partial) {
640                Ok((frame, consumed)) => {
641                    self.push(&frame)?;
642                    self.partial.drain(..consumed);
643                }
644                // A short buffer is not an error: wait for the rest of the
645                // frame. A declared length above the frame ceiling is a real
646                // error and `Frame::parse` reports it before this match.
647                Err(Error::Truncated { .. }) => return Ok(()),
648                Err(Error::StreamFrameLen { declared, .. }) if declared <= MAX_FRAME_PAYLOAD => {
649                    return Ok(())
650                }
651                Err(error) => return Err(error),
652            }
653        }
654    }
655
656    /// Feed one parsed frame.
657    pub fn push(&mut self, frame: &Frame) -> Result<()> {
658        if frame.sequence != self.expected {
659            return Err(Error::StreamSequence {
660                expected: self.expected,
661                found: frame.sequence,
662            });
663        }
664        self.expected += 1;
665        self.frames += 1;
666        match frame.kind {
667            FrameKind::Begin => {
668                if self.begun {
669                    return Err(Error::StreamState("duplicate begin frame"));
670                }
671                if self.ended {
672                    return Err(Error::StreamState("begin after end"));
673                }
674                if frame.payload.len() != 24 {
675                    return Err(Error::StreamFrameLen {
676                        declared: 24,
677                        available: frame.payload.len() as u64,
678                    });
679                }
680                let major = u16::from_le_bytes([frame.payload[0], frame.payload[1]]);
681                let minor = u16::from_le_bytes([frame.payload[2], frame.payload[3]]);
682                if major != crate::layout::FORMAT_MAJOR || minor > crate::layout::FORMAT_MINOR {
683                    return Err(Error::VersionMajor(major));
684                }
685                let flags = u16::from_le_bytes([frame.payload[4], frame.payload[5]]);
686                if flags & !crate::layout::FLAG_KNOWN_MASK != 0 {
687                    return Err(Error::UnknownFlags(flags & !crate::layout::FLAG_KNOWN_MASK));
688                }
689                if frame.payload[6..8] != [0, 0] {
690                    return Err(Error::ReservedNonZero {
691                        section: "stream-begin",
692                        offset: 6,
693                    });
694                }
695                let dialect = read_u64(&frame.payload, 8)?;
696                if dialect != crate::layout::dialect_hash() {
697                    return Err(Error::DialectMismatch(dialect));
698                }
699                let size = read_u64(&frame.payload, 16)?;
700                if size > crate::layout::Limits::default().max_total_bytes {
701                    return Err(Error::Limit {
702                        kind: "total bytes",
703                        limit: crate::layout::Limits::default().max_total_bytes,
704                    });
705                }
706                self.total_size = Some(size);
707                self.flags = flags;
708                self.begun = true;
709            }
710            FrameKind::Batch => {
711                if !self.begun || self.ended {
712                    return Err(Error::StreamState("batch outside a generation"));
713                }
714                if frame.payload.len() < 9 {
715                    return Err(Error::StreamFrameLen {
716                        declared: 9,
717                        available: frame.payload.len() as u64,
718                    });
719                }
720                let section = frame.payload[0] as usize;
721                if section >= SECTION_COUNT {
722                    return Err(Error::StreamState("unknown section id"));
723                }
724                let count = read_u32(&frame.payload, 1)?;
725                let len = read_u32(&frame.payload, 5)? as usize;
726                if frame.payload.len() != 9 + len {
727                    return Err(Error::StreamFrameLen {
728                        declared: len as u32,
729                        available: (frame.payload.len() - 9) as u64,
730                    });
731                }
732                if section != self.cursor && section != self.cursor + 1 {
733                    return Err(Error::StreamState("sections out of order"));
734                }
735                if section == self.cursor + 1 {
736                    self.cursor = section;
737                }
738                if self.seen[section] && self.counts[section] != count {
739                    return Err(Error::StreamState("section count changed mid-stream"));
740                }
741                self.seen[section] = true;
742                self.counts[section] = count;
743                self.sections[section].extend_from_slice(&frame.payload[9..]);
744            }
745            FrameKind::Delta => {
746                if !self.ended {
747                    return Err(Error::StreamState("delta before end"));
748                }
749                let mut cursor = 0usize;
750                let count = take_u32(&frame.payload, &mut cursor)?;
751                if count > crate::layout::Limits::default().max_aux {
752                    return Err(Error::StreamDelta("too many delta operations"));
753                }
754                for _ in 0..count {
755                    self.pending_deltas
756                        .push(DeltaOp::decode(&frame.payload, &mut cursor)?);
757                }
758                if cursor != frame.payload.len() {
759                    return Err(Error::StreamDelta("trailing bytes in delta payload"));
760                }
761            }
762            FrameKind::End => {
763                if !self.begun {
764                    return Err(Error::StreamState("end before begin"));
765                }
766                if frame.payload.len() != 40 {
767                    return Err(Error::StreamFrameLen {
768                        declared: 40,
769                        available: frame.payload.len() as u64,
770                    });
771                }
772                let mut hash = [0u8; 32];
773                hash.copy_from_slice(&frame.payload[..32]);
774                let size = read_u64(&frame.payload, 32)?;
775                if self.ended {
776                    // Incremental generation: apply pending deltas first.
777                    let base = self
778                        .module
779                        .as_ref()
780                        .ok_or(Error::StreamState("no module to patch"))?;
781                    let updated = apply_delta(base, &self.pending_deltas)?;
782                    self.pending_deltas.clear();
783                    let image = encode_with(&updated, EncodeOpts::default())?;
784                    if image.len() as u64 != size {
785                        return Err(Error::StreamDelta("delta changed the declared size"));
786                    }
787                    if hash_image(&image)? != hash {
788                        return Err(Error::StreamHashMismatch);
789                    }
790                    let validated = Validated::new(&image, crate::layout::DecodeOpts::default())?;
791                    let module = crate::decode::decode_validated(&validated)?;
792                    self.module = Some(module);
793                    self.image = Some(image);
794                    return Ok(());
795                }
796                let image = self.assemble(size)?;
797                if hash_image(&image)? != hash {
798                    return Err(Error::StreamHashMismatch);
799                }
800                let validated = Validated::new(&image, crate::layout::DecodeOpts::default())?;
801                let module = crate::decode::decode_validated(&validated)?;
802                self.module = Some(module);
803                self.image = Some(image);
804                self.ended = true;
805            }
806        }
807        Ok(())
808    }
809
810    fn assemble(&self, size: u64) -> Result<Vec<u8>> {
811        if size < HEADER_SIZE as u64 {
812            return Err(Error::StreamDelta("declared size smaller than a header"));
813        }
814        if self.seen.iter().any(|seen| !seen) {
815            return Err(Error::StreamState("generation missing a section"));
816        }
817        let mut image = vec![0u8; HEADER_SIZE];
818        let mut header = crate::layout::Header {
819            major: crate::layout::FORMAT_MAJOR,
820            minor: crate::layout::FORMAT_MINOR,
821            flags: self.flags,
822            header_size: HEADER_SIZE as u16,
823            content_hash: [0u8; 32],
824            dialect_hash: crate::layout::dialect_hash(),
825            total_size: 0,
826            sections: [crate::layout::SectionSpan::default(); SECTION_COUNT],
827        };
828        for section in Section::ALL {
829            while image.len() % 8 != 0 {
830                image.push(0);
831            }
832            header.span_mut(section).offset = image.len() as u32;
833            header.span_mut(section).count = self.counts[section as usize];
834            image.extend_from_slice(&self.sections[section as usize]);
835        }
836        while image.len() % 8 != 0 {
837            image.push(0);
838        }
839        header.total_size = image.len() as u64;
840        header.write(&mut image);
841        let hash = hash_image(&image)?;
842        image[16..48].copy_from_slice(&hash);
843        if image.len() as u64 != size {
844            return Err(Error::StreamDelta(
845                "reassembled size disagrees with the end frame",
846            ));
847        }
848        Ok(image)
849    }
850
851    /// The reassembled module, once a generation completed.
852    pub fn module(&self) -> Option<&InfinityModule> {
853        self.module.as_ref()
854    }
855
856    /// The reassembled canonical image, once a generation completed.
857    pub fn image(&self) -> Option<&[u8]> {
858        self.image.as_deref()
859    }
860
861    /// Finish the stream, requiring at least one completed generation and no
862    /// half-received frame left over.
863    pub fn finish(self) -> Result<StreamedModule> {
864        if !self.partial.is_empty() {
865            return Err(Error::Truncated {
866                needed: self.partial.len() as u64
867                    + self.partial[16..20]
868                        .iter()
869                        .fold(0u64, |acc, byte| acc * 256 + *byte as u64),
870                found: self.partial.len() as u64,
871            });
872        }
873        let module = self.module.ok_or(Error::StreamUnterminated)?;
874        let image = self.image.ok_or(Error::StreamUnterminated)?;
875        let hash = hash_image(&image)?;
876        Ok(StreamedModule {
877            module,
878            image,
879            content_hash: hash,
880        })
881    }
882
883    /// Number of frames consumed.
884    pub fn frames(&self) -> u64 {
885        self.frames
886    }
887}
888
889/// Stream encoder facade: prepares frames for a module and for deltas.
890#[derive(Debug)]
891pub struct StreamEncoder {
892    frames: VecDeque<Frame>,
893    sequence: u64,
894}
895
896impl StreamEncoder {
897    /// Encode `module` as a complete generation.
898    pub fn new(module: &InfinityModule, chunk: usize) -> Result<Self> {
899        let frames = encode_stream(module, chunk)?;
900        let sequence = frames.len() as u64;
901        Ok(Self {
902            frames: frames.into(),
903            sequence,
904        })
905    }
906
907    /// Append an incremental generation: the delta ops plus the closing end
908    /// frame carrying the hash of the *whole* re-encoded module.
909    pub fn push_delta(&mut self, base: &InfinityModule, ops: &[DeltaOp]) -> Result<()> {
910        let updated = apply_delta(base, ops)?;
911        let image = encode_with(&updated, EncodeOpts::default())?;
912        let mut payload = Vec::new();
913        payload.extend_from_slice(&(ops.len() as u32).to_le_bytes());
914        for op in ops {
915            op.encode(&mut payload)?;
916        }
917        self.frames
918            .push_back(Frame::new(FrameKind::Delta, self.sequence, payload));
919        self.sequence += 1;
920        let mut end = Vec::new();
921        end.extend_from_slice(&hash_image(&image)?);
922        end.extend_from_slice(&(image.len() as u64).to_le_bytes());
923        self.frames
924            .push_back(Frame::new(FrameKind::End, self.sequence, end));
925        self.sequence += 1;
926        Ok(())
927    }
928
929    /// All frames in order.
930    pub fn frames(&self) -> Vec<Frame> {
931        self.frames.iter().cloned().collect()
932    }
933
934    /// Concatenated frame bytes (a single byte stream).
935    pub fn to_bytes(&self) -> Vec<u8> {
936        let mut out = Vec::new();
937        for frame in &self.frames {
938            out.extend_from_slice(&frame.to_bytes());
939        }
940        out
941    }
942}
943
944fn write_string(out: &mut Vec<u8>, text: &str) -> Result<()> {
945    let bytes = text.as_bytes();
946    if bytes.len() > u16::MAX as usize {
947        return Err(Error::TooLarge);
948    }
949    out.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
950    out.extend_from_slice(bytes);
951    Ok(())
952}
953
954fn write_opt_string(out: &mut Vec<u8>, text: Option<&str>) -> Result<()> {
955    match text {
956        None => {
957            out.push(0);
958            Ok(())
959        }
960        Some(text) => {
961            out.push(1);
962            write_string(out, text)
963        }
964    }
965}
966
967fn take_u8(buf: &[u8], cursor: &mut usize) -> Result<u8> {
968    if *cursor >= buf.len() {
969        return Err(Error::StreamDelta("truncated delta payload"));
970    }
971    let value = buf[*cursor];
972    *cursor += 1;
973    Ok(value)
974}
975
976fn take_u32(buf: &[u8], cursor: &mut usize) -> Result<u32> {
977    if *cursor + 4 > buf.len() {
978        return Err(Error::StreamDelta("truncated delta payload"));
979    }
980    let value = read_u32(buf, *cursor)?;
981    *cursor += 4;
982    Ok(value)
983}
984
985fn take_u64(buf: &[u8], cursor: &mut usize) -> Result<u64> {
986    if *cursor + 8 > buf.len() {
987        return Err(Error::StreamDelta("truncated delta payload"));
988    }
989    let value = read_u64(buf, *cursor)?;
990    *cursor += 8;
991    Ok(value)
992}
993
994fn take_i64(buf: &[u8], cursor: &mut usize) -> Result<i64> {
995    take_u64(buf, cursor).map(|value| value as i64)
996}
997
998fn take_hash(buf: &[u8], cursor: &mut usize) -> Result<[u8; 32]> {
999    if *cursor + 32 > buf.len() {
1000        return Err(Error::StreamDelta("truncated delta payload"));
1001    }
1002    let mut hash = [0u8; 32];
1003    hash.copy_from_slice(&buf[*cursor..*cursor + 32]);
1004    *cursor += 32;
1005    Ok(hash)
1006}
1007
1008fn take_string(buf: &[u8], cursor: &mut usize) -> Result<String> {
1009    if *cursor + 2 > buf.len() {
1010        return Err(Error::StreamDelta("truncated delta payload"));
1011    }
1012    let len = u16::from_le_bytes([buf[*cursor], buf[*cursor + 1]]) as usize;
1013    *cursor += 2;
1014    if *cursor + len > buf.len() {
1015        return Err(Error::StreamDelta("truncated delta payload"));
1016    }
1017    let text = std::str::from_utf8(&buf[*cursor..*cursor + len])
1018        .map_err(|_| Error::Utf8 {
1019            offset: *cursor as u64,
1020        })?
1021        .to_string();
1022    *cursor += len;
1023    Ok(text)
1024}
1025
1026fn take_opt_string(buf: &[u8], cursor: &mut usize) -> Result<Option<String>> {
1027    match take_u8(buf, cursor)? {
1028        0 => Ok(None),
1029        1 => Ok(Some(take_string(buf, cursor)?)),
1030        _ => Err(Error::StreamDelta("invalid optional string tag")),
1031    }
1032}
1033
1034fn decode_fragment(buf: &[u8], cursor: &mut usize) -> Result<InfinityModule> {
1035    let len = take_u32(buf, cursor)? as usize;
1036    if *cursor + len > buf.len() {
1037        return Err(Error::StreamDelta("truncated fragment"));
1038    }
1039    let bytes = &buf[*cursor..*cursor + len];
1040    *cursor += len;
1041    crate::decode::decode(bytes)
1042}
1043
1044/// Apply a list of delta operations and return the re-encoded hash.
1045pub fn delta_hash(module: &InfinityModule, ops: &[DeltaOp]) -> Result<[u8; 32]> {
1046    let updated = apply_delta(module, ops)?;
1047    let image = encode_with(&updated, EncodeOpts::default())?;
1048    hash_image(&image)
1049}
1050
1051/// Reassemble a whole byte stream into a module.
1052pub fn decode_stream(bytes: &[u8]) -> Result<StreamedModule> {
1053    let mut decoder = StreamDecoder::new();
1054    decoder.push_bytes(bytes)?;
1055    decoder.finish()
1056}
1057
1058/// Convenience: number of bytes a generation occupies after padding.
1059pub fn aligned_section_len(len: usize) -> usize {
1060    align8(len as u64) as usize
1061}
1062
1063/// Test helper: annotations that a delta touched.
1064pub fn delta_annotations(ops: &[DeltaOp]) -> Vec<Annotation> {
1065    ops.iter()
1066        .filter_map(|op| match op {
1067            DeltaOp::SetAnnotation { node, kind } => Some(Annotation {
1068                node: *node,
1069                kind: kind.clone(),
1070            }),
1071            _ => None,
1072        })
1073        .collect()
1074}