1use std::fmt;
2
3#[derive(Clone, Copy, PartialEq, Eq, Hash)]
5pub struct Hash([u8; 32]);
6
7impl Hash {
8 pub fn from_bytes(bytes: &[u8; 32]) -> Self {
10 Hash(*bytes)
11 }
12
13 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
15 if bytes.len() < 32 {
16 return Err(AreevError::Format(format!(
17 "hash requires 32 bytes, got {}",
18 bytes.len()
19 )));
20 }
21 let mut arr = [0u8; 32];
22 arr.copy_from_slice(&bytes[..32]);
23 Ok(Hash(arr))
24 }
25
26 pub fn from_hex(hex_str: &str) -> Result<Self> {
27 let bytes = hex::decode(hex_str)
28 .map_err(|e| AreevError::Format(format!("invalid hex hash: {}", e)))?;
29 if bytes.len() != 32 {
30 return Err(AreevError::Format(format!(
31 "hash must be 32 bytes, got {}",
32 bytes.len()
33 )));
34 }
35 Ok(Self::from_bytes(&bytes.try_into().unwrap()))
36 }
37
38 pub fn as_bytes(&self) -> &[u8; 32] {
39 &self.0
40 }
41
42 pub fn to_hex(&self) -> String {
43 hex::encode(self.0)
44 }
45}
46
47impl fmt::Debug for Hash {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "Hash({})", &self.to_hex()[..16])
50 }
51}
52
53impl fmt::Display for Hash {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{}", self.to_hex())
56 }
57}
58
59impl serde::Serialize for Hash {
60 fn serialize<S: serde::Serializer>(
61 &self,
62 serializer: S,
63 ) -> std::result::Result<S::Ok, S::Error> {
64 serializer.serialize_str(&self.to_hex())
65 }
66}
67
68impl<'de> serde::Deserialize<'de> for Hash {
69 fn deserialize<D: serde::Deserializer<'de>>(
70 deserializer: D,
71 ) -> std::result::Result<Self, D::Error> {
72 let s = String::deserialize(deserializer)?;
73 Hash::from_hex(&s).map_err(serde::de::Error::custom)
74 }
75}
76
77#[derive(Debug)]
79pub enum AreevError {
80 NotFound(Hash),
81 Format(String),
82 Validation(String),
83 Serialization(String),
84 ToolRenderUnsupported(String),
85 Storage(String),
86 StoreBusy(String),
89 TlsUnavailable(String),
95 ReadOnly(String),
103 ReadOnlyOpenFailed(String),
113 SchemaNotProvisioned(String),
125 LegalHold(String),
135 SupersessionConflict(Hash),
136 SupersessionChainTooDeep(Hash),
142 AnnIndexUnsupported(String),
148 CryptoError(String),
149 AttestationInvalid(String),
154 AttestationRequired(String),
157 SigningKeyInvalid(String),
159 AccumulateRetryExhausted,
160 AccumulateInternal(String),
161 AccumulateBackpressureRejected,
162 Internal(String),
163 AuthzDenied(String),
165 AuthzUnknownPrincipal(String),
167 AuthzConfigInvalid(String),
169 AuthzTokenUnrecognized,
172}
173
174impl AreevError {
175 pub fn code(&self) -> &'static str {
181 match self {
182 Self::NotFound(_) => "MEM-E001",
183 Self::SupersessionConflict(_) => "MEM-E002",
184 Self::SupersessionChainTooDeep(_) => "STO-E006",
185 Self::AnnIndexUnsupported(_) => "STO-E007",
186 Self::ToolRenderUnsupported(_) => "MEM-E110",
187 Self::Format(_) => "FMT-E001",
188 Self::Serialization(_) => "FMT-E002",
189 Self::Validation(_) => "VAL-E001",
190 Self::Storage(_) => "STO-E001",
191 Self::StoreBusy(_) => "STO-E002",
192 Self::TlsUnavailable(_) => "STO-E003",
193 Self::ReadOnly(_) => "STO-E004",
194 Self::ReadOnlyOpenFailed(_) => "STO-E005",
195 Self::SchemaNotProvisioned(_) => "STO-E008",
196 Self::LegalHold(_) => "STO-E009",
197 Self::CryptoError(_) => "CRY-E001",
198 Self::AttestationInvalid(_) => "CRY-E002",
199 Self::AttestationRequired(_) => "CRY-E003",
200 Self::SigningKeyInvalid(_) => "CRY-E004",
201 Self::AccumulateRetryExhausted => "CAL-E083",
204 Self::AccumulateInternal(_) => "CAL-E084",
205 Self::AccumulateBackpressureRejected => "CAL-E085",
206 Self::Internal(_) => "SYS-E001",
207 Self::AuthzDenied(_) => "AUT-E001",
208 Self::AuthzUnknownPrincipal(_) => "AUT-E002",
209 Self::AuthzConfigInvalid(_) => "AUT-E003",
210 Self::AuthzTokenUnrecognized => "AUT-E004",
211 }
212 }
213}
214
215impl std::fmt::Display for AreevError {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 match self {
220 Self::NotFound(h) => write!(f, "MEM-E001: grain not found: {h}"),
221 Self::SupersessionConflict(h) => write!(f, "MEM-E002: already superseded: {h}"),
222 Self::SupersessionChainTooDeep(h) => write!(
223 f,
224 "STO-E006: supersession chain from {h} did not terminate within the bounded walk — the supersedes links may be cyclic or corrupt"
225 ),
226 Self::AnnIndexUnsupported(m) => write!(
227 f,
228 "STO-E007: no approximate vector index on this backend: {m}"
229 ),
230 Self::ToolRenderUnsupported(m) => write!(f, "MEM-E110: tool render unsupported: {m}"),
231 Self::Format(m) => write!(f, "FMT-E001: format error: {m}"),
232 Self::Serialization(m) => write!(f, "FMT-E002: serialization error: {m}"),
233 Self::Validation(m) => write!(f, "VAL-E001: validation error: {m}"),
234 Self::Storage(m) => write!(f, "STO-E001: storage error: {m}"),
235 Self::StoreBusy(m) => write!(f, "STO-E002: store busy: {m}"),
236 Self::TlsUnavailable(m) => write!(f, "STO-E003: {m}"),
237 Self::ReadOnly(m) => write!(f, "STO-E004: refusing write on a read-only memory: {m}"),
238 Self::ReadOnlyOpenFailed(m) => write!(f, "STO-E005: {m}"),
239 Self::SchemaNotProvisioned(m) => write!(f, "STO-E008: {m}"),
240 Self::LegalHold(m) => write!(f, "STO-E009: {m}"),
241 Self::CryptoError(m) => write!(f, "CRY-E001: crypto error: {m}"),
242 Self::AttestationInvalid(m) => write!(f, "CRY-E002: attestation invalid: {m}"),
243 Self::AttestationRequired(m) => write!(f, "CRY-E003: attestation required: {m}"),
244 Self::SigningKeyInvalid(m) => write!(f, "CRY-E004: signing key invalid: {m}"),
245 Self::AccumulateRetryExhausted => write!(f, "CAL-E083: ACCUMULATE retry budget exhausted"),
246 Self::AccumulateInternal(m) => write!(f, "CAL-E084: ACCUMULATE internal failure: {m}"),
247 Self::AccumulateBackpressureRejected => write!(f, "CAL-E085: ACCUMULATE backpressure: inflight cap exceeded"),
248 Self::Internal(m) => write!(f, "SYS-E001: internal error: {m}"),
249 Self::AuthzDenied(m) => write!(f, "AUT-E001: authorization denied: {m}"),
250 Self::AuthzUnknownPrincipal(p) => write!(f, "AUT-E002: unknown principal: {p}"),
251 Self::AuthzConfigInvalid(m) => write!(f, "AUT-E003: {m}"),
252 Self::AuthzTokenUnrecognized => write!(f, "AUT-E004: token not recognized"),
253 }
254 }
255}
256
257impl std::error::Error for AreevError {}
258
259pub type Result<T> = std::result::Result<T, AreevError>;
260
261#[cfg(test)]
262mod error_code_tests {
263 use super::*;
264
265 fn all_variants() -> Vec<AreevError> {
267 let h = Hash::from_bytes(&[0u8; 32]);
268 vec![
269 AreevError::NotFound(h),
270 AreevError::SupersessionConflict(h),
271 AreevError::SupersessionChainTooDeep(h),
272 AreevError::AnnIndexUnsupported("x".into()),
273 AreevError::AttestationInvalid("x".into()),
274 AreevError::AttestationRequired("x".into()),
275 AreevError::SigningKeyInvalid("x".into()),
276 AreevError::ToolRenderUnsupported("x".into()),
277 AreevError::Format("x".into()),
278 AreevError::Serialization("x".into()),
279 AreevError::Validation("x".into()),
280 AreevError::Storage("x".into()),
281 AreevError::StoreBusy("x".into()),
282 AreevError::TlsUnavailable("x".into()),
283 AreevError::ReadOnly("x".into()),
284 AreevError::ReadOnlyOpenFailed("x".into()),
285 AreevError::SchemaNotProvisioned("x".into()),
286 AreevError::LegalHold("x".into()),
287 AreevError::CryptoError("x".into()),
288 AreevError::AccumulateRetryExhausted,
289 AreevError::AccumulateInternal("x".into()),
290 AreevError::AccumulateBackpressureRejected,
291 AreevError::Internal("x".into()),
292 AreevError::AuthzDenied("x".into()),
293 AreevError::AuthzUnknownPrincipal("x".into()),
294 AreevError::AuthzConfigInvalid("x".into()),
295 AreevError::AuthzTokenUnrecognized,
296 ]
297 }
298
299 #[test]
302 fn code_prefixes_every_display() {
303 for e in all_variants() {
304 let msg = e.to_string();
305 let code = e.code();
306 assert!(
307 msg.starts_with(&format!("{code}: ")),
308 "`{msg}` must start with its code `{code}`"
309 );
310 }
311 }
312
313 #[test]
316 fn codes_follow_the_repo_standard() {
317 for e in all_variants() {
318 let c = e.code();
319 let (domain, num) = c.split_once("-E").unwrap_or_else(|| panic!("bad code: {c}"));
320 assert_eq!(domain.len(), 3, "{c}: domain must be 3 letters");
321 assert!(domain.chars().all(|ch| ch.is_ascii_uppercase()), "{c}: domain uppercase");
322 assert!(!num.is_empty() && num.chars().all(|ch| ch.is_ascii_digit()), "{c}: numeric suffix");
323 }
324 }
325}