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    /// 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
162impl 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]
171pub 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]
205pub 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)]
213pub struct ConfigLoadError(figment::Error);
214
215impl 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    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        ConfigError::Load(ConfigLoadError(err))
225    }
226}
227
228impl 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
242impl 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)]
255mod 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}