bcp_decoder/error.rs
1use bcp_types::error::TypeError;
2use bcp_wire::WireError;
3
4/// Errors that can occur during BCP payload decoding.
5///
6/// The decoder validates at multiple levels: header integrity, block
7/// frame structure, TLV body fields, stream termination, decompression,
8/// and content store resolution. Each error variant captures enough
9/// context for meaningful diagnostics.
10///
11/// Error hierarchy:
12///
13/// ```text
14/// DecodeError
15/// ├── InvalidHeader(WireError) ← magic, version, or reserved byte wrong
16/// ├── BlockTooLarge ← single block body exceeds size limit
17/// ├── MissingField ← required TLV field absent in block body
18/// ├── InvalidUtf8 ← string field contains non-UTF-8 bytes
19/// ├── MissingEndSentinel ← payload ran out without END block
20/// ├── TrailingData ← extra bytes after END sentinel
21/// ├── DecompressFailed ← zstd decompression error
22/// ├── DecompressionBomb ← decompressed size exceeds safety limit
23/// ├── UnresolvedReference ← BLAKE3 hash not found in content store
24/// ├── MissingContentStore ← IS_REFERENCE block but no store provided
25/// ├── Type(TypeError) ← from bcp-types body deserialization
26/// ├── Wire(WireError) ← from bcp-wire frame parsing
27/// └── Io(std::io::Error) ← from underlying I/O reads
28/// ```
29#[derive(Debug, thiserror::Error)]
30pub enum DecodeError {
31 /// The 8-byte file header failed validation.
32 ///
33 /// This wraps a [`WireError`] from `BcpHeader::read_from` — the
34 /// inner error distinguishes between bad magic, unsupported version,
35 /// and non-zero reserved byte.
36 #[error("invalid header: {0}")]
37 InvalidHeader(WireError),
38
39 /// A block body exceeds the maximum allowed size.
40 #[error("block body too large: {size} bytes at offset {offset}")]
41 BlockTooLarge { size: usize, offset: usize },
42
43 /// A required field was missing from a known block type's body.
44 ///
45 /// This provides richer context than the underlying
46 /// [`TypeError::MissingRequiredField`] by including the block type name
47 /// and the field's wire ID.
48 #[error("required field {field_name} (id={field_id}) missing in {block_type} block")]
49 MissingField {
50 block_type: &'static str,
51 field_name: &'static str,
52 field_id: u64,
53 },
54
55 /// A string field contained invalid UTF-8 bytes.
56 #[error("invalid UTF-8 in field {field_name} of {block_type} block")]
57 InvalidUtf8 {
58 block_type: &'static str,
59 field_name: &'static str,
60 },
61
62 /// The payload ended without an END sentinel block (type=0xFF).
63 ///
64 /// Every valid BCP payload must terminate with an END block. If the
65 /// byte stream is exhausted before encountering one, the payload is
66 /// considered truncated.
67 #[error("payload does not end with END sentinel")]
68 MissingEndSentinel,
69
70 /// Extra bytes were found after the END sentinel.
71 ///
72 /// Per the spec, this is a warning-level condition — the payload
73 /// decoded successfully, but the trailing data may indicate
74 /// corruption or a buggy encoder. The decoder captures this as an
75 /// error variant so callers can decide how to handle it.
76 #[error("unexpected data after END sentinel ({extra_bytes} bytes)")]
77 TrailingData { extra_bytes: usize },
78
79 /// Zstd decompression failed.
80 ///
81 /// Returned when a block's `COMPRESSED` flag (bit 1) or the header's
82 /// `COMPRESSED` flag (bit 0) is set and the zstd decoder cannot parse
83 /// the compressed data. Common causes: truncated input, corrupt frame,
84 /// or non-zstd data with the flag erroneously set.
85 #[error("zstd decompression failed: {0}")]
86 DecompressFailed(String),
87
88 /// Decompressed data exceeds the safety limit.
89 ///
90 /// Prevents decompression bombs — payloads crafted to decompress into
91 /// vastly larger output. The `limit` is the caller-configured maximum
92 /// (default: 16 MiB per block, 256 MiB for whole-payload).
93 #[error("decompressed size {actual} exceeds limit {limit}")]
94 DecompressionBomb { actual: usize, limit: usize },
95
96 /// A block has the `IS_REFERENCE` flag set but its 32-byte BLAKE3
97 /// hash was not found in the content store.
98 ///
99 /// This means the content was previously content-addressed during
100 /// encoding but the corresponding data was not provided to the
101 /// decoder's content store.
102 #[error("unresolved reference: BLAKE3 hash not found in content store")]
103 UnresolvedReference { hash: [u8; 32] },
104
105 /// A block has the `IS_REFERENCE` flag but no content store was
106 /// provided to the decoder.
107 ///
108 /// Use [`BcpDecoder::decode_with_store`] instead of
109 /// [`BcpDecoder::decode`] when decoding payloads that contain
110 /// content-addressed blocks.
111 #[error("block has IS_REFERENCE flag but no content store was provided")]
112 MissingContentStore,
113
114 /// A body deserialization error from `bcp-types`.
115 ///
116 /// This covers missing required fields, unknown wire types, and
117 /// invalid enum values encountered while parsing TLV fields within
118 /// a block body.
119 #[error(transparent)]
120 Type(#[from] TypeError),
121
122 /// A wire-level framing error from `bcp-wire`.
123 ///
124 /// Surfaces when a block frame's varint is malformed, the body
125 /// length exceeds the remaining bytes, or other structural issues.
126 #[error(transparent)]
127 Wire(#[from] WireError),
128
129 /// An I/O error from the underlying reader (streaming decoder).
130 #[error(transparent)]
131 Io(#[from] std::io::Error),
132}