Skip to main content

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}