Skip to main content

tor_config/
err.rs

1//! Declare error types.
2
3use std::path::PathBuf;
4
5use tor_basic_utils::PathExt as _;
6use 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]
18pub 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
62impl From<derive_builder::UninitializedFieldError> for ConfigBuildError {
63    fn from(val: derive_builder::UninitializedFieldError) -> Self {
64        ConfigBuildError::MissingField {
65            field: val.field_name().to_string(),
66        }
67    }
68}
69
70impl From<derive_builder::SubfieldBuildError<ConfigBuildError>> for ConfigBuildError {
71    fn from(e: derive_builder::SubfieldBuildError<ConfigBuildError>) -> Self {
72        let (field, problem) = e.into_parts();
73        problem.within(field)
74    }
75}
76
77impl From<void::Void> for ConfigBuildError {
78    fn from(value: void::Void) -> Self {
79        void::unreachable(value)
80    }
81}
82
83impl ConfigBuildError {
84    /// Return a new ConfigBuildError that prefixes its field name with
85    /// `prefix` and a dot.
86    #[must_use]
87    pub fn within(&self, prefix: &str) -> Self {
88        use ConfigBuildError::*;
89        let addprefix = |field: &str| format!("{}.{}", prefix, field);
90        match self {
91            MissingField { field } => MissingField {
92                field: addprefix(field),
93            },
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            Invalid { field, problem } => Invalid {
102                field: addprefix(field),
103                problem: problem.clone(),
104            },
105            Inconsistent { fields, problem } => Inconsistent {
106                fields: fields.iter().map(|f| addprefix(f)).collect(),
107                problem: problem.clone(),
108            },
109            NoCompileTimeSupport { field, problem } => NoCompileTimeSupport {
110                field: addprefix(field),
111                problem: problem.clone(),
112            },
113        }
114    }
115}
116
117impl 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]
126pub 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
150impl 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]
159pub 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]
193pub 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)]
201pub struct ConfigLoadError(figment::Error);
202
203impl 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    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        ConfigError::Load(ConfigLoadError(err))
213    }
214}
215
216impl 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
230impl 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)]
243mod 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}