Skip to main content

bcp_wire/
block_frame.rs

1use crate::error::WireError;
2use crate::varint::{decode_varint, encode_varint};
3
4/// Per-block flags bitfield.
5///
6/// Bit layout:
7///   bit 0 = has summary sub-block appended after the body
8///   bit 1 = body is compressed with zstd
9///   bit 2 = body is a BLAKE3 hash reference, not inline data
10///   bits 3-7 = reserved
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct BlockFlags(u8);
13
14impl BlockFlags {
15    pub const NONE: Self = Self(0);
16    pub const HAS_SUMMARY: Self = Self(0b0000_0001);
17    pub const COMPRESSED: Self = Self(0b0000_0010);
18    pub const IS_REFERENCE: Self = Self(0b0000_0100);
19
20    pub fn from_raw(raw: u8) -> Self {
21        Self(raw)
22    }
23
24    pub fn raw(self) -> u8 {
25        self.0
26    }
27
28    pub fn has_summary(self) -> bool {
29        self.0 & Self::HAS_SUMMARY.0 != 0
30    }
31
32    pub fn is_compressed(self) -> bool {
33        self.0 & Self::COMPRESSED.0 != 0
34    }
35
36    pub fn is_reference(self) -> bool {
37        self.0 & Self::IS_REFERENCE.0 != 0
38    }
39}
40
41/// Known block type IDs.
42///
43/// These are the semantic type tags that appear on the wire.
44/// The `bcp-types` crate defines the full typed structs for each.
45pub mod block_type {
46    pub const CODE: u8 = 0x01;
47    pub const CONVERSATION: u8 = 0x02;
48    pub const FILE_TREE: u8 = 0x03;
49    pub const TOOL_RESULT: u8 = 0x04;
50    pub const DOCUMENT: u8 = 0x05;
51    pub const STRUCTURED_DATA: u8 = 0x06;
52    pub const DIFF: u8 = 0x07;
53    pub const ANNOTATION: u8 = 0x08;
54    pub const EMBEDDING_REF: u8 = 0x09;
55    pub const IMAGE: u8 = 0x0A;
56    pub const EXTENSION: u8 = 0xFE;
57    pub const END: u8 = 0xFF;
58}
59
60/// Block frame — the wire envelope wrapping every block's body.
61///
62/// ```text
63/// ┌──────────────────────────────────────────────────┐
64/// │ block_type   (varint, 1-2 bytes typically)       │
65/// │ block_flags  (uint8, 1 byte)                     │
66/// │ content_len  (varint, 1-5 bytes typically)       │
67/// │ body         [content_len bytes]                  │
68/// └──────────────────────────────────────────────────┘
69/// ```
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct BlockFrame {
72    /// The semantic block type (CODE=0x01, CONVERSATION=0x02, etc.).
73    pub block_type: u8,
74
75    /// Per-block flags.
76    pub flags: BlockFlags,
77
78    /// The raw body bytes (content_len bytes from the wire).
79    pub body: Vec<u8>,
80}
81
82/// Maximum varint size in bytes, used for buffer sizing.
83const MAX_VARINT_LEN: usize = 10;
84
85impl BlockFrame {
86    /// Write this block frame to the provided writer.
87    ///
88    /// Wire layout written:
89    ///   1. block_type as varint
90    ///   2. block_flags as single u8
91    ///   3. body.len() as varint (content_len)
92    ///   4. body bytes
93    ///
94    /// # Returns
95    ///
96    /// Total number of bytes written.
97    pub fn write_to(&self, w: &mut impl std::io::Write) -> Result<usize, WireError> {
98        let mut bytes_written = 0;
99        let mut varint_buf = [0u8; MAX_VARINT_LEN];
100
101        // 1. Block type as varint
102        let n = encode_varint(u64::from(self.block_type), &mut varint_buf);
103        w.write_all(&varint_buf[..n])?;
104        bytes_written += n;
105
106        // 2. Flags as a single raw byte
107        w.write_all(&[self.flags.raw()])?;
108        bytes_written += 1;
109
110        // 3. Content length as varint
111        let n = encode_varint(self.body.len() as u64, &mut varint_buf);
112        w.write_all(&varint_buf[..n])?;
113        bytes_written += n;
114
115        // 4. Body bytes
116        w.write_all(&self.body)?;
117        bytes_written += self.body.len();
118
119        Ok(bytes_written)
120    }
121
122    /// Read a block frame from the provided byte slice.
123    ///
124    /// # Returns
125    ///
126    /// `Some((frame, bytes_consumed))` for normal blocks, or
127    /// `None` if the block type is END (0xFF), signaling the
128    /// end of the block stream.
129    ///
130    /// # Errors
131    ///
132    /// - [`WireError::UnexpectedEof`] if the slice is too short.
133    /// - [`WireError::VarintTooLong`] if a varint is malformed.
134    pub fn read_from(buf: &[u8]) -> Result<Option<(Self, usize)>, WireError> {
135        let mut cursor = 0;
136
137        // 1. Block type (varint)
138        let (block_type_raw, n) = decode_varint(
139            buf.get(cursor..)
140                .ok_or(WireError::UnexpectedEof { offset: cursor })?,
141        )?;
142        cursor += n;
143
144        // END sentinel: check the full varint value, not just the low byte.
145        // A truncating `as u8` cast would falsely match multi-byte varints
146        // whose low 8 bits happen to be 0xFF (e.g., 16383 → 0xFF).
147        if block_type_raw == u64::from(block_type::END) {
148            return Ok(None);
149        }
150
151        let block_type = u8::try_from(block_type_raw).map_err(|_| {
152            WireError::InvalidBlockType {
153                raw: block_type_raw,
154            }
155        })?;
156
157        // 2. Flags (single byte)
158        let flags_byte = *buf
159            .get(cursor)
160            .ok_or(WireError::UnexpectedEof { offset: cursor })?;
161        cursor += 1;
162        let flags = BlockFlags::from_raw(flags_byte);
163
164        // 3. Content length (varint)
165        let (content_len, n) = decode_varint(
166            buf.get(cursor..)
167                .ok_or(WireError::UnexpectedEof { offset: cursor })?,
168        )?;
169        cursor += n;
170
171        // Check for overflow before converting to usize
172        let content_len_usize = usize::try_from(content_len).map_err(|_| {
173            WireError::UnexpectedEof {
174                offset: cursor, // position after content_len varint
175            }
176        })?;
177
178        // 4. Body bytes
179        let body_end = match cursor.checked_add(content_len_usize) {
180            Some(end) => end,
181            None => {
182                return Err(WireError::UnexpectedEof {
183                    offset: buf.len(),
184                })
185            }
186        };
187        if buf.len() < body_end {
188            return Err(WireError::UnexpectedEof { offset: buf.len() });
189        }
190        let body = buf[cursor..body_end].to_vec();
191        cursor = body_end;
192
193        Ok(Some((
194            Self {
195                block_type,
196                flags,
197                body,
198            },
199            cursor,
200        )))
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    /// Helper: write a frame to a Vec and return the bytes.
209    fn write_frame(frame: &BlockFrame) -> Vec<u8> {
210        let mut buf = Vec::new();
211        frame.write_to(&mut buf).unwrap();
212        buf
213    }
214
215    #[test]
216    fn roundtrip_code_block() {
217        let frame = BlockFrame {
218            block_type: block_type::CODE,
219            flags: BlockFlags::NONE,
220            body: b"fn main() {}".to_vec(),
221        };
222        let bytes = write_frame(&frame);
223        let (parsed, consumed) = BlockFrame::read_from(&bytes).unwrap().unwrap();
224        assert_eq!(parsed, frame);
225        assert_eq!(consumed, bytes.len());
226    }
227
228    #[test]
229    fn roundtrip_with_flags() {
230        let frame = BlockFrame {
231            block_type: block_type::TOOL_RESULT,
232            flags: BlockFlags::from_raw(
233                BlockFlags::HAS_SUMMARY.raw() | BlockFlags::COMPRESSED.raw(),
234            ),
235            body: vec![0xDE, 0xAD, 0xBE, 0xEF],
236        };
237        let bytes = write_frame(&frame);
238        let (parsed, _) = BlockFrame::read_from(&bytes).unwrap().unwrap();
239        assert!(parsed.flags.has_summary());
240        assert!(parsed.flags.is_compressed());
241        assert!(!parsed.flags.is_reference());
242        assert_eq!(parsed.body, vec![0xDE, 0xAD, 0xBE, 0xEF]);
243    }
244
245    #[test]
246    fn roundtrip_empty_body() {
247        let frame = BlockFrame {
248            block_type: block_type::ANNOTATION,
249            flags: BlockFlags::NONE,
250            body: vec![],
251        };
252        let bytes = write_frame(&frame);
253        let (parsed, consumed) = BlockFrame::read_from(&bytes).unwrap().unwrap();
254        assert_eq!(parsed, frame);
255        assert_eq!(consumed, bytes.len());
256    }
257
258    #[test]
259    fn roundtrip_large_body() {
260        // 10KB body to test multi-byte content_len varint
261        let frame = BlockFrame {
262            block_type: block_type::CODE,
263            flags: BlockFlags::NONE,
264            body: vec![0xAB; 10_000],
265        };
266        let bytes = write_frame(&frame);
267        let (parsed, consumed) = BlockFrame::read_from(&bytes).unwrap().unwrap();
268        assert_eq!(parsed.body.len(), 10_000);
269        assert_eq!(consumed, bytes.len());
270    }
271
272    #[test]
273    fn end_block_returns_none() {
274        // Manually write an END block: type=0xFF
275        let mut buf = Vec::new();
276        let mut varint_buf = [0u8; 10];
277        let n = encode_varint(u64::from(block_type::END), &mut varint_buf);
278        buf.extend_from_slice(&varint_buf[..n]);
279
280        let result = BlockFrame::read_from(&buf).unwrap();
281        assert!(result.is_none());
282    }
283
284    #[test]
285    fn read_truncated_body() {
286        // Write a frame claiming 100 bytes of body but only provide 5
287        let frame = BlockFrame {
288            block_type: block_type::CODE,
289            flags: BlockFlags::NONE,
290            body: vec![0xFF; 100],
291        };
292        let full_bytes = write_frame(&frame);
293
294        // Chop it short: keep the header but only 5 bytes of body
295        let truncated = &full_bytes[..full_bytes.len() - 95];
296        let result = BlockFrame::read_from(truncated);
297        assert!(matches!(result, Err(WireError::UnexpectedEof { .. })));
298    }
299
300    #[test]
301    fn multiple_frames_sequential() {
302        // Write two frames back-to-back, then read them both
303        let frame1 = BlockFrame {
304            block_type: block_type::CODE,
305            flags: BlockFlags::NONE,
306            body: b"first".to_vec(),
307        };
308        let frame2 = BlockFrame {
309            block_type: block_type::CONVERSATION,
310            flags: BlockFlags::NONE,
311            body: b"second".to_vec(),
312        };
313
314        let mut buf = Vec::new();
315        frame1.write_to(&mut buf).unwrap();
316        frame2.write_to(&mut buf).unwrap();
317
318        // Read first frame
319        let (parsed1, consumed1) = BlockFrame::read_from(&buf).unwrap().unwrap();
320        assert_eq!(parsed1, frame1);
321
322        // Read second frame starting where the first ended
323        let (parsed2, consumed2) = BlockFrame::read_from(&buf[consumed1..]).unwrap().unwrap();
324        assert_eq!(parsed2, frame2);
325        assert_eq!(consumed1 + consumed2, buf.len());
326    }
327
328    #[test]
329    fn all_block_types_roundtrip() {
330        let types = [
331            block_type::CODE,
332            block_type::CONVERSATION,
333            block_type::FILE_TREE,
334            block_type::TOOL_RESULT,
335            block_type::DOCUMENT,
336            block_type::STRUCTURED_DATA,
337            block_type::DIFF,
338            block_type::ANNOTATION,
339            block_type::EMBEDDING_REF,
340            block_type::IMAGE,
341            block_type::EXTENSION,
342        ];
343        for &bt in &types {
344            let frame = BlockFrame {
345                block_type: bt,
346                flags: BlockFlags::NONE,
347                body: vec![bt], // body is just the type byte for identification
348            };
349            let bytes = write_frame(&frame);
350            let (parsed, _) = BlockFrame::read_from(&bytes).unwrap().unwrap();
351            assert_eq!(parsed.block_type, bt, "failed for block type {bt:#04X}");
352            assert_eq!(parsed.body, vec![bt]);
353        }
354    }
355}