Skip to main content

galata_vault/
error.rs

1//! One error type, with stable codes. The Python package reports the same
2//! code for the same failure, from the same table ([`kind_of`]).
3//!
4//! [`Error`] is an enum: match on the variant for structured detail (a
5//! conflict's versions, the refused path's suggestion, the integrity
6//! failure), or on [`Error::code`] for the stable string, which is exactly
7//! what this crate reported before the enum existed.
8//!
9//! Messages are built from allow-listed parts: the path or label, a name the
10//! caller used, versions and failure kinds. Never a token, key material, a
11//! value or a config body. An error's `Display` is its whole message;
12//! [`std::error::Error::source`] returns the underlying transport or store
13//! cause where there is one.
14
15use std::sync::Arc;
16
17use crate::client::{ApiError, Pre};
18use crate::keys::KeyError;
19use crate::proto::api::ErrorCode;
20use crate::proto::audit::ChainError;
21use crate::proto::integrity::IntegrityError;
22use crate::seal::SealError;
23
24use crate::store::StoreError;
25
26/// The codes this crate raises itself, beside the server's own
27/// (`token_expired`, `precondition_failed`, …).
28pub mod code {
29    /// A token string that does not parse or whose checksum does not match.
30    pub const INVALID_TOKEN: &str = "invalid_token";
31    /// A token file that is unreadable, too permissive, or holds more than
32    /// one token.
33    pub const INVALID_TOKEN_FILE: &str = "invalid_token_file";
34    /// A server URL that is not `https://`, or `http://` to loopback.
35    pub const INVALID_SERVER: &str = "invalid_server";
36    /// A proxy URL that does not parse.
37    pub const INVALID_PROXY: &str = "invalid_proxy";
38    /// `GV_SERVER` is not set.
39    pub const MISSING_SERVER: &str = "missing_server";
40    /// The environment names both token sources, or neither.
41    pub const INVALID_ENVIRONMENT: &str = "invalid_environment";
42    /// An empty name, or one in the reserved `gv:` namespace.
43    pub const INVALID_NAME: &str = "invalid_name";
44    /// A config body that does not parse in its declared format.
45    pub const INVALID_CONFIG: &str = "invalid_config";
46    /// An audit head that is not one this crate returned.
47    pub const INVALID_AUDIT_HEAD: &str = "invalid_audit_head";
48    /// A credential literal in a config body.
49    pub const CREDENTIAL_LITERAL: &str = "credential_literal";
50    /// A config format the operation does not support.
51    pub const UNSUPPORTED_FORMAT: &str = "unsupported_format";
52    /// A config body that is not UTF-8.
53    pub const NOT_TEXT: &str = "not_text";
54    /// No such record, version, vault or token.
55    pub const NOT_FOUND: &str = "not_found";
56    /// This credential may not do it.
57    pub const FORBIDDEN: &str = "forbidden";
58    /// Another writer won; nothing was overwritten.
59    pub const CONFLICT: &str = "conflict";
60    /// The vault was rotated underneath the handle: refresh it and retry.
61    pub const STALE_GENERATION: &str = "stale_generation";
62    /// The server could not be reached.
63    pub const UNREACHABLE: &str = "unreachable";
64    /// Any other failure.
65    pub const ERROR: &str = "error";
66    /// A descriptor, bundle, record or children signature does not verify.
67    pub const BAD_SIGNATURE: &str = "bad_signature";
68    /// A key does not match the pinned vault or its descriptor.
69    pub const KEY_MISMATCH: &str = "key_mismatch";
70    /// A signed or encrypted binding disagrees with what the server reported.
71    pub const BINDING_MISMATCH: &str = "binding_mismatch";
72    /// The server showed an older record version than this client saw.
73    pub const VERSION_ROLLBACK: &str = "version_rollback";
74    /// The server showed an older or different generation than this client saw.
75    pub const GENERATION_ROLLBACK: &str = "generation_rollback";
76    /// The audit chain does not verify against the known head.
77    pub const AUDIT_MISMATCH: &str = "audit_mismatch";
78    /// A key store or state store failed.
79    pub const STORE: &str = "store";
80    /// A path absent from the known tree (the error carries a suggestion).
81    pub const UNKNOWN_PATH: &str = "unknown_path";
82    /// A string that is not an environment path or project name.
83    pub const INVALID_PATH: &str = "invalid_path";
84    /// A kit file that does not parse, or is of the wrong kind.
85    pub const INVALID_KIT: &str = "invalid_kit";
86    /// No project of that name is configured in local state.
87    pub const UNKNOWN_PROJECT: &str = "unknown_project";
88    /// A project of that name is already configured.
89    pub const PROJECT_EXISTS: &str = "project_exists";
90    /// The environment already exists.
91    pub const ENV_EXISTS: &str = "env_exists";
92    /// The path is a project, where an environment was expected.
93    pub const IS_PROJECT: &str = "is_project";
94    /// The environment has environments below it.
95    pub const HAS_CHILDREN: &str = "has_children";
96    /// The server already has a vault for this key.
97    pub const VAULT_EXISTS: &str = "vault_exists";
98    /// No key is held for the path or any node above it.
99    pub const NO_KEY: &str = "no_key";
100    /// A kit or local project is pinned to another server or vault.
101    pub const SERVER_MISMATCH: &str = "server_mismatch";
102    /// A rekey is in progress and must be resumed or aborted first.
103    pub const REKEY_IN_PROGRESS: &str = "rekey_in_progress";
104    /// No rekey is in progress (or not for that path).
105    pub const NO_REKEY: &str = "no_rekey";
106    /// The rekey's parent already points at the new key: it can only resume.
107    pub const REKEY_FORWARD_ONLY: &str = "rekey_forward_only";
108    /// The data directory is open in another process or handle: a `gv-server
109    /// local`, or another embedded opener (feature `embedded`).
110    pub const DATA_DIR_IN_USE: &str = "data_dir_in_use";
111    /// A data directory that is not a directory, is reachable by its group or
112    /// others, or cannot be read (feature `embedded`).
113    pub const INVALID_DATA_DIR: &str = "invalid_data_dir";
114    /// The server's capabilities do not list protocol 2: nothing
115    /// authenticated was sent to it.
116    pub const UNSUPPORTED_PROTOCOL: &str = "unsupported_protocol";
117    /// The operation depends on a value this client does not know (a newer
118    /// server's token scope, audit action or actor, children entry mode):
119    /// it was refused and nothing changed. Upgrade the client.
120    pub const UNSUPPORTED_BY_CLIENT: &str = "unsupported_by_client";
121}
122
123/// What kind of failure a code is. The Python package raises one exception
124/// class per kind.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126#[non_exhaustive]
127pub enum ErrorKind {
128    /// The token is malformed, unknown, revoked or expired, or its vault
129    /// expired.
130    Auth,
131    /// The credential's scope or allow-list does not permit it.
132    Forbidden,
133    /// No such record, version, or the record was deleted.
134    NotFound,
135    /// Another writer won; nothing was overwritten.
136    Conflict,
137    /// The server could not be reached.
138    Transport,
139    /// Refused on this side before any request: a bad argument, file,
140    /// environment, document or local state.
141    Invalid,
142    /// Something the server served does not verify. Treat the server's
143    /// answer as untrustworthy.
144    Integrity,
145    /// Anything else.
146    Other,
147}
148
149impl ErrorKind {
150    /// The kind's name, as the Python package sees it.
151    pub fn as_str(self) -> &'static str {
152        match self {
153            ErrorKind::Auth => "auth",
154            ErrorKind::Forbidden => "forbidden",
155            ErrorKind::NotFound => "not_found",
156            ErrorKind::Conflict => "conflict",
157            ErrorKind::Transport => "transport",
158            ErrorKind::Invalid => "invalid",
159            ErrorKind::Integrity => "integrity",
160            ErrorKind::Other => "error",
161        }
162    }
163}
164
165/// The kind a code belongs to: one table for Rust and Python.
166///
167/// A code the protocol defines is classified from [`ErrorCode`] itself, so
168/// renaming a variant there is a compile error here rather than a refusal
169/// that quietly becomes [`ErrorKind::Other`]. Codes this crate raises itself
170/// are matched by their `code::` constant. A protocol code this crate
171/// chooses no kind for keeps [`ErrorKind::Other`], as it always has, and
172/// [`Error::kind`] takes such a refusal's kind from the status class.
173pub fn kind_of(code: &str) -> ErrorKind {
174    if let Some(kind) = ErrorCode::parse(code).and_then(kind_of_protocol) {
175        return kind;
176    }
177    match code {
178        code::INVALID_TOKEN => ErrorKind::Auth,
179        code::UNREACHABLE => ErrorKind::Transport,
180        code::INVALID_TOKEN_FILE
181        | code::INVALID_SERVER
182        | code::INVALID_PROXY
183        | code::MISSING_SERVER
184        | code::INVALID_ENVIRONMENT
185        | code::INVALID_NAME
186        | code::INVALID_CONFIG
187        | code::INVALID_AUDIT_HEAD
188        | code::CREDENTIAL_LITERAL
189        | code::UNSUPPORTED_FORMAT
190        | code::NOT_TEXT
191        | code::UNKNOWN_PATH
192        | code::INVALID_PATH
193        | code::INVALID_KIT
194        | code::UNKNOWN_PROJECT
195        | code::PROJECT_EXISTS
196        | code::ENV_EXISTS
197        | code::IS_PROJECT
198        | code::HAS_CHILDREN
199        | code::VAULT_EXISTS
200        | code::NO_KEY
201        | code::SERVER_MISMATCH
202        | code::REKEY_IN_PROGRESS
203        | code::NO_REKEY
204        | code::REKEY_FORWARD_ONLY
205        | code::DATA_DIR_IN_USE
206        | code::INVALID_DATA_DIR
207        | code::UNSUPPORTED_PROTOCOL
208        | code::UNSUPPORTED_BY_CLIENT => ErrorKind::Invalid,
209        code::BINDING_MISMATCH | code::AUDIT_MISMATCH => ErrorKind::Integrity,
210        _ => ErrorKind::Other,
211    }
212}
213
214/// The kind of a code the protocol defines, where this crate classifies it.
215///
216/// `None` means the code keeps [`ErrorKind::Other`], exactly as it did when
217/// this table matched wire strings. Naming the variants rather than their
218/// strings is the point: a rename in `galata-vault-proto` stops the build
219/// here. Adding a variant does not, and needs no change: it is `None` until
220/// a kind is chosen for it.
221fn kind_of_protocol(code: ErrorCode) -> Option<ErrorKind> {
222    Some(match code {
223        ErrorCode::Unauthorized | ErrorCode::TokenExpired => ErrorKind::Auth,
224        ErrorCode::Forbidden => ErrorKind::Forbidden,
225        ErrorCode::NotFound => ErrorKind::NotFound,
226        ErrorCode::PreconditionFailed
227        | ErrorCode::Conflict
228        | ErrorCode::StaleGeneration
229        | ErrorCode::VersionMismatch => ErrorKind::Conflict,
230        ErrorCode::BadSignature
231        | ErrorCode::KeyMismatch
232        | ErrorCode::VersionRollback
233        | ErrorCode::GenerationRollback => ErrorKind::Integrity,
234        _ => return None,
235    })
236}
237
238/// The kind a refusal's HTTP status implies: how a server code this client
239/// does not know is classified (`docs/spec/http-api.md#7`).
240pub fn kind_of_status(status: u16) -> ErrorKind {
241    match status {
242        401 => ErrorKind::Auth,
243        403 => ErrorKind::Forbidden,
244        404 => ErrorKind::NotFound,
245        409 | 412 => ErrorKind::Conflict,
246        _ => ErrorKind::Other,
247    }
248}
249
250/// What a write expected the record to be, in a conflict.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252#[non_exhaustive]
253pub enum Expected {
254    /// That it did not exist (or was deleted).
255    Absent,
256    /// That its latest version was this one.
257    Version(u64),
258}
259
260impl From<Pre> for Expected {
261    fn from(pre: Pre) -> Expected {
262        match pre {
263            Pre::Create | Pre::Revive(_) => Expected::Absent,
264            Pre::Update(v) => Expected::Version(v),
265        }
266    }
267}
268
269impl std::fmt::Display for Expected {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        match self {
272            Expected::Absent => f.write_str("no existing record"),
273            Expected::Version(v) => write!(f, "version {v}"),
274        }
275    }
276}
277
278/// A protocol verification failure: what the server served does not
279/// verify.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281#[non_exhaustive]
282pub enum IntegrityFailure {
283    /// A descriptor, bundle, record or children signature does not verify.
284    BadSignature(&'static str),
285    /// A key or identity does not match the pinned vault.
286    KeyMismatch(&'static str),
287    /// A signed or encrypted binding disagrees with what the server reported.
288    BindingMismatch(&'static str),
289    /// The server showed an older record version than this client saw.
290    VersionRollback {
291        /// The version this client saw.
292        known: u64,
293        /// The version the server showed.
294        got: u64,
295    },
296    /// The server showed an older or different generation than this client saw.
297    GenerationRollback {
298        /// The generation this client saw.
299        known: u32,
300        /// The generation the server showed.
301        got: u32,
302    },
303    /// The audit chain does not continue the known head.
304    AuditMismatch,
305}
306
307impl IntegrityFailure {
308    /// The stable code.
309    pub fn code(&self) -> &'static str {
310        match self {
311            IntegrityFailure::BadSignature(_) => code::BAD_SIGNATURE,
312            IntegrityFailure::KeyMismatch(_) => code::KEY_MISMATCH,
313            IntegrityFailure::BindingMismatch(_) => code::BINDING_MISMATCH,
314            IntegrityFailure::VersionRollback { .. } => code::VERSION_ROLLBACK,
315            IntegrityFailure::GenerationRollback { .. } => code::GENERATION_ROLLBACK,
316            IntegrityFailure::AuditMismatch => code::AUDIT_MISMATCH,
317        }
318    }
319}
320
321impl From<&IntegrityError> for IntegrityFailure {
322    fn from(e: &IntegrityError) -> IntegrityFailure {
323        match *e {
324            IntegrityError::BadSignature(w) => IntegrityFailure::BadSignature(w),
325            IntegrityError::KeyMismatch(w) => IntegrityFailure::KeyMismatch(w),
326            IntegrityError::BindingMismatch(w) | IntegrityError::Malformed(w) => {
327                IntegrityFailure::BindingMismatch(w)
328            }
329            IntegrityError::VersionRollback { known, got } => {
330                IntegrityFailure::VersionRollback { known, got }
331            }
332            IntegrityError::GenerationRollback { known, got } => {
333                IntegrityFailure::GenerationRollback { known, got }
334            } // An integrity failure galata-vault-proto names that this build does not:
335              // still an integrity failure.
336        }
337    }
338}
339
340/// Every failure this crate returns. `Send`, `Sync` and `Clone`.
341#[derive(Debug, Clone)]
342#[non_exhaustive]
343pub enum Error {
344    /// Refused on this side before any request: a bad argument, file,
345    /// environment, document or local state. `code` says which.
346    Invalid {
347        /// The stable code.
348        code: &'static str,
349        /// The message.
350        message: String,
351    },
352    /// The token is malformed, unknown, revoked or expired, or its vault
353    /// expired (`unauthorized`, `token_expired`, `invalid_token`).
354    Auth {
355        /// The stable code.
356        code: String,
357        /// The message.
358        message: String,
359    },
360    /// The credential's scope or allow-list does not permit it.
361    Forbidden {
362        /// The message.
363        message: String,
364    },
365    /// No such record, version or token, or the record was deleted.
366    NotFound {
367        /// The message.
368        message: String,
369    },
370    /// The server does not know an owner's vault: it expired, was deleted,
371    /// or was re-rooted. An expired one can be repaired at the same id.
372    VaultNotFound {
373        /// The environment path.
374        label: String,
375        /// The message.
376        message: String,
377    },
378    /// A path that is not in the known tree, refused before any request.
379    UnknownPath {
380        /// The path asked for.
381        path: String,
382        /// The closest known path, if one is plausibly a typo.
383        suggestion: Option<String>,
384        /// Whether the path's project is known at all.
385        project_known: bool,
386        /// The message.
387        message: String,
388    },
389    /// Another writer won; nothing was overwritten.
390    Conflict {
391        /// The record name.
392        name: String,
393        /// What the write expected.
394        expected: Expected,
395        /// The server's latest version, if the record exists.
396        current: Option<u64>,
397        /// The message.
398        message: String,
399    },
400    /// The vault was rotated underneath this handle: call
401    /// [`crate::Vault::refresh`] and retry.
402    StaleGeneration {
403        /// The vault's label.
404        label: String,
405        /// The message.
406        message: String,
407    },
408    /// Something the server served does not verify.
409    Integrity {
410        /// What failed.
411        failure: IntegrityFailure,
412        /// The message.
413        message: String,
414    },
415    /// Any other refusal by the server, its code kept verbatim, including a
416    /// code this client does not know (`http_<status>` when it named none).
417    Server {
418        /// The stable code.
419        code: String,
420        /// The HTTP status.
421        status: u16,
422        /// The message.
423        message: String,
424    },
425    /// The server could not be reached; `source()` is the I/O or TLS error.
426    Transport {
427        /// The server.
428        server: String,
429        /// The message.
430        message: String,
431        /// The underlying cause.
432        source: Arc<dyn std::error::Error + Send + Sync>,
433    },
434    /// A key store or state store failed; `source()` is its error.
435    Store {
436        /// The message.
437        message: String,
438        /// The store's error.
439        source: Arc<StoreError>,
440    },
441    /// Any other failure (code `error`).
442    Other {
443        /// The message.
444        message: String,
445    },
446}
447
448impl Error {
449    /// The stable code, the same string Python's `.code` reports.
450    pub fn code(&self) -> &str {
451        match self {
452            Error::Invalid { code, .. } => code,
453            Error::Auth { code, .. } | Error::Server { code, .. } => code,
454            Error::Forbidden { .. } => code::FORBIDDEN,
455            Error::NotFound { .. } | Error::VaultNotFound { .. } => code::NOT_FOUND,
456            Error::UnknownPath { .. } => code::UNKNOWN_PATH,
457            Error::Conflict { .. } => code::CONFLICT,
458            Error::StaleGeneration { .. } => code::STALE_GENERATION,
459            Error::Integrity { failure, .. } => failure.code(),
460            Error::Transport { .. } => code::UNREACHABLE,
461            Error::Store { .. } => code::STORE,
462            Error::Other { .. } => code::ERROR,
463        }
464    }
465
466    /// The kind of failure, from [`kind_of`]; for a server code this client
467    /// does not know, from the HTTP status class ([`kind_of_status`]).
468    pub fn kind(&self) -> ErrorKind {
469        match self {
470            Error::Server { code, status, .. } if ErrorCode::parse(code).is_none() => {
471                kind_of_status(*status)
472            }
473            _ => kind_of(self.code()),
474        }
475    }
476
477    /// The whole message.
478    pub fn message(&self) -> &str {
479        match self {
480            Error::Invalid { message, .. }
481            | Error::Auth { message, .. }
482            | Error::Forbidden { message }
483            | Error::NotFound { message }
484            | Error::VaultNotFound { message, .. }
485            | Error::UnknownPath { message, .. }
486            | Error::Conflict { message, .. }
487            | Error::StaleGeneration { message, .. }
488            | Error::Integrity { message, .. }
489            | Error::Server { message, .. }
490            | Error::Transport { message, .. }
491            | Error::Store { message, .. }
492            | Error::Other { message } => message,
493        }
494    }
495
496    fn message_mut(&mut self) -> &mut String {
497        match self {
498            Error::Invalid { message, .. }
499            | Error::Auth { message, .. }
500            | Error::Forbidden { message }
501            | Error::NotFound { message }
502            | Error::VaultNotFound { message, .. }
503            | Error::UnknownPath { message, .. }
504            | Error::Conflict { message, .. }
505            | Error::StaleGeneration { message, .. }
506            | Error::Integrity { message, .. }
507            | Error::Server { message, .. }
508            | Error::Transport { message, .. }
509            | Error::Store { message, .. }
510            | Error::Other { message } => message,
511        }
512    }
513
514    /// The same error, its message prefixed with `context: `.
515    pub(crate) fn context(mut self, context: impl std::fmt::Display) -> Error {
516        let m = self.message_mut();
517        *m = format!("{context}: {m}");
518        self
519    }
520
521    pub(crate) fn local(code: &'static str, message: impl Into<String>) -> Error {
522        Error::Invalid {
523            code,
524            message: message.into(),
525        }
526    }
527
528    pub(crate) fn forbidden(message: impl Into<String>) -> Error {
529        Error::Forbidden {
530            message: message.into(),
531        }
532    }
533
534    pub(crate) fn not_found(message: impl Into<String>) -> Error {
535        Error::NotFound {
536            message: message.into(),
537        }
538    }
539
540    pub(crate) fn other(message: impl Into<String>) -> Error {
541        Error::Other {
542            message: message.into(),
543        }
544    }
545
546    pub(crate) fn stale(label: &str, message: impl Into<String>) -> Error {
547        Error::StaleGeneration {
548            label: label.to_owned(),
549            message: message.into(),
550        }
551    }
552
553    /// Refused because the operation depends on a value this client does not
554    /// know. Nothing was changed.
555    pub(crate) fn unsupported(message: impl Into<String>) -> Error {
556        Error::Invalid {
557            code: code::UNSUPPORTED_BY_CLIENT,
558            message: message.into(),
559        }
560    }
561
562    /// `label: integrity check failed: <what>`.
563    pub(crate) fn integrity(label: &str, e: IntegrityError) -> Error {
564        Error::Integrity {
565            failure: IntegrityFailure::from(&e),
566            message: format!("{label}: integrity check failed: {e}"),
567        }
568    }
569
570    pub(crate) fn conflict(name: &str, pre: Pre, current: Option<u64>, deleting: bool) -> Error {
571        let expected = Expected::from(pre);
572        let current_text = current.map_or("none".to_owned(), |v| v.to_string());
573        let outcome = if deleting {
574            "nothing was deleted"
575        } else {
576            "nothing was overwritten"
577        };
578        Error::Conflict {
579            name: name.to_owned(),
580            expected,
581            current,
582            message: format!(
583                "{name} changed on the server: expected {expected}, the server has version {current_text}; {outcome}"
584            ),
585        }
586    }
587
588    /// A value-crate failure, with context: an integrity code when it is one.
589    pub(crate) fn seal(e: SealError, context: impl std::fmt::Display) -> Error {
590        let message = format!("{context}: {}", chain(&e));
591        match seal_class(&e) {
592            Class::Integrity(failure) => Error::Integrity { failure, message },
593            Class::Stale => Error::StaleGeneration {
594                label: context.to_string(),
595                message,
596            },
597            Class::Unsupported => Error::unsupported(message),
598            Class::Other => Error::Other { message },
599        }
600    }
601
602    /// A key-crate failure, with context: an integrity code when it is one.
603    pub(crate) fn key(e: KeyError, context: impl std::fmt::Display) -> Error {
604        let message = format!("{context}: {}", chain(&e));
605        match key_class(&e) {
606            Class::Integrity(failure) => Error::Integrity { failure, message },
607            Class::Unsupported => Error::unsupported(message),
608            _ => Error::Other { message },
609        }
610    }
611
612    /// An audit chain that does not verify. A row naming an action or actor
613    /// this client does not know is not a verdict on the chain: it is
614    /// `unsupported_by_client`, and nothing is recorded.
615    pub(crate) fn audit_chain(e: ChainError, context: impl std::fmt::Display) -> Error {
616        let message = format!("{context}: {}", chain(&e));
617        match e {
618            ChainError::UnknownValue { .. } => Error::unsupported(message),
619            ChainError::HeadMismatch => Error::Integrity {
620                failure: IntegrityFailure::BindingMismatch("audit head"),
621                message,
622            },
623            _ => Error::Integrity {
624                failure: IntegrityFailure::AuditMismatch,
625                message,
626            },
627        }
628    }
629
630    /// A request that did not succeed. A refusal or a transport failure is
631    /// labelled with `label`; an answer that did not decode is not.
632    pub(crate) fn api(e: ApiError, label: Option<&str>) -> Error {
633        let labelled = |m: String| match label {
634            Some(l) => format!("{l}: {m}"),
635            None => m,
636        };
637        match &e {
638            ApiError::Transport(t) => Error::Transport {
639                server: t.server().to_owned(),
640                message: labelled(t.to_string()),
641                source: t.cause().unwrap_or_else(|| Arc::new(t.clone())),
642            },
643            ApiError::UnsupportedProtocol { .. } => Error::Invalid {
644                code: code::UNSUPPORTED_PROTOCOL,
645                message: labelled(e.to_string()),
646            },
647            ApiError::Refused { status, .. } => {
648                let code = e.stable_code();
649                let message = labelled(e.to_string());
650                match kind_of(&code) {
651                    ErrorKind::Auth => Error::Auth { code, message },
652                    ErrorKind::Forbidden => Error::Forbidden { message },
653                    ErrorKind::NotFound => Error::NotFound { message },
654                    _ => Error::Server {
655                        code,
656                        status: *status,
657                        message,
658                    },
659                }
660            }
661            _ => Error::Other {
662                message: e.to_string(),
663            },
664        }
665    }
666
667    /// A store failure.
668    pub(crate) fn store(e: StoreError) -> Error {
669        Error::Store {
670            message: e.message().to_owned(),
671            source: Arc::new(e),
672        }
673    }
674
675    #[cfg(feature = "http")]
676    pub(crate) fn build(e: crate::client::BuildError) -> Error {
677        match e {
678            crate::client::BuildError::InvalidServer(m) => Error::local(code::INVALID_SERVER, m),
679            other => Error::local(code::INVALID_PROXY, other.to_string()),
680        }
681    }
682}
683
684impl std::fmt::Display for Error {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        f.write_str(self.message())
687    }
688}
689
690impl std::error::Error for Error {
691    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
692        match self {
693            Error::Transport { source, .. } => Some(&**source),
694            Error::Store { source, .. } => Some(&**source),
695            _ => None,
696        }
697    }
698}
699
700/// An error and its sources, `: `-separated (what `anyhow`'s `{:#}` gave).
701pub(crate) fn chain(e: &dyn std::error::Error) -> String {
702    let mut out = e.to_string();
703    let mut cur = e.source();
704    while let Some(s) = cur {
705        out.push_str(": ");
706        out.push_str(&s.to_string());
707        cur = s.source();
708    }
709    out
710}
711
712enum Class {
713    Integrity(IntegrityFailure),
714    Stale,
715    Unsupported,
716    Other,
717}
718
719fn key_class(e: &KeyError) -> Class {
720    match e {
721        KeyError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
722        KeyError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
723        KeyError::NameMismatch => {
724            Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
725        }
726        KeyError::Kind { .. } => Class::Integrity(IntegrityFailure::BindingMismatch("bundle kind")),
727        KeyError::UnsupportedScope(_) => Class::Unsupported,
728        _ => Class::Other,
729    }
730}
731
732fn seal_class(e: &SealError) -> Class {
733    match e {
734        SealError::Integrity(i) => Class::Integrity(IntegrityFailure::from(i)),
735        SealError::Key(k) => key_class(k),
736        SealError::Binding(w) => Class::Integrity(IntegrityFailure::BindingMismatch(w)),
737        SealError::NameMismatch => {
738            Class::Integrity(IntegrityFailure::BindingMismatch("record name"))
739        }
740        SealError::KindMismatch { .. } => {
741            Class::Integrity(IntegrityFailure::BindingMismatch("record kind"))
742        }
743        SealError::StaleGeneration { .. } => Class::Stale,
744        SealError::UnsupportedByClient(_) => Class::Unsupported,
745        _ => Class::Other,
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    #[test]
754    fn codes_map_to_kinds() {
755        assert_eq!(kind_of("token_expired"), ErrorKind::Auth);
756        assert_eq!(kind_of(code::INVALID_TOKEN), ErrorKind::Auth);
757        assert_eq!(kind_of("precondition_failed"), ErrorKind::Conflict);
758        assert_eq!(kind_of("version_mismatch"), ErrorKind::Conflict);
759        assert_eq!(kind_of(code::STALE_GENERATION), ErrorKind::Conflict);
760        assert_eq!(kind_of(code::CREDENTIAL_LITERAL), ErrorKind::Invalid);
761        assert_eq!(kind_of(code::UNKNOWN_PATH), ErrorKind::Invalid);
762        for c in [
763            code::BAD_SIGNATURE,
764            code::KEY_MISMATCH,
765            code::VERSION_ROLLBACK,
766            code::GENERATION_ROLLBACK,
767            code::AUDIT_MISMATCH,
768        ] {
769            assert_eq!(kind_of(c), ErrorKind::Integrity, "{c}");
770        }
771        assert_eq!(Error::forbidden("x").code(), code::FORBIDDEN);
772    }
773
774    /// Every protocol code this crate classifies, named through [`ErrorCode`]
775    /// rather than a copy of its wire string: a rename in
776    /// `galata-vault-proto` fails here instead of quietly becoming
777    /// [`ErrorKind::Other`].
778    #[test]
779    fn protocol_codes_classify_through_their_type() {
780        for (c, kind) in [
781            (ErrorCode::Unauthorized, ErrorKind::Auth),
782            (ErrorCode::TokenExpired, ErrorKind::Auth),
783            (ErrorCode::Forbidden, ErrorKind::Forbidden),
784            (ErrorCode::NotFound, ErrorKind::NotFound),
785            (ErrorCode::PreconditionFailed, ErrorKind::Conflict),
786            (ErrorCode::Conflict, ErrorKind::Conflict),
787            (ErrorCode::StaleGeneration, ErrorKind::Conflict),
788            (ErrorCode::VersionMismatch, ErrorKind::Conflict),
789            (ErrorCode::BadSignature, ErrorKind::Integrity),
790            (ErrorCode::KeyMismatch, ErrorKind::Integrity),
791            (ErrorCode::VersionRollback, ErrorKind::Integrity),
792            (ErrorCode::GenerationRollback, ErrorKind::Integrity),
793        ] {
794            assert_eq!(kind_of(c.as_str()), kind, "{c}");
795        }
796        // A protocol code this crate does not classify keeps the general
797        // kind, as before; `Error::kind` reads the status class instead.
798        assert_eq!(kind_of(ErrorCode::RateLimited.as_str()), ErrorKind::Other);
799        assert_eq!(kind_of(ErrorCode::Unavailable.as_str()), ErrorKind::Other);
800    }
801
802    #[test]
803    fn integrity_failures_keep_their_code_through_context() {
804        let e = Error::seal(
805            SealError::Integrity(IntegrityError::BadSignature("record")),
806            "acme/dev: version 2 of DB",
807        );
808        assert_eq!(
809            (e.code(), e.kind()),
810            (code::BAD_SIGNATURE, ErrorKind::Integrity)
811        );
812        assert!(e.message().starts_with("acme/dev: version 2 of DB"), "{e}");
813        assert!(matches!(
814            e,
815            Error::Integrity {
816                failure: IntegrityFailure::BadSignature("record"),
817                ..
818            }
819        ));
820        let stale = Error::seal(
821            SealError::StaleGeneration {
822                bundle: 1,
823                vault: 2,
824            },
825            "a",
826        );
827        assert_eq!(stale.code(), code::STALE_GENERATION);
828    }
829
830    #[test]
831    fn a_conflict_carries_both_versions() {
832        let e = Error::conflict("app", Pre::Update(7), Some(8), false);
833        assert!(matches!(
834            e,
835            Error::Conflict {
836                expected: Expected::Version(7),
837                current: Some(8),
838                ..
839            }
840        ));
841        assert_eq!(e.code(), code::CONFLICT);
842        assert_eq!(
843            e.message(),
844            "app changed on the server: expected version 7, the server has version 8; nothing was overwritten"
845        );
846        assert_eq!(e.clone().context("acme/dev").kind(), ErrorKind::Conflict);
847    }
848
849    /// A code from a newer server keeps its string and the server's message,
850    /// and takes its kind from the status class.
851    #[test]
852    fn an_unknown_server_code_keeps_its_code_message_and_status_kind() {
853        let body = br#"{"error":"future_conflict","message":"try again later"}"#;
854        let transport = crate::client::RecordingTransport::responding(move |_| {
855            Ok(crate::client::Response::new(409, body.to_vec()))
856        });
857        let e = crate::client::Api::new(transport)
858            .status(crate::client::Auth::None)
859            .unwrap_err();
860        let e = Error::api(e, Some("acme/dev"));
861        assert_eq!(e.code(), "future_conflict");
862        assert_eq!(e.kind(), ErrorKind::Conflict);
863        assert!(e.message().contains("try again later"), "{e}");
864        assert_eq!(kind_of_status(401), ErrorKind::Auth);
865        assert_eq!(kind_of_status(418), ErrorKind::Other);
866        assert_eq!(kind_of(code::UNSUPPORTED_BY_CLIENT), ErrorKind::Invalid);
867        assert_eq!(kind_of(code::UNSUPPORTED_PROTOCOL), ErrorKind::Invalid);
868    }
869
870    #[test]
871    fn an_error_is_shareable() {
872        fn shared<T: Send + Sync + Clone + std::error::Error>() {}
873        shared::<Error>();
874    }
875
876    #[test]
877    fn a_transport_failure_keeps_its_cause() {
878        let io = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
879        let t = crate::client::TransportError::with_source("https://vault.example", io);
880        let e = Error::api(ApiError::Transport(t), Some("acme/dev"));
881        assert_eq!(e.code(), code::UNREACHABLE);
882        assert!(e.message().starts_with("acme/dev: could not reach"), "{e}");
883        let source = std::error::Error::source(&e).expect("a cause");
884        assert!(source.downcast_ref::<std::io::Error>().is_some());
885    }
886}