cu29_logstream/
capture.rs1use 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#[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
24pub trait CaptureDataSet: CopperListTuple {
26 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#[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
83pub 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 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 #[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
142pub 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}