Skip to main content

exo_avc/
error.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Error types for the AVC layer.
18
19use thiserror::Error;
20
21/// Errors arising from AVC operations.
22///
23/// Every variant carries enough context to diagnose the failure without
24/// access to the source code. Validation denials are not errors — they
25/// flow through `AvcDecision::Deny` with reason codes. Errors here cover
26/// structural, cryptographic, and registry failures.
27#[derive(Debug, Error, Clone, PartialEq, Eq)]
28pub enum AvcError {
29    /// Canonical CBOR encoding for an AVC payload failed.
30    #[error("AVC serialization failed: {reason}")]
31    Serialization { reason: String },
32
33    /// Required string field was empty after trimming.
34    #[error("AVC field `{field}` must not be empty")]
35    EmptyField { field: &'static str },
36
37    /// Schema version is not supported by this binary.
38    #[error("AVC schema version {got} is unsupported (supported: {supported})")]
39    UnsupportedSchema { got: u16, supported: u16 },
40
41    /// Protocol version is outside the supported compatibility range.
42    #[error(
43        "AVC protocol version {got} is unsupported (supported: {min_supported}..={max_supported})"
44    )]
45    UnsupportedProtocol {
46        got: u16,
47        min_supported: u16,
48        max_supported: u16,
49    },
50
51    /// A basis point value was outside the legal `0..=10_000` range.
52    #[error("AVC basis point field `{field}` value {value} exceeds 10_000")]
53    BasisPointOutOfRange { field: &'static str, value: u32 },
54
55    /// A timestamp invariant was violated (e.g. expired-on-issue).
56    #[error("AVC timestamp invariant violated: {reason}")]
57    InvalidTimestamp { reason: String },
58
59    /// Delegation widened scope of any kind.
60    #[error("AVC delegation rejected: scope widened in `{dimension}`")]
61    DelegationWidens { dimension: &'static str },
62
63    /// Delegation chain was rejected for a non-widening structural reason.
64    #[error("AVC delegation rejected: {reason}")]
65    DelegationRejected { reason: String },
66
67    /// Registry write conflict (e.g. duplicate revocation or unknown key).
68    #[error("AVC registry error: {reason}")]
69    Registry { reason: String },
70
71    /// Invalid input was supplied to a public function.
72    #[error("AVC invalid input: {reason}")]
73    InvalidInput { reason: String },
74}
75
76impl<T> From<ciborium::ser::Error<T>> for AvcError {
77    fn from(_: ciborium::ser::Error<T>) -> Self {
78        AvcError::Serialization {
79            reason: "CBOR serialization failed".into(),
80        }
81    }
82}
83
84impl<T> From<ciborium::de::Error<T>> for AvcError {
85    fn from(_: ciborium::de::Error<T>) -> Self {
86        AvcError::Serialization {
87            reason: "CBOR deserialization failed".into(),
88        }
89    }
90}
91
92impl From<exo_core::ExoError> for AvcError {
93    fn from(value: exo_core::ExoError) -> Self {
94        match value {
95            exo_core::ExoError::SerializationError { reason } => AvcError::Serialization { reason },
96            other => AvcError::InvalidInput {
97                reason: other.to_string(),
98            },
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn display_covers_every_variant() {
109        let cases: Vec<AvcError> = vec![
110            AvcError::Serialization {
111                reason: "cbor".into(),
112            },
113            AvcError::EmptyField { field: "purpose" },
114            AvcError::UnsupportedSchema {
115                got: 99,
116                supported: 1,
117            },
118            AvcError::UnsupportedProtocol {
119                got: 99,
120                min_supported: 1,
121                max_supported: 1,
122            },
123            AvcError::BasisPointOutOfRange {
124                field: "risk",
125                value: 99_999,
126            },
127            AvcError::InvalidTimestamp {
128                reason: "expired".into(),
129            },
130            AvcError::DelegationWidens {
131                dimension: "permissions",
132            },
133            AvcError::DelegationRejected {
134                reason: "depth".into(),
135            },
136            AvcError::Registry {
137                reason: "missing".into(),
138            },
139            AvcError::InvalidInput {
140                reason: "bad".into(),
141            },
142        ];
143        for err in cases {
144            let s = err.to_string();
145            assert!(!s.is_empty(), "error display empty for {err:?}");
146        }
147    }
148
149    #[test]
150    fn from_exo_error_serialization_preserves_reason() {
151        let inner = exo_core::ExoError::SerializationError {
152            reason: "boom".into(),
153        };
154        let mapped: AvcError = inner.into();
155        match mapped {
156            AvcError::Serialization { reason } => assert_eq!(reason, "boom"),
157            other => panic!("expected Serialization, got {other:?}"),
158        }
159    }
160
161    #[test]
162    fn from_exo_error_other_maps_to_invalid_input() {
163        let inner = exo_core::ExoError::InvalidMerkleProof;
164        let mapped: AvcError = inner.into();
165        match mapped {
166            AvcError::InvalidInput { reason } => assert!(reason.contains("invalid merkle proof")),
167            other => panic!("expected InvalidInput, got {other:?}"),
168        }
169    }
170
171    #[test]
172    fn ciborium_serialization_error_maps_to_serialization_variant() {
173        let inner: ciborium::ser::Error<std::io::Error> = ciborium::ser::Error::Value("bad".into());
174        let mapped: AvcError = inner.into();
175        assert!(matches!(mapped, AvcError::Serialization { .. }));
176    }
177
178    #[test]
179    fn ciborium_deserialization_error_maps_to_serialization_variant() {
180        let inner: ciborium::de::Error<std::io::Error> =
181            ciborium::de::Error::Semantic(None, "bad".into());
182        let mapped: AvcError = inner.into();
183        assert!(matches!(mapped, AvcError::Serialization { .. }));
184    }
185
186    #[test]
187    fn clone_eq_debug() {
188        let a = AvcError::EmptyField { field: "purpose" };
189        let b = a.clone();
190        assert_eq!(a, b);
191        assert!(format!("{a:?}").contains("EmptyField"));
192    }
193}