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
    /// At least one of a set of fields must be present,
34
    /// but none were.
35
    #[error("At least {min_required} of these fields must be provided: {fields:?}")]
36
    MissingOneOf {
37
        /// The minimum number of fields that must be provided.
38
        min_required: usize,
39
        /// The names of the fields.
40
        fields: Vec<String>,
41
    },
42
    /// Multiple fields are inconsistent.
43
    #[error("Fields {fields:?} are inconsistent: {problem}")]
44
    Inconsistent {
45
        /// The names of the inconsistent fields
46
        fields: Vec<String>,
47
        /// The problem that makes them inconsistent
48
        problem: String,
49
    },
50
    /// The requested configuration is not supported in this build
51
    #[error("Field {field:?} specifies a configuration not supported in this build: {problem}")]
52
    // TODO should we report the cargo feature, if applicable?  And if so, of `arti`
53
    // or of the underlying crate?  This seems like a can of worms.
54
    NoCompileTimeSupport {
55
        /// The names of the (primary) field requesting the unsupported configuration
56
        field: String,
57
        /// The description of the problem
58
        problem: String,
59
    },
60
}
61

            
62
impl From<derive_builder::UninitializedFieldError> for ConfigBuildError {
63
4
    fn from(val: derive_builder::UninitializedFieldError) -> Self {
64
4
        ConfigBuildError::MissingField {
65
4
            field: val.field_name().to_string(),
66
4
        }
67
4
    }
68
}
69

            
70
impl From<derive_builder::SubfieldBuildError<ConfigBuildError>> for ConfigBuildError {
71
2
    fn from(e: derive_builder::SubfieldBuildError<ConfigBuildError>) -> Self {
72
2
        let (field, problem) = e.into_parts();
73
2
        problem.within(field)
74
2
    }
75
}
76

            
77
impl From<void::Void> for ConfigBuildError {
78
    fn from(value: void::Void) -> Self {
79
        void::unreachable(value)
80
    }
81
}
82

            
83
impl ConfigBuildError {
84
    /// Return a new ConfigBuildError that prefixes its field name with
85
    /// `prefix` and a dot.
86
    #[must_use]
87
577
    pub fn within(&self, prefix: &str) -> Self {
88
        use ConfigBuildError::*;
89
908
        let addprefix = |field: &str| format!("{}.{}", prefix, field);
90
577
        match self {
91
6
            MissingField { field } => MissingField {
92
6
                field: addprefix(field),
93
6
            },
94
            MissingOneOf {
95
                min_required,
96
                fields,
97
            } => MissingOneOf {
98
                min_required: *min_required,
99
                fields: fields.iter().map(|f| addprefix(f)).collect(),
100
            },
101
2
            Invalid { field, problem } => Invalid {
102
2
                field: addprefix(field),
103
2
                problem: problem.clone(),
104
2
            },
105
569
            Inconsistent { fields, problem } => Inconsistent {
106
896
                fields: fields.iter().map(|f| addprefix(f)).collect(),
107
569
                problem: problem.clone(),
108
            },
109
            NoCompileTimeSupport { field, problem } => NoCompileTimeSupport {
110
                field: addprefix(field),
111
                problem: problem.clone(),
112
            },
113
        }
114
577
    }
115
}
116

            
117
impl HasKind for ConfigBuildError {
118
    fn kind(&self) -> ErrorKind {
119
        ErrorKind::InvalidConfig
120
    }
121
}
122

            
123
/// An error caused when attempting to reconfigure an existing Arti client, or one of its modules.
124
#[derive(Debug, Clone, thiserror::Error)]
125
#[non_exhaustive]
126
pub enum ReconfigureError {
127
    /// Tried to change a field that cannot change on a running client.
128
    #[error("Cannot change {field} on a running client.")]
129
    CannotChange {
130
        /// The field (or fields) that we tried to change.
131
        field: String,
132
    },
133

            
134
    /// Tried to change a field in an unsupported way in a running client.
135
    ///
136
    /// `manner` should be an adverbial preprositional phrase,
137
    /// like "from on to off".
138
    #[error("Cannot change {field} {manner} on a running client.")]
139
    CannotChangeToValue {
140
        /// The field (or fields) that we tried to change.
141
        field: String,
142
        /// The way in which we tried to change them.
143
        manner: String,
144
    },
145

            
146
    /// The requested configuration is not supported in this situation
147
    ///
148
    /// Something, probably discovered at runtime, is not compatible with
149
    /// the specified configuration.
150
    ///
151
    /// This ought *not* to be returned when the configuration is simply not supported
152
    /// by this build of arti -
153
    /// that should be reported at config build type as `ConfigBuildError::Unsupported`.
154
    #[error("Configuration not supported in this situation: {0}")]
155
    UnsupportedSituation(String),
156

            
157
    /// There was a programming error somewhere in our code, or the calling code.
158
    #[error("Programming error")]
159
    Bug(#[from] tor_error::Bug),
160
}
161

            
162
impl HasKind for ReconfigureError {
163
    fn kind(&self) -> ErrorKind {
164
        ErrorKind::InvalidConfigTransition
165
    }
166
}
167

            
168
/// An error that occurs while trying to read and process our configuration.
169
#[derive(Debug, Clone, thiserror::Error)]
170
#[non_exhaustive]
171
pub enum ConfigError {
172
    /// We encoundered a problem checking file permissions (for example, no such file)
173
    #[error("Problem accessing configuration file(s)")]
174
    FileAccess(#[source] fs_mistrust::Error),
175
    /// We encoundered a problem checking file permissions (for example, no such file)
176
    ///
177
    /// This variant name is misleading - see the docs for [`fs_mistrust::Error`].
178
    /// Please use [`ConfigError::FileAccess`] instead.
179
    #[deprecated = "use ConfigError::FileAccess instead"]
180
    #[error("Problem accessing configuration file(s)")]
181
    Permissions(#[source] fs_mistrust::Error),
182
    /// Our underlying configuration library gave an error while loading our
183
    /// configuration.
184
    #[error("Couldn't load configuration")]
185
    Load(#[source] ConfigLoadError),
186
    /// Encountered an IO error with a configuration file or directory.
187
    ///
188
    /// Note that some IO errors may be reported as `Load` errors,
189
    /// due to limitations of the underlying library.
190
    #[error("IoError while {} {}", action, path.display_lossy())]
191
    Io {
192
        /// The action while we were trying to perform
193
        action: &'static str,
194
        /// The path we were trying to do it to.
195
        path: PathBuf,
196
        /// The underlying problem
197
        #[source]
198
        err: std::sync::Arc<std::io::Error>,
199
    },
200
}
201

            
202
/// An error that occurred while trying to look up a configuration value.
203
#[derive(Clone, Debug, thiserror::Error)]
204
#[non_exhaustive]
205
pub enum ConfigGetValueError {
206
    /// Some internal error occurred.
207
    #[error("Internal error")]
208
    Bug(#[from] tor_error::Bug),
209
}
210

            
211
/// Wrapper for our an error type from our underlying configuration library.
212
#[derive(Debug, Clone)]
213
pub struct ConfigLoadError(figment::Error);
214

            
215
impl ConfigError {
216
    /// Wrap `err` as a ConfigError.
217
    ///
218
    /// This is not a From implementation, since we don't want to expose our
219
    /// underlying configuration library.
220
8
    pub(crate) fn from_cfg_err(err: figment::Error) -> Self {
221
        // TODO: It would be lovely to extract IO errors from figment::Error
222
        // and report them as Error::Io.  Unfortunately, it doesn't seem
223
        // possible to do that given the design of figment::Error.
224
8
        ConfigError::Load(ConfigLoadError(err))
225
8
    }
226
}
227

            
228
impl std::fmt::Display for ConfigLoadError {
229
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230
        let s = self.0.to_string();
231
        write!(f, "{}", s)?;
232
        if s.contains("invalid escape") || s.contains("invalid hex escape") {
233
            write!(
234
                f,
235
                "   (If you wanted to include a literal \\ character, you need to escape it by writing two in a row: \\\\)"
236
            )?;
237
        }
238
        Ok(())
239
    }
240
}
241

            
242
impl std::error::Error for ConfigLoadError {
243
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
244
        // A `ConfigLoadError` isn't really a new higher-level error,
245
        // it just wraps an existing figment error and formats it a little differently.
246
        // Our `Display` implementation writes the `self.0` error message,
247
        // so here in `source()` we skip `self.0` and return *its* source error.
248
        // Otherwise an error formatter which iterates over error sources would print the same
249
        // error message twice.
250
        self.0.source()
251
    }
252
}
253

            
254
#[cfg(test)]
255
mod test {
256
    // @@ begin test lint list maintained by maint/add_warning @@
257
    #![allow(clippy::bool_assert_comparison)]
258
    #![allow(clippy::clone_on_copy)]
259
    #![allow(clippy::dbg_macro)]
260
    #![allow(clippy::mixed_attributes_style)]
261
    #![allow(clippy::print_stderr)]
262
    #![allow(clippy::print_stdout)]
263
    #![allow(clippy::single_char_pattern)]
264
    #![allow(clippy::unwrap_used)]
265
    #![allow(clippy::unchecked_time_subtraction)]
266
    #![allow(clippy::useless_vec)]
267
    #![allow(clippy::needless_pass_by_value)]
268
    #![allow(clippy::string_slice)] // See arti#2571
269
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
270
    use super::*;
271

            
272
    #[test]
273
    fn within() {
274
        let e1 = ConfigBuildError::MissingField {
275
            field: "lettuce".to_owned(),
276
        };
277
        let e2 = ConfigBuildError::Invalid {
278
            field: "tomato".to_owned(),
279
            problem: "too crunchy".to_owned(),
280
        };
281
        let e3 = ConfigBuildError::Inconsistent {
282
            fields: vec!["mayo".to_owned(), "avocado".to_owned()],
283
            problem: "pick one".to_owned(),
284
        };
285

            
286
        assert_eq!(
287
            &e1.within("sandwich").to_string(),
288
            "Field was not provided: sandwich.lettuce"
289
        );
290
        assert_eq!(
291
            &e2.within("sandwich").to_string(),
292
            "Value of sandwich.tomato was incorrect: too crunchy"
293
        );
294
        assert_eq!(
295
            &e3.within("sandwich").to_string(),
296
            r#"Fields ["sandwich.mayo", "sandwich.avocado"] are inconsistent: pick one"#
297
        );
298
    }
299

            
300
    #[derive(derive_builder::Builder, Debug, Clone)]
301
    #[builder(build_fn(error = "ConfigBuildError"))]
302
    #[allow(dead_code)]
303
    struct Cephalopod {
304
        // arms have suction cups for their whole length
305
        arms: u8,
306
        // Tentacles have suction cups at the ends
307
        tentacles: u8,
308
    }
309

            
310
    #[test]
311
    fn build_err() {
312
        let squid = CephalopodBuilder::default().arms(8).tentacles(2).build();
313
        let octopus = CephalopodBuilder::default().arms(8).build();
314
        assert!(squid.is_ok());
315
        let squid = squid.unwrap();
316
        assert_eq!(squid.arms, 8);
317
        assert_eq!(squid.tentacles, 2);
318
        assert!(octopus.is_err());
319
        assert_eq!(
320
            &octopus.unwrap_err().to_string(),
321
            "Field was not provided: tentacles"
322
        );
323
    }
324

            
325
    #[derive(derive_builder::Builder, Debug)]
326
    #[builder(build_fn(error = "ConfigBuildError"))]
327
    #[allow(dead_code)]
328
    struct Pet {
329
        #[builder(sub_builder)]
330
        best_friend: Cephalopod,
331
    }
332

            
333
    #[test]
334
    fn build_subfield_err() {
335
        let mut petb = PetBuilder::default();
336
        petb.best_friend().tentacles(3);
337
        let pet = petb.build();
338
        assert_eq!(
339
            pet.unwrap_err().to_string(),
340
            "Field was not provided: best_friend.arms"
341
        );
342
    }
343
}