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 SupersessionConflict(Hash),
126 SupersessionChainTooDeep(Hash),
132 AnnIndexUnsupported(String),
138 CryptoError(String),
139 AttestationInvalid(String),
144 AttestationRequired(String),
147 SigningKeyInvalid(String),
149 AccumulateRetryExhausted,
150 AccumulateInternal(String),
151 AccumulateBackpressureRejected,
152 Internal(String),
153 AuthzDenied(String),
155 AuthzUnknownPrincipal(String),
157 AuthzConfigInvalid(String),
159 AuthzTokenUnrecognized,
162}
163
164impl AreevError {
165 pub fn code(&self) -> &'static str {
171 match self {
172 Self::NotFound(_) => "MEM-E001",
173 Self::SupersessionConflict(_) => "MEM-E002",
174 Self::SupersessionChainTooDeep(_) => "STO-E006",
175 Self::AnnIndexUnsupported(_) => "STO-E007",
176 Self::ToolRenderUnsupported(_) => "MEM-E110",
177 Self::Format(_) => "FMT-E001",
178 Self::Serialization(_) => "FMT-E002",
179 Self::Validation(_) => "VAL-E001",
180 Self::Storage(_) => "STO-E001",
181 Self::StoreBusy(_) => "STO-E002",
182 Self::TlsUnavailable(_) => "STO-E003",
183 Self::ReadOnly(_) => "STO-E004",
184 Self::ReadOnlyOpenFailed(_) => "STO-E005",
185 Self::SchemaNotProvisioned(_) => "STO-E008",
186 Self::CryptoError(_) => "CRY-E001",
187 Self::AttestationInvalid(_) => "CRY-E002",
188 Self::AttestationRequired(_) => "CRY-E003",
189 Self::SigningKeyInvalid(_) => "CRY-E004",
190 Self::AccumulateRetryExhausted => "CAL-E083",
193 Self::AccumulateInternal(_) => "CAL-E084",
194 Self::AccumulateBackpressureRejected => "CAL-E085",
195 Self::Internal(_) => "SYS-E001",
196 Self::AuthzDenied(_) => "AUT-E001",
197 Self::AuthzUnknownPrincipal(_) => "AUT-E002",
198 Self::AuthzConfigInvalid(_) => "AUT-E003",
199 Self::AuthzTokenUnrecognized => "AUT-E004",
200 }
201 }
202}
203
204impl std::fmt::Display for AreevError {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 match self {
209 Self::NotFound(h) => write!(f, "MEM-E001: grain not found: {h}"),
210 Self::SupersessionConflict(h) => write!(f, "MEM-E002: already superseded: {h}"),
211 Self::SupersessionChainTooDeep(h) => write!(
212 f,
213 "STO-E006: supersession chain from {h} did not terminate within the bounded walk — the supersedes links may be cyclic or corrupt"
214 ),
215 Self::AnnIndexUnsupported(m) => write!(
216 f,
217 "STO-E007: no approximate vector index on this backend: {m}"
218 ),
219 Self::ToolRenderUnsupported(m) => write!(f, "MEM-E110: tool render unsupported: {m}"),
220 Self::Format(m) => write!(f, "FMT-E001: format error: {m}"),
221 Self::Serialization(m) => write!(f, "FMT-E002: serialization error: {m}"),
222 Self::Validation(m) => write!(f, "VAL-E001: validation error: {m}"),
223 Self::Storage(m) => write!(f, "STO-E001: storage error: {m}"),
224 Self::StoreBusy(m) => write!(f, "STO-E002: store busy: {m}"),
225 Self::TlsUnavailable(m) => write!(f, "STO-E003: {m}"),
226 Self::ReadOnly(m) => write!(f, "STO-E004: refusing write on a read-only memory: {m}"),
227 Self::ReadOnlyOpenFailed(m) => write!(f, "STO-E005: {m}"),
228 Self::SchemaNotProvisioned(m) => write!(f, "STO-E008: {m}"),
229 Self::CryptoError(m) => write!(f, "CRY-E001: crypto error: {m}"),
230 Self::AttestationInvalid(m) => write!(f, "CRY-E002: attestation invalid: {m}"),
231 Self::AttestationRequired(m) => write!(f, "CRY-E003: attestation required: {m}"),
232 Self::SigningKeyInvalid(m) => write!(f, "CRY-E004: signing key invalid: {m}"),
233 Self::AccumulateRetryExhausted => write!(f, "CAL-E083: ACCUMULATE retry budget exhausted"),
234 Self::AccumulateInternal(m) => write!(f, "CAL-E084: ACCUMULATE internal failure: {m}"),
235 Self::AccumulateBackpressureRejected => write!(f, "CAL-E085: ACCUMULATE backpressure: inflight cap exceeded"),
236 Self::Internal(m) => write!(f, "SYS-E001: internal error: {m}"),
237 Self::AuthzDenied(m) => write!(f, "AUT-E001: authorization denied: {m}"),
238 Self::AuthzUnknownPrincipal(p) => write!(f, "AUT-E002: unknown principal: {p}"),
239 Self::AuthzConfigInvalid(m) => write!(f, "AUT-E003: {m}"),
240 Self::AuthzTokenUnrecognized => write!(f, "AUT-E004: token not recognized"),
241 }
242 }
243}
244
245impl std::error::Error for AreevError {}
246
247pub type Result<T> = std::result::Result<T, AreevError>;
248
249#[cfg(test)]
250mod error_code_tests {
251 use super::*;
252
253 fn all_variants() -> Vec<AreevError> {
255 let h = Hash::from_bytes(&[0u8; 32]);
256 vec![
257 AreevError::NotFound(h),
258 AreevError::SupersessionConflict(h),
259 AreevError::SupersessionChainTooDeep(h),
260 AreevError::AnnIndexUnsupported("x".into()),
261 AreevError::AttestationInvalid("x".into()),
262 AreevError::AttestationRequired("x".into()),
263 AreevError::SigningKeyInvalid("x".into()),
264 AreevError::ToolRenderUnsupported("x".into()),
265 AreevError::Format("x".into()),
266 AreevError::Serialization("x".into()),
267 AreevError::Validation("x".into()),
268 AreevError::Storage("x".into()),
269 AreevError::StoreBusy("x".into()),
270 AreevError::TlsUnavailable("x".into()),
271 AreevError::ReadOnly("x".into()),
272 AreevError::ReadOnlyOpenFailed("x".into()),
273 AreevError::SchemaNotProvisioned("x".into()),
274 AreevError::CryptoError("x".into()),
275 AreevError::AccumulateRetryExhausted,
276 AreevError::AccumulateInternal("x".into()),
277 AreevError::AccumulateBackpressureRejected,
278 AreevError::Internal("x".into()),
279 AreevError::AuthzDenied("x".into()),
280 AreevError::AuthzUnknownPrincipal("x".into()),
281 AreevError::AuthzConfigInvalid("x".into()),
282 AreevError::AuthzTokenUnrecognized,
283 ]
284 }
285
286 #[test]
289 fn code_prefixes_every_display() {
290 for e in all_variants() {
291 let msg = e.to_string();
292 let code = e.code();
293 assert!(
294 msg.starts_with(&format!("{code}: ")),
295 "`{msg}` must start with its code `{code}`"
296 );
297 }
298 }
299
300 #[test]
303 fn codes_follow_the_repo_standard() {
304 for e in all_variants() {
305 let c = e.code();
306 let (domain, num) = c.split_once("-E").unwrap_or_else(|| panic!("bad code: {c}"));
307 assert_eq!(domain.len(), 3, "{c}: domain must be 3 letters");
308 assert!(domain.chars().all(|ch| ch.is_ascii_uppercase()), "{c}: domain uppercase");
309 assert!(!num.is_empty() && num.chars().all(|ch| ch.is_ascii_digit()), "{c}: numeric suffix");
310 }
311 }
312}