use bcp_types::block::{Block, BlockContent};
use bcp_types::block_type::BlockType;
use bcp_types::content_store::ContentStore;
use bcp_types::summary::Summary;
use bcp_wire::block_frame::BlockFrame;
use bcp_wire::header::{HEADER_SIZE, BcpHeader};
use crate::decompression::{self, MAX_BLOCK_DECOMPRESSED_SIZE, MAX_PAYLOAD_DECOMPRESSED_SIZE};
use crate::error::DecodeError;
pub struct DecodedPayload {
pub header: BcpHeader,
pub blocks: Vec<Block>,
}
pub struct BcpDecoder;
impl BcpDecoder {
pub fn decode(payload: &[u8]) -> Result<DecodedPayload, DecodeError> {
Self::decode_inner(payload, None)
}
pub fn decode_with_store(
payload: &[u8],
store: &dyn ContentStore,
) -> Result<DecodedPayload, DecodeError> {
Self::decode_inner(payload, Some(store))
}
fn decode_inner(
payload: &[u8],
store: Option<&dyn ContentStore>,
) -> Result<DecodedPayload, DecodeError> {
let header = BcpHeader::read_from(payload).map_err(DecodeError::InvalidHeader)?;
let block_data: std::borrow::Cow<'_, [u8]> = if header.flags.is_compressed() {
let compressed = &payload[HEADER_SIZE..];
let decompressed =
decompression::decompress(compressed, MAX_PAYLOAD_DECOMPRESSED_SIZE)?;
std::borrow::Cow::Owned(decompressed)
} else {
std::borrow::Cow::Borrowed(&payload[HEADER_SIZE..])
};
let mut cursor = 0;
let mut blocks = Vec::new();
let mut found_end = false;
while cursor < block_data.len() {
let remaining = &block_data[cursor..];
if let Some((frame, consumed)) = BlockFrame::read_from(remaining)? {
let block = Self::decode_block_frame(&frame, store)?;
blocks.push(block);
cursor += consumed;
} else {
found_end = true;
cursor += Self::end_sentinel_size(remaining)?;
break;
}
}
if !found_end {
return Err(DecodeError::MissingEndSentinel);
}
if cursor < block_data.len() {
return Err(DecodeError::TrailingData {
extra_bytes: block_data.len() - cursor,
});
}
Ok(DecodedPayload { header, blocks })
}
fn decode_block_frame(
frame: &BlockFrame,
store: Option<&dyn ContentStore>,
) -> Result<Block, DecodeError> {
let block_type = BlockType::from_wire_id(frame.block_type);
let resolved_body = if frame.flags.is_reference() {
let store = store.ok_or(DecodeError::MissingContentStore)?;
if frame.body.len() != 32 {
return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
offset: frame.body.len(),
}));
}
let hash: [u8; 32] = frame.body[..32].try_into().unwrap();
store
.get(&hash)
.ok_or(DecodeError::UnresolvedReference { hash })?
} else {
frame.body.clone()
};
let decompressed_body = if frame.flags.is_compressed() {
decompression::decompress(&resolved_body, MAX_BLOCK_DECOMPRESSED_SIZE)?
} else {
resolved_body
};
let mut body = decompressed_body.as_slice();
let mut summary = None;
if frame.flags.has_summary() {
let (sum, consumed) = Summary::decode(body)?;
summary = Some(sum);
body = &body[consumed..];
}
let content = BlockContent::decode_body(&block_type, body)?;
Ok(Block {
block_type,
flags: frame.flags,
summary,
content,
})
}
fn end_sentinel_size(buf: &[u8]) -> Result<usize, DecodeError> {
let (_, type_len) = bcp_wire::varint::decode_varint(buf)?;
let mut size = type_len;
if size >= buf.len() {
return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
offset: size,
}));
}
size += 1;
let rest = buf.get(size..).ok_or(DecodeError::Wire(
bcp_wire::WireError::UnexpectedEof { offset: size },
))?;
if rest.is_empty() {
return Err(DecodeError::Wire(bcp_wire::WireError::UnexpectedEof {
offset: size,
}));
}
let (_, len_size) = bcp_wire::varint::decode_varint(rest)?;
size += len_size;
Ok(size)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bcp_encoder::BcpEncoder;
use bcp_types::diff::DiffHunk;
use bcp_types::enums::{
AnnotationKind, DataFormat, FormatHint, Lang, MediaType, Priority, Role, Status,
};
use bcp_types::file_tree::{FileEntry, FileEntryKind};
use bcp_wire::block_frame::{BlockFlags, BlockFrame};
fn roundtrip(encoder: &BcpEncoder) -> DecodedPayload {
let payload = encoder.encode().unwrap();
BcpDecoder::decode(&payload).unwrap()
}
#[test]
fn decode_parses_encoder_output() {
let payload = BcpEncoder::new()
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.encode()
.unwrap();
let decoded = BcpDecoder::decode(&payload).unwrap();
assert_eq!(decoded.blocks.len(), 1);
assert_eq!(decoded.header.version_major, 1);
assert_eq!(decoded.header.version_minor, 0);
}
#[test]
fn roundtrip_single_code_block() {
let decoded =
roundtrip(BcpEncoder::new().add_code(Lang::Rust, "lib.rs", b"pub fn hello() {}"));
assert_eq!(decoded.blocks.len(), 1);
let block = &decoded.blocks[0];
assert_eq!(block.block_type, BlockType::Code);
assert!(block.summary.is_none());
match &block.content {
BlockContent::Code(code) => {
assert_eq!(code.lang, Lang::Rust);
assert_eq!(code.path, "lib.rs");
assert_eq!(code.content, b"pub fn hello() {}");
assert!(code.line_range.is_none());
}
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_multiple_block_types() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Python, "app.py", b"print('hi')")
.add_conversation(Role::User, b"What is this?")
.add_conversation(Role::Assistant, b"A greeting script.")
.add_tool_result("pytest", Status::Ok, b"1 passed")
.add_document("README", b"# Hello", FormatHint::Markdown),
);
assert_eq!(decoded.blocks.len(), 5);
let types: Vec<_> = decoded
.blocks
.iter()
.map(|b| b.block_type.clone())
.collect();
assert_eq!(
types,
vec![
BlockType::Code,
BlockType::Conversation,
BlockType::Conversation,
BlockType::ToolResult,
BlockType::Document,
]
);
}
#[test]
fn roundtrip_with_summary() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.with_summary("Application entry point.").unwrap(),
);
assert_eq!(decoded.blocks.len(), 1);
let block = &decoded.blocks[0];
assert!(block.flags.has_summary());
assert_eq!(
block.summary.as_ref().unwrap().text,
"Application entry point."
);
match &block.content {
BlockContent::Code(code) => {
assert_eq!(code.path, "main.rs");
}
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_with_priority_annotation() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Rust, "lib.rs", b"// code")
.with_priority(Priority::High).unwrap(),
);
assert_eq!(decoded.blocks.len(), 2);
assert_eq!(decoded.blocks[0].block_type, BlockType::Code);
assert_eq!(decoded.blocks[1].block_type, BlockType::Annotation);
match &decoded.blocks[1].content {
BlockContent::Annotation(ann) => {
assert_eq!(ann.target_block_id, 0);
assert_eq!(ann.kind, AnnotationKind::Priority);
assert_eq!(ann.value, vec![Priority::High.to_wire_byte()]);
}
other => panic!("expected Annotation, got {other:?}"),
}
}
#[test]
fn roundtrip_all_block_types() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.add_conversation(Role::User, b"hello")
.add_file_tree(
"/project",
vec![FileEntry {
name: "lib.rs".to_string(),
kind: FileEntryKind::File,
size: 100,
children: vec![],
}],
)
.add_tool_result("rg", Status::Ok, b"3 matches")
.add_document("README", b"# Title", FormatHint::Markdown)
.add_structured_data(DataFormat::Json, b"{\"key\": \"val\"}")
.add_diff(
"src/lib.rs",
vec![DiffHunk {
old_start: 1,
new_start: 1,
lines: b"+new line\n".to_vec(),
}],
)
.add_annotation(0, AnnotationKind::Tag, b"important")
.add_image(MediaType::Png, "screenshot", b"\x89PNG")
.add_extension("myco", "custom", b"data"),
);
assert_eq!(decoded.blocks.len(), 10);
let types: Vec<_> = decoded
.blocks
.iter()
.map(|b| b.block_type.clone())
.collect();
assert_eq!(
types,
vec![
BlockType::Code,
BlockType::Conversation,
BlockType::FileTree,
BlockType::ToolResult,
BlockType::Document,
BlockType::StructuredData,
BlockType::Diff,
BlockType::Annotation,
BlockType::Image,
BlockType::Extension,
]
);
}
#[test]
fn roundtrip_code_with_line_range() {
let decoded = roundtrip(BcpEncoder::new().add_code_range(
Lang::Rust,
"lib.rs",
b"fn foo() {}",
10,
20,
));
match &decoded.blocks[0].content {
BlockContent::Code(code) => {
assert_eq!(code.line_range, Some((10, 20)));
}
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_conversation_with_tool_call_id() {
let decoded =
roundtrip(BcpEncoder::new().add_conversation_tool(Role::Tool, b"result", "call_abc"));
match &decoded.blocks[0].content {
BlockContent::Conversation(conv) => {
assert_eq!(conv.tool_call_id.as_deref(), Some("call_abc"));
}
other => panic!("expected Conversation, got {other:?}"),
}
}
#[test]
fn roundtrip_preserves_all_field_values() {
let decoded = roundtrip(
BcpEncoder::new()
.add_file_tree(
"/project/src",
vec![
FileEntry {
name: "main.rs".to_string(),
kind: FileEntryKind::File,
size: 512,
children: vec![],
},
FileEntry {
name: "lib".to_string(),
kind: FileEntryKind::Directory,
size: 0,
children: vec![FileEntry {
name: "utils.rs".to_string(),
kind: FileEntryKind::File,
size: 128,
children: vec![],
}],
},
],
)
.add_diff(
"Cargo.toml",
vec![
DiffHunk {
old_start: 5,
new_start: 5,
lines: b"+tokio = \"1\"\n".to_vec(),
},
DiffHunk {
old_start: 20,
new_start: 21,
lines: b"-old_dep = \"0.1\"\n+new_dep = \"0.2\"\n".to_vec(),
},
],
),
);
assert_eq!(decoded.blocks.len(), 2);
match &decoded.blocks[0].content {
BlockContent::FileTree(tree) => {
assert_eq!(tree.root_path, "/project/src");
assert_eq!(tree.entries.len(), 2);
assert_eq!(tree.entries[0].name, "main.rs");
assert_eq!(tree.entries[0].size, 512);
assert_eq!(tree.entries[1].name, "lib");
assert_eq!(tree.entries[1].children.len(), 1);
assert_eq!(tree.entries[1].children[0].name, "utils.rs");
}
other => panic!("expected FileTree, got {other:?}"),
}
match &decoded.blocks[1].content {
BlockContent::Diff(diff) => {
assert_eq!(diff.path, "Cargo.toml");
assert_eq!(diff.hunks.len(), 2);
assert_eq!(diff.hunks[0].old_start, 5);
assert_eq!(diff.hunks[1].old_start, 20);
assert_eq!(diff.hunks[1].new_start, 21);
}
other => panic!("expected Diff, got {other:?}"),
}
}
#[test]
fn rejects_bad_magic() {
let mut payload = BcpEncoder::new()
.add_conversation(Role::User, b"hi")
.encode()
.unwrap();
payload[0] = b'X';
let result = BcpDecoder::decode(&payload);
assert!(matches!(result, Err(DecodeError::InvalidHeader(_))));
}
#[test]
fn rejects_truncated_header() {
let result = BcpDecoder::decode(&[0x4C, 0x43, 0x50, 0x00]);
assert!(matches!(result, Err(DecodeError::InvalidHeader(_))));
}
#[test]
fn rejects_missing_end_sentinel() {
let payload = BcpEncoder::new()
.add_conversation(Role::User, b"hi")
.encode()
.unwrap();
let truncated = &payload[..payload.len() - 4];
let result = BcpDecoder::decode(truncated);
assert!(matches!(result, Err(DecodeError::MissingEndSentinel)));
}
#[test]
fn detects_trailing_data() {
let mut payload = BcpEncoder::new()
.add_conversation(Role::User, b"hi")
.encode()
.unwrap();
payload.extend_from_slice(b"trailing garbage");
let result = BcpDecoder::decode(&payload);
assert!(matches!(
result,
Err(DecodeError::TrailingData { extra_bytes: 16 })
));
}
#[test]
fn unknown_block_type_captured_not_rejected() {
use bcp_wire::header::HeaderFlags;
let mut payload = vec![0u8; HEADER_SIZE];
let header = BcpHeader::new(HeaderFlags::NONE);
header.write_to(&mut payload).unwrap();
let frame = BlockFrame {
block_type: 0x42,
flags: BlockFlags::NONE,
body: b"hello".to_vec(),
};
frame.write_to(&mut payload).unwrap();
let end = BlockFrame {
block_type: 0xFF,
flags: BlockFlags::NONE,
body: Vec::new(),
};
end.write_to(&mut payload).unwrap();
let decoded = BcpDecoder::decode(&payload).unwrap();
assert_eq!(decoded.blocks.len(), 1);
assert_eq!(decoded.blocks[0].block_type, BlockType::Unknown(0x42));
match &decoded.blocks[0].content {
BlockContent::Unknown { type_id, body } => {
assert_eq!(*type_id, 0x42);
assert_eq!(body, b"hello");
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn optional_fields_absent_result_in_none() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Rust, "x.rs", b"let x = 1;")
.add_conversation(Role::User, b"msg"),
);
match &decoded.blocks[0].content {
BlockContent::Code(code) => assert!(code.line_range.is_none()),
other => panic!("expected Code, got {other:?}"),
}
match &decoded.blocks[1].content {
BlockContent::Conversation(conv) => assert!(conv.tool_call_id.is_none()),
other => panic!("expected Conversation, got {other:?}"),
}
}
#[test]
fn summary_extraction_with_body() {
let decoded = roundtrip(
BcpEncoder::new()
.add_document(
"Guide",
b"# Getting Started\n\nWelcome!",
FormatHint::Markdown,
)
.with_summary("Onboarding guide for new contributors.").unwrap(),
);
let block = &decoded.blocks[0];
assert!(block.flags.has_summary());
assert_eq!(
block.summary.as_ref().unwrap().text,
"Onboarding guide for new contributors."
);
match &block.content {
BlockContent::Document(doc) => {
assert_eq!(doc.title, "Guide");
assert_eq!(doc.content, b"# Getting Started\n\nWelcome!");
assert_eq!(doc.format_hint, FormatHint::Markdown);
}
other => panic!("expected Document, got {other:?}"),
}
}
#[test]
fn rfc_example_roundtrip() {
let decoded = roundtrip(
BcpEncoder::new()
.add_code(Lang::Rust, "src/main.rs", b"fn main() { todo!() }")
.with_summary("Entry point: CLI setup and server startup.").unwrap()
.with_priority(Priority::High).unwrap()
.add_conversation(Role::User, b"Fix the timeout bug.")
.add_conversation(Role::Assistant, b"I'll examine the pool config...")
.add_tool_result("ripgrep", Status::Ok, b"3 matches found."),
);
assert_eq!(decoded.blocks.len(), 5);
assert_eq!(decoded.blocks[0].block_type, BlockType::Code);
assert_eq!(
decoded.blocks[0].summary.as_ref().unwrap().text,
"Entry point: CLI setup and server startup."
);
assert_eq!(decoded.blocks[1].block_type, BlockType::Annotation);
assert_eq!(decoded.blocks[2].block_type, BlockType::Conversation);
assert_eq!(decoded.blocks[3].block_type, BlockType::Conversation);
assert_eq!(decoded.blocks[4].block_type, BlockType::ToolResult);
}
#[test]
fn empty_body_blocks() {
let decoded = roundtrip(BcpEncoder::new().add_extension("ns", "type", b""));
match &decoded.blocks[0].content {
BlockContent::Extension(ext) => {
assert_eq!(ext.namespace, "ns");
assert_eq!(ext.type_name, "type");
assert!(ext.content.is_empty());
}
other => panic!("expected Extension, got {other:?}"),
}
}
#[test]
fn roundtrip_per_block_compression() {
let big_content = "fn main() { println!(\"hello world\"); }\n".repeat(50);
let payload = BcpEncoder::new()
.add_code(Lang::Rust, "main.rs", big_content.as_bytes())
.with_compression().unwrap()
.encode()
.unwrap();
let frame_buf = &payload[HEADER_SIZE..];
let (frame, _) = BlockFrame::read_from(frame_buf).unwrap().unwrap();
assert!(frame.flags.is_compressed());
let decoded = BcpDecoder::decode(&payload).unwrap();
assert_eq!(decoded.blocks.len(), 1);
match &decoded.blocks[0].content {
BlockContent::Code(code) => {
assert_eq!(code.path, "main.rs");
assert_eq!(code.content, big_content.as_bytes());
}
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_per_block_compression_with_summary() {
let big_content = "pub fn process() -> Result<(), Error> { Ok(()) }\n".repeat(50);
let payload = BcpEncoder::new()
.add_code(Lang::Rust, "lib.rs", big_content.as_bytes())
.with_summary("Main processing function.").unwrap()
.with_compression().unwrap()
.encode()
.unwrap();
let decoded = BcpDecoder::decode(&payload).unwrap();
let block = &decoded.blocks[0];
assert!(block.flags.has_summary());
assert!(block.flags.is_compressed());
assert_eq!(
block.summary.as_ref().unwrap().text,
"Main processing function."
);
match &block.content {
BlockContent::Code(code) => assert_eq!(code.content, big_content.as_bytes()),
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_whole_payload_compression() {
let big_content = "use std::io;\n".repeat(100);
let payload = BcpEncoder::new()
.add_code(Lang::Rust, "a.rs", big_content.as_bytes())
.add_code(Lang::Rust, "b.rs", big_content.as_bytes())
.compress_payload()
.encode()
.unwrap();
let decoded = BcpDecoder::decode(&payload).unwrap();
assert_eq!(decoded.blocks.len(), 2);
assert!(decoded.header.flags.is_compressed());
for block in &decoded.blocks {
match &block.content {
BlockContent::Code(code) => {
assert_eq!(code.content, big_content.as_bytes());
}
other => panic!("expected Code, got {other:?}"),
}
}
}
#[test]
fn roundtrip_content_addressing() {
use bcp_encoder::MemoryContentStore;
use std::sync::Arc;
let store = Arc::new(MemoryContentStore::new());
let payload = BcpEncoder::new()
.set_content_store(store.clone())
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.with_content_addressing().unwrap()
.encode()
.unwrap();
let frame_buf = &payload[HEADER_SIZE..];
let (frame, _) = BlockFrame::read_from(frame_buf).unwrap().unwrap();
assert!(frame.flags.is_reference());
assert_eq!(frame.body.len(), 32);
let result = BcpDecoder::decode(&payload);
assert!(matches!(result, Err(DecodeError::MissingContentStore)));
let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
assert_eq!(decoded.blocks.len(), 1);
match &decoded.blocks[0].content {
BlockContent::Code(code) => {
assert_eq!(code.path, "main.rs");
assert_eq!(code.content, b"fn main() {}");
}
other => panic!("expected Code, got {other:?}"),
}
}
#[test]
fn roundtrip_auto_dedup() {
use bcp_encoder::MemoryContentStore;
use std::sync::Arc;
let store = Arc::new(MemoryContentStore::new());
let payload = BcpEncoder::new()
.set_content_store(store.clone())
.auto_dedup()
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.add_code(Lang::Rust, "main.rs", b"fn main() {}") .encode()
.unwrap();
let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
assert_eq!(decoded.blocks.len(), 2);
for block in &decoded.blocks {
match &block.content {
BlockContent::Code(code) => {
assert_eq!(code.content, b"fn main() {}");
}
other => panic!("expected Code, got {other:?}"),
}
}
}
#[test]
fn unresolved_reference_errors() {
use bcp_encoder::MemoryContentStore;
use std::sync::Arc;
let encode_store = Arc::new(MemoryContentStore::new());
let payload = BcpEncoder::new()
.set_content_store(encode_store)
.add_code(Lang::Rust, "main.rs", b"fn main() {}")
.with_content_addressing().unwrap()
.encode()
.unwrap();
let decode_store = MemoryContentStore::new();
let result = BcpDecoder::decode_with_store(&payload, &decode_store);
assert!(matches!(
result,
Err(DecodeError::UnresolvedReference { .. })
));
}
#[test]
fn roundtrip_refs_with_whole_payload_compression() {
use bcp_encoder::MemoryContentStore;
use std::sync::Arc;
let store = Arc::new(MemoryContentStore::new());
let big_content = "fn process() -> bool { true }\n".repeat(50);
let payload = BcpEncoder::new()
.set_content_store(store.clone())
.compress_payload()
.add_code(Lang::Rust, "main.rs", big_content.as_bytes())
.with_content_addressing().unwrap()
.add_conversation(Role::User, b"Review this code")
.encode()
.unwrap();
let decoded = BcpDecoder::decode_with_store(&payload, store.as_ref()).unwrap();
assert_eq!(decoded.blocks.len(), 2);
match &decoded.blocks[0].content {
BlockContent::Code(code) => {
assert_eq!(code.content, big_content.as_bytes());
}
other => panic!("expected Code, got {other:?}"),
}
match &decoded.blocks[1].content {
BlockContent::Conversation(conv) => {
assert_eq!(conv.content, b"Review this code");
}
other => panic!("expected Conversation, got {other:?}"),
}
}
}