1
//! Declare error types.
2

            
3
use std::path::PathBuf;
4

            
5
use tor_basic_utils::PathExt as _;
6
use tor_error::{ErrorKind, HasKind};
7

            
8
/// An error related to an option passed to Arti via a configuration
9
/// builder.
10
//
11
// API NOTE: When possible, we should expose this error type rather than
12
// wrapping it in `TorError`. It can provide specific information about  what
13
// part of the configuration was invalid.
14
//
15
// This is part of the public API.
16
#[derive(Debug, Clone, thiserror::Error)]
17
#[non_exhaustive]
18
pub enum ConfigBuildError {
19
    /// A mandatory field was not present.
20
    #[error("Field was not provided: {field}")]
21
    MissingField {
22
        /// The name of the missing field.
23
        field: String,
24
    },
25
    /// A single field had a value that proved to be unusable.
26
    #[error("Value of {field} was incorrect: {problem}")]
27
    Invalid {
28
        /// The name of the invalid field
29
        field: String,
30
        /// A description of the problem.
31
        problem: String,
32
    },
33
    /// Multiple fields are inconsistent.
34
    #[error("Fields {fields:?} are inconsistent: {problem}")]
35
    Inconsistent {
36
        /// The names of the inconsistent fields
37
        fields: Vec<String>,
38
        /// The problem that makes them inconsistent
39
        problem: String,
40
    },
41
    /// The requested configuration is not supported in this build
42
    #[error("Field {field:?} specifies a configuration not supported in this build: {problem}")]
43
    // TODO should we report the cargo feature, if applicable?  And if so, of `arti`
44
    // or of the underlying crate?  This seems like a can of worms.
45
    NoCompileTimeSupport {
46
        /// The names of the (primary) field requesting the unsupported configuration
47
        field: String,
48
        /// The description of the problem
49
        problem: String,
50
    },
51
}
52

            
53
impl From<derive_builder::UninitializedFieldError> for ConfigBuildError {
54
4
    fn from(val: derive_builder::UninitializedFieldError) -> Self {
55
4
        ConfigBuildError::MissingField {
56
4
            field: val.field_name().to_string(),
57
4
        }
58
4
    }
59
}
60

            
61
impl From<derive_builder::SubfieldBuildError<ConfigBuildError>> for ConfigBuildError {
62
2
    fn from(e: derive_builder::SubfieldBuildError<ConfigBuildError>) -> Self {
63
2
        let (field, problem) = e.into_parts();
64
2
        problem.within(field)
65
2
    }
66
}
67

            
68
impl ConfigBuildError {
69
    /// Return a new ConfigBuildError that prefixes its field name with
70
    /// `prefix` and a dot.
71
    #[must_use]
72
541
    pub fn within(&self, prefix: &str) -> Self {
73
        use ConfigBuildError::*;
74
852
        let addprefix = |field: &str| format!("{}.{}", prefix, field);
75
541
        match self {
76
6
            MissingField { field } => MissingField {
77
6
                field: addprefix(field),
78
6
            },
79
2
            Invalid { field, problem } => Invalid {
80
2
                field: addprefix(field),
81
2
                problem: problem.clone(),
82
2
            },
83
533
            Inconsistent { fields, problem } => Inconsistent {
84
840
                fields: fields.iter().map(|f| addprefix(f)).collect(),
85
533
                problem: problem.clone(),
86
            },
87
            NoCompileTimeSupport { field, problem } => NoCompileTimeSupport {
88
                field: addprefix(field),
89
                problem: problem.clone(),
90
            },
91
        }
92
541
    }
93
}
94

            
95
impl HasKind for ConfigBuildError {
96
    fn kind(&self) -> ErrorKind {
97
        ErrorKind::InvalidConfig
98
    }
99
}
100

            
101
/// An error caused when attempting to reconfigure an existing Arti client, or one of its modules.
102
#[derive(Debug, Clone, thiserror::Error)]
103
#[non_exhaustive]
104
pub enum ReconfigureError {
105
    /// Tried to change a field that cannot change on a running client.
106
    #[error("Cannot change {field} on a running client.")]
107
    CannotChange {
108
        /// The field (or fields) that we tried to change.
109
        field: String,
110
    },
111

            
112
    /// The requested configuration is not supported in this situation
113
    ///
114
    /// Something, probably discovered at runtime, is not compatible with
115
    /// the specified configuration.
116
    ///
117
    /// This ought *not* to be returned when the configuration is simply not supported
118
    /// by this build of arti -
119
    /// that should be reported at config build type as `ConfigBuildError::Unsupported`.
120
    #[error("Configuration not supported in this situation: {0}")]
121
    UnsupportedSituation(String),
122

            
123
    /// There was a programming error somewhere in our code, or the calling code.
124
    #[error("Programming error")]
125
    Bug(#[from] tor_error::Bug),
126
}
127

            
128
impl HasKind for ReconfigureError {
129
    fn kind(&self) -> ErrorKind {
130
        ErrorKind::InvalidConfigTransition
131
    }
132
}
133

            
134
/// An error that occurs while trying to read and process our configuration.
135
#[derive(Debug, Clone, thiserror::Error)]
136
#[non_exhaustive]
137
pub enum ConfigError {
138
    /// We encoundered a problem checking file permissions (for example, no such file)
139
    #[error("Problem accessing configuration file(s)")]
140
    FileAccess(#[source] fs_mistrust::Error),
141
    /// We encoundered a problem checking file permissions (for example, no such file)
142
    ///
143
    /// This variant name is misleading - see the docs for [`fs_mistrust::Error`].
144
    /// Please use [`ConfigError::FileAccess`] instead.
145
    #[deprecated = "use ConfigError::FileAccess instead"]
146
    #[error("Problem accessing configuration file(s)")]
147
    Permissions(#[source] fs_mistrust::Error),
148
    /// Our underlying configuration library gave an error while loading our
149
    /// configuration.
150
    #[error("Couldn't load configuration")]
151
    Load(#[source] ConfigLoadError),
152
    /// Encountered an IO error with a configuration file or directory.
153
    ///
154
    /// Note that some IO errors may be reported as `Load` errors,
155
    /// due to limitations of the underlying library.
156
    #[error("IoError while {} {}", action, path.display_lossy())]
157
    Io {
158
        /// The action while we were trying to perform
159
        action: &'static str,
160
        /// The path we were trying to do it to.
161
        path: PathBuf,
162
        /// The underlying problem
163
        #[source]
164
        err: std::sync::Arc<std::io::Error>,
165
    },
166
}
167

            
168
/// Wrapper for our an error type from our underlying configuration library.
169
#[derive(Debug, Clone)]
170
pub struct ConfigLoadError(figment::Error);
171

            
172
impl ConfigError {
173
    /// Wrap `err` as a ConfigError.
174
    ///
175
    /// This is not a From implementation, since we don't want to expose our
176
    /// underlying configuration library.
177
8
    pub(crate) fn from_cfg_err(err: figment::Error) -> Self {
178
        // TODO: It would be lovely to extract IO errors from figment::Error
179
        // and report them as Error::Io.  Unfortunately, it doesn't seem
180
        // possible to do that given the design of figment::Error.
181
8
        ConfigError::Load(ConfigLoadError(err))
182
8
    }
183
}
184

            
185
impl std::fmt::Display for ConfigLoadError {
186
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187
        let s = self.0.to_string();
188
        write!(f, "{}", s)?;
189
        if s.contains("invalid escape") || s.contains("invalid hex escape") {
190
            write!(
191
                f,
192
                "   (If you wanted to include a literal \\ character, you need to escape it by writing two in a row: \\\\)"
193
            )?;
194
        }
195
        Ok(())
196
    }
197
}
198

            
199
impl std::error::Error for ConfigLoadError {
200
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
201
        // A `ConfigLoadError` isn't really a new higher-level error,
202
        // it just wraps an existing figment error and formats it a little differently.
203
        // Our `Display` implementation writes the `self.0` error message,
204
        // so here in `source()` we skip `self.0` and return *its* source error.
205
        // Otherwise an error formatter which iterates over error sources would print the same
206
        // error message twice.
207
        self.0.source()
208
    }
209
}
210

            
211
#[cfg(test)]
212
mod test {
213
    // @@ begin test lint list maintained by maint/add_warning @@
214
    #![allow(clippy::bool_assert_comparison)]
215
    #![allow(clippy::clone_on_copy)]
216
    #![allow(clippy::dbg_macro)]
217
    #![allow(clippy::mixed_attributes_style)]
218
    #![allow(clippy::print_stderr)]
219
    #![allow(clippy::print_stdout)]
220
    #![allow(clippy::single_char_pattern)]
221
    #![allow(clippy::unwrap_used)]
222
    #![allow(clippy::unchecked_time_subtraction)]
223
    #![allow(clippy::useless_vec)]
224
    #![allow(clippy::needless_pass_by_value)]
225
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
226
    use super::*;
227

            
228
    #[test]
229
    fn within() {
230
        let e1 = ConfigBuildError::MissingField {
231
            field: "lettuce".to_owned(),
232
        };
233
        let e2 = ConfigBuildError::Invalid {
234
            field: "tomato".to_owned(),
235
            problem: "too crunchy".to_owned(),
236
        };
237
        let e3 = ConfigBuildError::Inconsistent {
238
            fields: vec!["mayo".to_owned(), "avocado".to_owned()],
239
            problem: "pick one".to_owned(),
240
        };
241

            
242
        assert_eq!(
243
            &e1.within("sandwich").to_string(),
244
            "Field was not provided: sandwich.lettuce"
245
        );
246
        assert_eq!(
247
            &e2.within("sandwich").to_string(),
248
            "Value of sandwich.tomato was incorrect: too crunchy"
249
        );
250
        assert_eq!(
251
            &e3.within("sandwich").to_string(),
252
            r#"Fields ["sandwich.mayo", "sandwich.avocado"] are inconsistent: pick one"#
253
        );
254
    }
255

            
256
    #[derive(derive_builder::Builder, Debug, Clone)]
257
    #[builder(build_fn(error = "ConfigBuildError"))]
258
    #[allow(dead_code)]
259
    struct Cephalopod {
260
        // arms have suction cups for their whole length
261
        arms: u8,
262
        // Tentacles have suction cups at the ends
263
        tentacles: u8,
264
    }
265

            
266
    #[test]
267
    fn build_err() {
268
        let squid = CephalopodBuilder::default().arms(8).tentacles(2).build();
269
        let octopus = CephalopodBuilder::default().arms(8).build();
270
        assert!(squid.is_ok());
271
        let squid = squid.unwrap();
272
        assert_eq!(squid.arms, 8);
273
        assert_eq!(squid.tentacles, 2);
274
        assert!(octopus.is_err());
275
        assert_eq!(
276
            &octopus.unwrap_err().to_string(),
277
            "Field was not provided: tentacles"
278
        );
279
    }
280

            
281
    #[derive(derive_builder::Builder, Debug)]
282
    #[builder(build_fn(error = "ConfigBuildError"))]
283
    #[allow(dead_code)]
284
    struct Pet {
285
        #[builder(sub_builder)]
286
        best_friend: Cephalopod,
287
    }
288

            
289
    #[test]
290
    fn build_subfield_err() {
291
        let mut petb = PetBuilder::default();
292
        petb.best_friend().tentacles(3);
293
        let pet = petb.build();
294
        assert_eq!(
295
            pet.unwrap_err().to_string(),
296
            "Field was not provided: best_friend.arms"
297
        );
298
    }
299
}