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#[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#[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 #[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}