Skip to main content

nodedb_array/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Typed errors for the array engine.
4//!
5//! [`ArrayError`] is the crate-internal error enum. Every variant maps
6//! into the public [`NodeDbError::array`] constructor at the API
7//! boundary via the `From` impl at the bottom of this file.
8
9use nodedb_types::NodeDbError;
10use thiserror::Error;
11
12/// Crate-internal result alias.
13pub type ArrayResult<T> = std::result::Result<T, ArrayError>;
14
15/// Crate-internal error enum.
16///
17/// Domain errors carry the offending array name where it is known; the
18/// `array` field becomes the `array` slot on the public
19/// [`NodeDbError::array`] details.
20#[derive(Debug, Error)]
21pub enum ArrayError {
22    #[error("schema validation failed for '{array}': {detail}")]
23    InvalidSchema { array: String, detail: String },
24
25    #[error("dimension '{dim}' rejected on '{array}': {detail}")]
26    InvalidDim {
27        array: String,
28        dim: String,
29        detail: String,
30    },
31
32    #[error("attribute '{attr}' rejected on '{array}': {detail}")]
33    InvalidAttr {
34        array: String,
35        attr: String,
36        detail: String,
37    },
38
39    #[error("tile-extent vector rejected on '{array}': {detail}")]
40    InvalidTileExtents { array: String, detail: String },
41
42    #[error("coordinate arity {got} does not match schema arity {expected} on '{array}'")]
43    CoordArityMismatch {
44        array: String,
45        expected: usize,
46        got: usize,
47    },
48
49    #[error("coordinate out of domain on '{array}' dim '{dim}': {detail}")]
50    CoordOutOfDomain {
51        array: String,
52        dim: String,
53        detail: String,
54    },
55
56    #[error("cell-value type mismatch on '{array}' attr '{attr}': {detail}")]
57    CellTypeMismatch {
58        array: String,
59        attr: String,
60        detail: String,
61    },
62
63    #[error("segment corruption: {detail}")]
64    SegmentCorruption { detail: String },
65
66    #[error("unsupported WAL format version: {version}")]
67    UnsupportedFormat { version: u8 },
68
69    #[error("unsupported segment format version: {version}")]
70    UnsupportedSegmentFormat { version: u16 },
71
72    /// A replica-id string could not be parsed from hex.
73    #[error("invalid replica_id: {detail}")]
74    InvalidReplicaId { detail: String },
75
76    /// An HLC value is invalid (physical_ms overflow, logical overflow, etc.).
77    #[error("invalid HLC: {detail}")]
78    InvalidHlc { detail: String },
79
80    /// The HLC generator's internal mutex was poisoned.
81    #[error("HLC generator lock poisoned")]
82    HlcLockPoisoned,
83
84    /// An `ArrayOp` violates the shape contract (e.g. `Put` without attrs).
85    #[error("invalid array op: {detail}")]
86    InvalidOp { detail: String },
87
88    /// An error returned by the Loro CRDT library.
89    #[error("loro error: {detail}")]
90    LoroError { detail: String },
91
92    /// An encrypted (`SEGA`) segment was found but no KEK was provided.
93    #[error("encrypted segment requires a KEK but none was configured")]
94    MissingKek,
95
96    /// A plaintext (`NDAS`) segment was found but a KEK was configured,
97    /// refusing to load unencrypted data when encryption is enforced.
98    #[error("plaintext segment rejected — encryption is enforced by the configured KEK")]
99    KekRequired,
100
101    /// AES-256-GCM encryption of a segment failed.
102    #[error("segment encryption failed: {detail}")]
103    EncryptionFailed { detail: String },
104
105    /// AES-256-GCM decryption or authentication of a segment failed.
106    #[error("segment decryption failed: {detail}")]
107    DecryptionFailed { detail: String },
108
109    /// The Loro snapshot envelope version does not match `LORO_FORMAT_VERSION`.
110    ///
111    /// This is a hard rejection: loading a snapshot produced by a different
112    /// (incompatible) Loro format version would silently corrupt state.
113    #[error("loro snapshot version mismatch: expected {expected}, got {got}")]
114    LoroSnapshotVersionMismatch { expected: u8, got: u8 },
115}
116
117impl ArrayError {
118    /// The array name carried by this error (for the public details slot).
119    pub fn array_name(&self) -> &str {
120        match self {
121            ArrayError::InvalidSchema { array, .. }
122            | ArrayError::InvalidDim { array, .. }
123            | ArrayError::InvalidAttr { array, .. }
124            | ArrayError::InvalidTileExtents { array, .. }
125            | ArrayError::CoordArityMismatch { array, .. }
126            | ArrayError::CoordOutOfDomain { array, .. }
127            | ArrayError::CellTypeMismatch { array, .. } => array,
128            ArrayError::SegmentCorruption { .. }
129            | ArrayError::UnsupportedFormat { .. }
130            | ArrayError::UnsupportedSegmentFormat { .. }
131            | ArrayError::InvalidReplicaId { .. }
132            | ArrayError::InvalidHlc { .. }
133            | ArrayError::HlcLockPoisoned
134            | ArrayError::InvalidOp { .. }
135            | ArrayError::LoroError { .. }
136            | ArrayError::LoroSnapshotVersionMismatch { .. }
137            | ArrayError::MissingKek
138            | ArrayError::KekRequired
139            | ArrayError::EncryptionFailed { .. }
140            | ArrayError::DecryptionFailed { .. } => "",
141        }
142    }
143}
144
145impl From<ArrayError> for NodeDbError {
146    fn from(e: ArrayError) -> Self {
147        let array = e.array_name().to_string();
148        NodeDbError::array(array, e)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn array_error_carries_name() {
158        let e = ArrayError::CoordArityMismatch {
159            array: "genome".into(),
160            expected: 4,
161            got: 3,
162        };
163        assert_eq!(e.array_name(), "genome");
164    }
165
166    #[test]
167    fn array_error_converts_to_nodedb_error() {
168        let e = ArrayError::InvalidSchema {
169            array: "vcf".into(),
170            detail: "no dimensions".into(),
171        };
172        let n: NodeDbError = e.into();
173        assert!(n.to_string().contains("NDB-1300"));
174        assert!(n.to_string().contains("vcf"));
175    }
176
177    #[test]
178    fn array_error_round_trips_through_details() {
179        let e = ArrayError::InvalidDim {
180            array: "raster".into(),
181            dim: "lat".into(),
182            detail: "lo > hi".into(),
183        };
184        let n: NodeDbError = e.into();
185        let json = serde_json::to_value(&n).unwrap();
186        assert_eq!(json["details"]["kind"], "array");
187        assert_eq!(json["details"]["array"], "raster");
188    }
189}