Skip to main content

aven_core/sync/client/
errors.rs

1//! Stable error codes carried by client errors.
2//!
3//! Every engine error message starts `error <code>`, optionally followed by
4//! details and a `hint="..."`. Hosts map codes, never message text.
5use anyhow::Error;
6
7/// The stable error codes in the chain, outermost first.
8pub fn codes(error: &Error) -> impl Iterator<Item = String> + '_ {
9    error.chain().filter_map(|cause| {
10        cause
11            .to_string()
12            .strip_prefix("error ")
13            .and_then(|rest| rest.split_whitespace().next())
14            .map(str::to_string)
15    })
16}
17
18/// The outermost stable error code in the chain.
19pub fn code(error: &Error) -> Option<String> {
20    codes(error).next()
21}
22
23pub fn has_code(error: &Error, expected: &str) -> bool {
24    codes(error).any(|code| code == expected)
25}
26
27/// The server saw an older membership head than its current one.
28pub fn is_stale(error: &Error) -> bool {
29    error.is::<crate::sync::seed_claim::membership::StaleContext>()
30}
31
32/// The server refused this device's credential or membership, which may
33/// mean another device removed it.
34pub fn is_access_refusal(error: &Error) -> bool {
35    has_code(error, "sync-server-refused")
36        || has_code(error, "enrollment-unauthorized")
37        || has_code(error, "enrollment-revoked")
38        || has_code(error, "sync-device-removed")
39}
40
41/// The server reports this vault deleted for good: rerunning sync can't
42/// recover it, and a new setup is required.
43pub fn is_vault_deleted(error: &Error) -> bool {
44    has_code(error, VAULT_DELETED)
45}
46
47const VAULT_DELETED: &str = "sync-vault-deleted";
48
49/// Hosting replies cannot authenticate the outcome of a pending operation.
50pub(crate) fn hosting_error(hosting: aven_protocol::refusal::Hosting) -> Error {
51    use aven_protocol::refusal::Hosting;
52    let code = match hosting {
53        Hosting::Blocked => "sync-hosting-blocked",
54        Hosting::Deleted => VAULT_DELETED,
55        Hosting::Quota => "sync-hosting-quota",
56        Hosting::Unavailable => "sync-hosting-unavailable",
57    };
58    anyhow::anyhow!("error {code} outcome-unknown")
59}
60
61pub(crate) fn is_hosting_refusal(error: &Error) -> bool {
62    codes(error).any(|code| {
63        matches!(
64            code.as_str(),
65            "sync-hosting-blocked"
66                | "sync-hosting-quota"
67                | "sync-hosting-unavailable"
68                | VAULT_DELETED
69        )
70    })
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use aven_protocol::refusal::Hosting;
77
78    #[test]
79    fn every_hosting_refusal_stops_transfers_and_only_deletion_is_terminal() {
80        for hosting in [
81            Hosting::Blocked,
82            Hosting::Deleted,
83            Hosting::Quota,
84            Hosting::Unavailable,
85        ] {
86            let error = hosting_error(hosting).context("error sync-round");
87            assert!(is_hosting_refusal(&error));
88            assert!(!is_access_refusal(&error));
89            assert_eq!(is_vault_deleted(&error), hosting == Hosting::Deleted);
90        }
91    }
92}