Skip to main content

grc_20/
error.rs

1//! Error types for GRC-20 encoding/decoding and validation.
2
3use thiserror::Error;
4
5use crate::model::{DataType, Id};
6
7/// Error codes as defined in spec Section 8.3.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ErrorCode {
10    /// E001: Invalid magic/version
11    InvalidMagicOrVersion,
12    /// E002: Index out of bounds
13    IndexOutOfBounds,
14    /// E003: Invalid signature
15    InvalidSignature,
16    /// E004: Invalid UTF-8 encoding
17    InvalidUtf8,
18    /// E005: Malformed varint/length/reserved bits/encoding
19    MalformedEncoding,
20}
21
22impl ErrorCode {
23    /// Returns the error code string (e.g., "E001").
24    pub fn code(&self) -> &'static str {
25        match self {
26            ErrorCode::InvalidMagicOrVersion => "E001",
27            ErrorCode::IndexOutOfBounds => "E002",
28            ErrorCode::InvalidSignature => "E003",
29            ErrorCode::InvalidUtf8 => "E004",
30            ErrorCode::MalformedEncoding => "E005",
31        }
32    }
33}
34
35/// Error during binary decoding.
36#[derive(Debug, Clone, PartialEq, Error)]
37pub enum DecodeError {
38    // === E001: Invalid magic/version ===
39    #[error("[E001] invalid magic bytes: expected GRC2 or GRC2Z, found {found:?}")]
40    InvalidMagic { found: [u8; 4] },
41
42    #[error("[E001] unsupported version: {version}")]
43    UnsupportedVersion { version: u8 },
44
45    // === E002: Index out of bounds ===
46    #[error("[E002] {dict} index {index} out of bounds (size: {size})")]
47    IndexOutOfBounds {
48        dict: &'static str,
49        index: usize,
50        size: usize,
51    },
52
53    // === E004: Invalid UTF-8 ===
54    #[error("[E004] invalid UTF-8 in {field}")]
55    InvalidUtf8 { field: &'static str },
56
57    // === E005: Malformed encoding ===
58    #[error("[E005] unexpected end of input while reading {context}")]
59    UnexpectedEof { context: &'static str },
60
61    #[error("[E005] varint exceeds maximum length (10 bytes)")]
62    VarintTooLong,
63
64    #[error("[E005] varint overflow (value exceeds u64)")]
65    VarintOverflow,
66
67    #[error("[E005] {field} length {len} exceeds maximum {max}")]
68    LengthExceedsLimit {
69        field: &'static str,
70        len: usize,
71        max: usize,
72    },
73
74    #[error("[E005] invalid op type: {op_type}")]
75    InvalidOpType { op_type: u8 },
76
77    #[error("[E005] invalid data type: {data_type}")]
78    InvalidDataType { data_type: u8 },
79
80    #[error("[E005] invalid embedding sub-type: {sub_type}")]
81    InvalidEmbeddingSubType { sub_type: u8 },
82
83    #[error("[E005] invalid bool value: {value} (expected 0x00 or 0x01)")]
84    InvalidBool { value: u8 },
85
86    #[error("[E005] reserved bits are non-zero in {context}")]
87    ReservedBitsSet { context: &'static str },
88
89    #[error("[E005] POINT latitude {lat} out of range [-90, +90]")]
90    LatitudeOutOfRange { lat: f64 },
91
92    #[error("[E005] POINT longitude {lon} out of range [-180, +180]")]
93    LongitudeOutOfRange { lon: f64 },
94
95    #[error("[E005] position string contains invalid character: {char:?}")]
96    InvalidPositionChar { char: char },
97
98    #[error("[E005] position string length {len} exceeds maximum 64")]
99    PositionTooLong { len: usize },
100
101    #[error("[E005] embedding data length {actual} doesn't match expected {expected} for {dims} dims")]
102    EmbeddingDataMismatch {
103        dims: usize,
104        expected: usize,
105        actual: usize,
106    },
107
108    #[error("[E005] DECIMAL has trailing zeros in mantissa (not normalized)")]
109    DecimalNotNormalized,
110
111    #[error("[E005] DECIMAL mantissa bytes are not minimal")]
112    DecimalMantissaNotMinimal,
113
114    #[error("[E005] float value is NaN")]
115    FloatIsNan,
116
117    #[error("[E005] malformed encoding: {context}")]
118    MalformedEncoding { context: &'static str },
119
120    // === Compression errors ===
121    #[error("[E005] zstd decompression failed: {0}")]
122    DecompressionFailed(String),
123
124    #[error("[E005] decompressed size {actual} doesn't match declared {declared}")]
125    UncompressedSizeMismatch { declared: usize, actual: usize },
126
127    #[error("[E005] duplicate ID in {dict} dictionary: {id:?}")]
128    DuplicateDictionaryEntry { dict: &'static str, id: Id },
129}
130
131impl DecodeError {
132    /// Returns the error code for this error.
133    pub fn code(&self) -> ErrorCode {
134        match self {
135            DecodeError::InvalidMagic { .. } | DecodeError::UnsupportedVersion { .. } => {
136                ErrorCode::InvalidMagicOrVersion
137            }
138            DecodeError::IndexOutOfBounds { .. } => ErrorCode::IndexOutOfBounds,
139            DecodeError::InvalidUtf8 { .. } => ErrorCode::InvalidUtf8,
140            _ => ErrorCode::MalformedEncoding,
141        }
142    }
143}
144
145/// Error during binary encoding.
146#[derive(Debug, Clone, PartialEq, Error)]
147pub enum EncodeError {
148    #[error("{field} length {len} exceeds maximum {max}")]
149    LengthExceedsLimit {
150        field: &'static str,
151        len: usize,
152        max: usize,
153    },
154
155    #[error("embedding data length {data_len} doesn't match {dims} dims for sub-type {sub_type:?}")]
156    EmbeddingDimensionMismatch {
157        sub_type: u8,
158        dims: usize,
159        data_len: usize,
160    },
161
162    #[error("zstd compression failed: {0}")]
163    CompressionFailed(String),
164
165    #[error("DECIMAL value is not normalized (has trailing zeros)")]
166    DecimalNotNormalized,
167
168    #[error("float value is NaN")]
169    FloatIsNan,
170
171    #[error("POINT latitude {lat} out of range [-90, +90]")]
172    LatitudeOutOfRange { lat: f64 },
173
174    #[error("POINT longitude {lon} out of range [-180, +180]")]
175    LongitudeOutOfRange { lon: f64 },
176
177    #[error("position string contains invalid character")]
178    InvalidPositionChar,
179
180    #[error("position string length exceeds maximum 64")]
181    PositionTooLong,
182
183    #[error("DATE string is not valid ISO 8601: {reason}")]
184    InvalidDate { reason: &'static str },
185
186    #[error("batch entity has {actual} values but schema requires {expected}")]
187    BatchEntityValueCountMismatch { expected: usize, actual: usize },
188
189    #[error("invalid input: {context}")]
190    InvalidInput { context: &'static str },
191
192    #[error("duplicate author ID in canonical mode: {id:?}")]
193    DuplicateAuthor { id: Id },
194
195    #[error("duplicate value (property={property:?}, language={language:?}) in canonical mode")]
196    DuplicateValue { property: Id, language: Option<Id> },
197
198    #[error("duplicate unset property (property={property:?}, language={language:?}) in canonical mode")]
199    DuplicateUnset { property: Id, language: Option<Id> },
200}
201
202/// Error during semantic validation.
203#[derive(Debug, Clone, PartialEq, Error)]
204pub enum ValidationError {
205    #[error("value type mismatch for property {property:?}: expected {expected:?}")]
206    TypeMismatch { property: Id, expected: DataType },
207
208    #[error("entity {entity:?} is dead (tombstoned)")]
209    EntityIsDead { entity: Id },
210
211    #[error("relation {relation:?} is dead (tombstoned)")]
212    RelationIsDead { relation: Id },
213
214    #[error("property {property:?} not found in schema")]
215    PropertyNotFound { property: Id },
216
217    #[error("data type mismatch for property {property:?}: schema says {schema:?}, edit declares {declared:?}")]
218    DataTypeInconsistent {
219        property: Id,
220        schema: DataType,
221        declared: DataType,
222    },
223}