Skip to main content

cu29_logstream/
capture.rs

1//! Generated selective native codecs. Optional verification runs on output workers.
2
3use crate::{ApplicationSchema, Error, Result};
4use alloc::string::ToString;
5use bincode::{Encode, enc::Encoder, error::EncodeError};
6use cu29_runtime::copperlist::CopperList;
7use cu29_traits::CopperListTuple;
8
9// Gate generated methods using this crate's feature, not a downstream app's
10// feature names or the proc-macro host's independently resolved features.
11#[doc(hidden)]
12#[cfg(feature = "verify-reconstruction")]
13#[macro_export]
14macro_rules! __with_reconstruction_verification {
15    ($($item:item)*) => { $($item)* };
16}
17#[doc(hidden)]
18#[cfg(not(feature = "verify-reconstruction"))]
19#[macro_export]
20macro_rules! __with_reconstruction_verification {
21    ($($item:item)*) => {};
22}
23
24/// Generated from RON. Slot dispatch and storage are fixed at compile time.
25pub trait CaptureDataSet: CopperListTuple {
26    /// Per-output flags: `true` for reconstructed outputs, `false` for captured outputs.
27    const RECONSTRUCTION: &'static [bool];
28    fn stream_schema() -> ApplicationSchema;
29    fn encode_capture<E: Encoder>(&self, encoder: &mut E) -> core::result::Result<(), EncodeError>;
30    fn validate_capture(&self) -> Result<()>;
31    fn restore_sender_metadata(&mut self, captured: &Self);
32    #[cfg(feature = "verify-reconstruction")]
33    fn encode_reconstruction<E: Encoder>(
34        &self,
35        encoder: &mut E,
36    ) -> core::result::Result<(), EncodeError>;
37    #[cfg(feature = "verify-reconstruction")]
38    fn reconstruction_digest(&self) -> Result<[u8; 32]> {
39        payload_digest(&ReconstructionView(self))
40    }
41}
42
43pub struct CaptureView<'a, P>(pub &'a P);
44impl<P: CaptureDataSet> Encode for CaptureView<'_, P> {
45    fn encode<E: Encoder>(&self, encoder: &mut E) -> core::result::Result<(), EncodeError> {
46        self.0.encode_capture(encoder)
47    }
48}
49
50#[cfg(feature = "verify-reconstruction")]
51struct ReconstructionView<'a, P>(&'a P);
52#[cfg(feature = "verify-reconstruction")]
53impl<P: CaptureDataSet> Encode for ReconstructionView<'_, P> {
54    fn encode<E: Encoder>(&self, encoder: &mut E) -> core::result::Result<(), EncodeError> {
55        self.0.encode_reconstruction(encoder)
56    }
57}
58
59#[cfg(feature = "verify-reconstruction")]
60struct DigestWriter(blake3::Hasher);
61#[cfg(feature = "verify-reconstruction")]
62impl bincode::enc::write::Writer for DigestWriter {
63    fn write(&mut self, bytes: &[u8]) -> core::result::Result<(), EncodeError> {
64        self.0.update(bytes);
65        Ok(())
66    }
67}
68
69/// Debug-only digest of the reconstructed outputs, including payload presence.
70/// Hashes directly from borrowed payloads without an intermediate allocation.
71#[cfg(feature = "verify-reconstruction")]
72pub fn payload_digest(value: &impl Encode) -> Result<[u8; 32]> {
73    let mut writer = DigestWriter(blake3::Hasher::new());
74    value
75        .encode(&mut bincode::enc::EncoderImpl::new(
76            &mut writer,
77            bincode::config::standard(),
78        ))
79        .map_err(|e| Error::Codec(e.to_string()))?;
80    Ok(*writer.0.finalize().as_bytes())
81}
82
83/// A native capture. Production stores only the CopperList itself.
84pub struct CapturedList<P: CopperListTuple> {
85    pub copperlist: CopperList<P>,
86    #[cfg(feature = "verify-reconstruction")]
87    pub digest: Option<[u8; 32]>,
88}
89
90impl<P: CopperListTuple> CapturedList<P> {
91    /// Wrap an ordinary capture archive entry. Archives contain no debug digests.
92    pub fn new(copperlist: CopperList<P>) -> Self {
93        Self {
94            copperlist,
95            #[cfg(feature = "verify-reconstruction")]
96            digest: None,
97        }
98    }
99}
100
101pub fn encode_capture_record_into<P: CaptureDataSet>(
102    list: &CopperList<P>,
103    output: &mut [u8],
104) -> Result<usize> {
105    let header_len = crate::record::RECORD_HEADER_LEN;
106    if output.len() < header_len {
107        return Err(Error::BufferTooSmall {
108            needed: header_len,
109            available: output.len(),
110        });
111    }
112    let (header, payload) = output.split_at_mut(header_len);
113    let len = bincode::encode_into_slice(
114        (list.id, CaptureView(&list.msgs)),
115        payload,
116        bincode::config::standard(),
117    )
118    .map_err(|e| Error::Codec(e.to_string()))?;
119    // A developer build appends one fixed-size digest. Production is exactly
120    // the existing native CopperList format, including its presence planes.
121    #[cfg(feature = "verify-reconstruction")]
122    let len = {
123        let available = payload.len();
124        let trailer = payload
125            .get_mut(len..len + 32)
126            .ok_or(Error::BufferTooSmall {
127                needed: header_len + len + 32,
128                available: header_len + available,
129            })?;
130        trailer.copy_from_slice(&list.msgs.reconstruction_digest()?);
131        len + 32
132    };
133    crate::record::encode_record_header(
134        crate::RecordKind::CopperList,
135        list.id,
136        &payload[..len],
137        header,
138    )?;
139    Ok(header_len + len)
140}
141
142/// Decode once and borrow the native bytes for archival. Verification builds
143/// accept production captures too; production rejects debug trailers explicitly.
144pub fn decode_capture<P: CaptureDataSet>(payload: &[u8]) -> Result<(CapturedList<P>, &[u8])> {
145    let (copperlist, consumed): (CopperList<P>, usize) =
146        bincode::decode_from_slice(payload, bincode::config::standard())
147            .map_err(|e| Error::Codec(e.to_string()))?;
148    copperlist.msgs.validate_capture()?;
149    #[cfg(feature = "verify-reconstruction")]
150    let digest = match &payload[consumed..] {
151        [] => None,
152        bytes => Some(
153            bytes
154                .try_into()
155                .map_err(|_| Error::InvalidConfig("invalid reconstruction digest trailer"))?,
156        ),
157    };
158    #[cfg(not(feature = "verify-reconstruction"))]
159    if consumed != payload.len() {
160        return Err(Error::InvalidConfig(
161            "capture has trailing bytes; debug captures require verify-reconstruction",
162        ));
163    }
164    Ok((
165        CapturedList {
166            copperlist,
167            #[cfg(feature = "verify-reconstruction")]
168            digest,
169        },
170        &payload[..consumed],
171    ))
172}
173
174#[cfg(feature = "verify-reconstruction")]
175impl<P: CaptureDataSet> CapturedList<P> {
176    pub fn verify(&self) -> Result<()> {
177        if let Some(expected) = self.digest
178            && self.copperlist.msgs.reconstruction_digest()? != expected
179        {
180            return Err(Error::InvalidConfig(
181                "deterministic reconstruction diverged",
182            ));
183        }
184        Ok(())
185    }
186}