Skip to main content

syncweb_core/
error.rs

1use std::{error::Error as StdError, fmt::Display, path::PathBuf};
2
3use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, SyncwebError>;
6pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
7
8/// Flatten an error and its `source()` chain into one line.
9///
10/// [`SyncwebError::operation`] keeps a `Display` rendering rather than the
11/// source, so errors whose `Display` is empty (for example
12/// `iroh_blobs::api::Error::Io`, whose message lives in the source `io::Error`)
13/// would otherwise surface as a bare variant name.
14#[must_use]
15pub fn error_chain(error: &dyn StdError) -> String {
16    let mut text = error.to_string();
17    let mut cause = error.source();
18    while let Some(current) = cause {
19        let message = current.to_string();
20        if !message.is_empty() {
21            text.push_str(": ");
22            text.push_str(&message);
23        }
24        cause = current.source();
25    }
26    text
27}
28
29/// Top-level error type for syncweb-core library operations.
30#[derive(Debug, Error)]
31#[non_exhaustive]
32pub enum SyncwebError {
33    #[error("folder not found: {0}")]
34    FolderNotFound(String),
35
36    #[error("no synchronized folders are available")]
37    NoFolders,
38
39    #[error("folder selector {0:?} is not a namespace ID and more than one synchronized folder is available")]
40    AmbiguousFolderSelector(String),
41
42    #[error("folder already managed")]
43    FolderAlreadyManaged,
44
45    #[error("folder mode {mode} does not permit local writes")]
46    WriteDenied { mode: String },
47
48    #[error("invalid doc ticket: {0}")]
49    InvalidTicket(String),
50
51    #[error("invalid configuration: {0}")]
52    InvalidConfig(String),
53
54    #[error("invalid identity: {0}")]
55    InvalidIdentity(String),
56
57    #[error("invalid device ID: {0}")]
58    InvalidDeviceId(String),
59
60    #[error("invalid sync mode: {0}")]
61    InvalidSyncMode(String),
62
63    #[error("syncthing relay fallback is disabled")]
64    RelayDisabled,
65
66    #[error("no syncthing relay is reachable: {reasons}")]
67    RelayUnreachable { reasons: String },
68
69    #[error("relay frame exceeds {max} byte limit")]
70    RelayFrameTooLarge { max: usize },
71
72    #[error("relay message decode error: {0}")]
73    RelayDecode(String),
74
75    #[error("relay URL must use tcp:// scheme")]
76    RelayBadScheme,
77
78    #[error("relay URL must contain a host and port: {0}")]
79    RelayBadAddress(String),
80
81    #[error("HKDF key derivation failed: {0}")]
82    KeyDerivation(String),
83
84    #[error("identity file error at {path}: {source}")]
85    Identity {
86        path: PathBuf,
87        #[source]
88        source: BoxError,
89    },
90
91    #[error("{context}: {detail}")]
92    Operation { context: String, detail: String },
93
94    #[error("blob size exceeds u64::MAX")]
95    BlobTooLarge,
96
97    #[error("namespace could not be opened")]
98    NamespaceNotAvailable,
99
100    #[error("missing cryptographic signature: {0}")]
101    MissingSignature(String),
102
103    #[error("invalid cryptographic signature: {0}")]
104    InvalidSignature(String),
105
106    #[error("access denied: {0}")]
107    AccessDenied(String),
108
109    #[error(transparent)]
110    Io(#[from] std::io::Error),
111}
112
113impl SyncwebError {
114    pub fn identity(path: impl Into<PathBuf>, source: impl StdError + Send + Sync + 'static) -> Self {
115        Self::Identity {
116            path: path.into(),
117            source: Box::new(source),
118        }
119    }
120
121    pub fn operation(context: impl Into<String>, source: impl Display) -> Self {
122        Self::Operation {
123            context: context.into(),
124            detail: source.to_string(),
125        }
126    }
127
128    pub fn access_denied(reason: impl Into<String>) -> Self {
129        Self::AccessDenied(reason.into())
130    }
131}
132
133#[cfg(test)]
134mod error_chain_tests {
135    use super::*;
136
137    #[derive(Debug, Error)]
138    #[error("outer failure")]
139    struct Outer(#[source] Inner);
140
141    #[derive(Debug, Error)]
142    #[error("inner failure")]
143    struct Inner(#[source] std::io::Error);
144
145    /// The case that motivated the helper: a variant whose `Display` carries no
146    /// detail while the useful text lives on the source.
147    #[derive(Debug, Error)]
148    #[error("Error::Io")]
149    struct IoVariant(#[source] std::io::Error);
150
151    #[test]
152    fn the_whole_chain_is_flattened_in_order() {
153        let error = Outer(Inner(std::io::Error::other("disk went away")));
154        assert_eq!(error_chain(&error), "outer failure: inner failure: disk went away");
155    }
156
157    #[test]
158    fn a_source_only_message_is_included() {
159        let error = IoVariant(std::io::Error::other("path is not absolute"));
160        let text = error_chain(&error);
161        assert!(text.starts_with("Error::Io"), "{text}");
162        assert!(text.contains("path is not absolute"), "{text}");
163    }
164
165    #[test]
166    fn a_source_without_a_message_does_not_add_a_separator() {
167        #[derive(Debug, Error)]
168        #[error("top")]
169        struct EmptySource(#[source] Empty);
170
171        #[derive(Debug, Error)]
172        #[error("")]
173        struct Empty;
174
175        assert_eq!(error_chain(&EmptySource(Empty)), "top");
176    }
177}