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
595
    pub fn within(&self, prefix: &str) -> Self {
88
        use ConfigBuildError::*;
89
936
        let addprefix = |field: &str| format!("{}.{}", prefix, field);
90
595
        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
587
            Inconsistent { fields, problem } => Inconsistent {
106
924
                fields: fields.iter().map(|f| addprefix(f)).collect(),
107
587
                problem: problem.clone(),
108
            },
109
            NoCompileTimeSupport { field, problem } => NoCompileTimeSupport {
110
                field: addprefix(field),
111
                problem: problem.clone(),
112
            },
113
        }
114
595
    }
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
    /// The requested configuration is not supported in this situation
135
    ///
136
    /// Something, probably discovered at runtime, is not compatible with
137
    /// the specified configuration.
138
    ///
139
    /// This ought *not* to be returned when the configuration is simply not supported
140
    /// by this build of arti -
141
    /// that should be reported at config build type as `ConfigBuildError::Unsupported`.
142
    #[error("Configuration not supported in this situation: {0}")]
143
    UnsupportedSituation(String),
144

            
145
    /// There was a programming error somewhere in our code, or the calling code.
146
    #[error("Programming error")]
147
    Bug(#[from] tor_error::Bug),
148
}
149

            
150
impl HasKind for ReconfigureError {
151
    fn kind(&self) -> ErrorKind {
152
        ErrorKind::InvalidConfigTransition
153
    }
154
}
155

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

            
190
/// An error that occurred while trying to look up a configuration value.
191
#[derive(Clone, Debug, thiserror::Error)]
192
#[non_exhaustive]
193
pub enum ConfigGetValueError {
194
    /// Some internal error occurred.
195
    #[error("Internal error")]
196
    Bug(#[from] tor_error::Bug),
197
}
198

            
199
/// Wrapper for our an error type from our underlying configuration library.
200
#[derive(Debug, Clone)]
201
pub struct ConfigLoadError(figment::Error);
202

            
203
impl ConfigError {
204
    /// Wrap `err` as a ConfigError.
205
    ///
206
    /// This is not a From implementation, since we don't want to expose our
207
    /// underlying configuration library.
208
8
    pub(crate) fn from_cfg_err(err: figment::Error) -> Self {
209
        // TODO: It would be lovely to extract IO errors from figment::Error
210
        // and report them as Error::Io.  Unfortunately, it doesn't seem
211
        // possible to do that given the design of figment::Error.
212
8
        ConfigError::Load(ConfigLoadError(err))
213
8
    }
214
}
215

            
216
impl std::fmt::Display for ConfigLoadError {
217
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218
        let s = self.0.to_string();
219
        write!(f, "{}", s)?;
220
        if s.contains("invalid escape") || s.contains("invalid hex escape") {
221
            write!(
222
                f,
223
                "   (If you wanted to include a literal \\ character, you need to escape it by writing two in a row: \\\\)"
224
            )?;
225
        }
226
        Ok(())
227
    }
228
}
229

            
230
impl std::error::Error for ConfigLoadError {
231
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
232
        // A `ConfigLoadError` isn't really a new higher-level error,
233
        // it just wraps an existing figment error and formats it a little differently.
234
        // Our `Display` implementation writes the `self.0` error message,
235
        // so here in `source()` we skip `self.0` and return *its* source error.
236
        // Otherwise an error formatter which iterates over error sources would print the same
237
        // error message twice.
238
        self.0.source()
239
    }
240
}
241

            
242
#[cfg(test)]
243
mod test {
244
    // @@ begin test lint list maintained by maint/add_warning @@
245
    #![allow(clippy::bool_assert_comparison)]
246
    #![allow(clippy::clone_on_copy)]
247
    #![allow(clippy::dbg_macro)]
248
    #![allow(clippy::mixed_attributes_style)]
249
    #![allow(clippy::print_stderr)]
250
    #![allow(clippy::print_stdout)]
251
    #![allow(clippy::single_char_pattern)]
252
    #![allow(clippy::unwrap_used)]
253
    #![allow(clippy::unchecked_time_subtraction)]
254
    #![allow(clippy::useless_vec)]
255
    #![allow(clippy::needless_pass_by_value)]
256
    #![allow(clippy::string_slice)] // See arti#2571
257
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
258
    use super::*;
259

            
260
    #[test]
261
    fn within() {
262
        let e1 = ConfigBuildError::MissingField {
263
            field: "lettuce".to_owned(),
264
        };
265
        let e2 = ConfigBuildError::Invalid {
266
            field: "tomato".to_owned(),
267
            problem: "too crunchy".to_owned(),
268
        };
269
        let e3 = ConfigBuildError::Inconsistent {
270
            fields: vec!["mayo".to_owned(), "avocado".to_owned()],
271
            problem: "pick one".to_owned(),
272
        };
273

            
274
        assert_eq!(
275
            &e1.within("sandwich").to_string(),
276
            "Field was not provided: sandwich.lettuce"
277
        );
278
        assert_eq!(
279
            &e2.within("sandwich").to_string(),
280
            "Value of sandwich.tomato was incorrect: too crunchy"
281
        );
282
        assert_eq!(
283
            &e3.within("sandwich").to_string(),
284
            r#"Fields ["sandwich.mayo", "sandwich.avocado"] are inconsistent: pick one"#
285
        );
286
    }
287

            
288
    #[derive(derive_builder::Builder, Debug, Clone)]
289
    #[builder(build_fn(error = "ConfigBuildError"))]
290
    #[allow(dead_code)]
291
    struct Cephalopod {
292
        // arms have suction cups for their whole length
293
        arms: u8,
294
        // Tentacles have suction cups at the ends
295
        tentacles: u8,
296
    }
297

            
298
    #[test]
299
    fn build_err() {
300
        let squid = CephalopodBuilder::default().arms(8).tentacles(2).build();
301
        let octopus = CephalopodBuilder::default().arms(8).build();
302
        assert!(squid.is_ok());
303
        let squid = squid.unwrap();
304
        assert_eq!(squid.arms, 8);
305
        assert_eq!(squid.tentacles, 2);
306
        assert!(octopus.is_err());
307
        assert_eq!(
308
            &octopus.unwrap_err().to_string(),
309
            "Field was not provided: tentacles"
310
        );
311
    }
312

            
313
    #[derive(derive_builder::Builder, Debug)]
314
    #[builder(build_fn(error = "ConfigBuildError"))]
315
    #[allow(dead_code)]
316
    struct Pet {
317
        #[builder(sub_builder)]
318
        best_friend: Cephalopod,
319
    }
320

            
321
    #[test]
322
    fn build_subfield_err() {
323
        let mut petb = PetBuilder::default();
324
        petb.best_friend().tentacles(3);
325
        let pet = petb.build();
326
        assert_eq!(
327
            pet.unwrap_err().to_string(),
328
            "Field was not provided: best_friend.arms"
329
        );
330
    }
331
}