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    /// 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
53impl From<derive_builder::UninitializedFieldError> for ConfigBuildError {
54    fn from(val: derive_builder::UninitializedFieldError) -> Self {
55        ConfigBuildError::MissingField {
56            field: val.field_name().to_string(),
57        }
58    }
59}
60
61impl From<derive_builder::SubfieldBuildError<ConfigBuildError>> for ConfigBuildError {
62    fn from(e: derive_builder::SubfieldBuildError<ConfigBuildError>) -> Self {
63        let (field, problem) = e.into_parts();
64        problem.within(field)
65    }
66}
67
68impl From<void::Void> for ConfigBuildError {
69    fn from(value: void::Void) -> Self {
70        void::unreachable(value)
71    }
72}
73
74impl ConfigBuildError {
75    /// Return a new ConfigBuildError that prefixes its field name with
76    /// `prefix` and a dot.
77    #[must_use]
78    pub fn within(&self, prefix: &str) -> Self {
79        use ConfigBuildError::*;
80        let addprefix = |field: &str| format!("{}.{}", prefix, field);
81        match self {
82            MissingField { field } => MissingField {
83                field: addprefix(field),
84            },
85            Invalid { field, problem } => Invalid {
86                field: addprefix(field),
87                problem: problem.clone(),
88            },
89            Inconsistent { fields, problem } => Inconsistent {
90                fields: fields.iter().map(|f| addprefix(f)).collect(),
91                problem: problem.clone(),
92            },
93            NoCompileTimeSupport { field, problem } => NoCompileTimeSupport {
94                field: addprefix(field),
95                problem: problem.clone(),
96            },
97        }
98    }
99}
100
101impl HasKind for ConfigBuildError {
102    fn kind(&self) -> ErrorKind {
103        ErrorKind::InvalidConfig
104    }
105}
106
107/// An error caused when attempting to reconfigure an existing Arti client, or one of its modules.
108#[derive(Debug, Clone, thiserror::Error)]
109#[non_exhaustive]
110pub enum ReconfigureError {
111    /// Tried to change a field that cannot change on a running client.
112    #[error("Cannot change {field} on a running client.")]
113    CannotChange {
114        /// The field (or fields) that we tried to change.
115        field: String,
116    },
117
118    /// The requested configuration is not supported in this situation
119    ///
120    /// Something, probably discovered at runtime, is not compatible with
121    /// the specified configuration.
122    ///
123    /// This ought *not* to be returned when the configuration is simply not supported
124    /// by this build of arti -
125    /// that should be reported at config build type as `ConfigBuildError::Unsupported`.
126    #[error("Configuration not supported in this situation: {0}")]
127    UnsupportedSituation(String),
128
129    /// There was a programming error somewhere in our code, or the calling code.
130    #[error("Programming error")]
131    Bug(#[from] tor_error::Bug),
132}
133
134impl HasKind for ReconfigureError {
135    fn kind(&self) -> ErrorKind {
136        ErrorKind::InvalidConfigTransition
137    }
138}
139
140/// An error that occurs while trying to read and process our configuration.
141#[derive(Debug, Clone, thiserror::Error)]
142#[non_exhaustive]
143pub enum ConfigError {
144    /// We encoundered a problem checking file permissions (for example, no such file)
145    #[error("Problem accessing configuration file(s)")]
146    FileAccess(#[source] fs_mistrust::Error),
147    /// We encoundered a problem checking file permissions (for example, no such file)
148    ///
149    /// This variant name is misleading - see the docs for [`fs_mistrust::Error`].
150    /// Please use [`ConfigError::FileAccess`] instead.
151    #[deprecated = "use ConfigError::FileAccess instead"]
152    #[error("Problem accessing configuration file(s)")]
153    Permissions(#[source] fs_mistrust::Error),
154    /// Our underlying configuration library gave an error while loading our
155    /// configuration.
156    #[error("Couldn't load configuration")]
157    Load(#[source] ConfigLoadError),
158    /// Encountered an IO error with a configuration file or directory.
159    ///
160    /// Note that some IO errors may be reported as `Load` errors,
161    /// due to limitations of the underlying library.
162    #[error("IoError while {} {}", action, path.display_lossy())]
163    Io {
164        /// The action while we were trying to perform
165        action: &'static str,
166        /// The path we were trying to do it to.
167        path: PathBuf,
168        /// The underlying problem
169        #[source]
170        err: std::sync::Arc<std::io::Error>,
171    },
172}
173
174/// An error that occurred while trying to look up a configuration value.
175#[derive(Clone, Debug, thiserror::Error)]
176#[non_exhaustive]
177pub enum ConfigGetValueError {
178    /// Some internal error occurred.
179    #[error("Internal error")]
180    Bug(#[from] tor_error::Bug),
181}
182
183/// Wrapper for our an error type from our underlying configuration library.
184#[derive(Debug, Clone)]
185pub struct ConfigLoadError(figment::Error);
186
187impl ConfigError {
188    /// Wrap `err` as a ConfigError.
189    ///
190    /// This is not a From implementation, since we don't want to expose our
191    /// underlying configuration library.
192    pub(crate) fn from_cfg_err(err: figment::Error) -> Self {
193        // TODO: It would be lovely to extract IO errors from figment::Error
194        // and report them as Error::Io.  Unfortunately, it doesn't seem
195        // possible to do that given the design of figment::Error.
196        ConfigError::Load(ConfigLoadError(err))
197    }
198}
199
200impl std::fmt::Display for ConfigLoadError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        let s = self.0.to_string();
203        write!(f, "{}", s)?;
204        if s.contains("invalid escape") || s.contains("invalid hex escape") {
205            write!(
206                f,
207                "   (If you wanted to include a literal \\ character, you need to escape it by writing two in a row: \\\\)"
208            )?;
209        }
210        Ok(())
211    }
212}
213
214impl std::error::Error for ConfigLoadError {
215    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
216        // A `ConfigLoadError` isn't really a new higher-level error,
217        // it just wraps an existing figment error and formats it a little differently.
218        // Our `Display` implementation writes the `self.0` error message,
219        // so here in `source()` we skip `self.0` and return *its* source error.
220        // Otherwise an error formatter which iterates over error sources would print the same
221        // error message twice.
222        self.0.source()
223    }
224}
225
226#[cfg(test)]
227mod test {
228    // @@ begin test lint list maintained by maint/add_warning @@
229    #![allow(clippy::bool_assert_comparison)]
230    #![allow(clippy::clone_on_copy)]
231    #![allow(clippy::dbg_macro)]
232    #![allow(clippy::mixed_attributes_style)]
233    #![allow(clippy::print_stderr)]
234    #![allow(clippy::print_stdout)]
235    #![allow(clippy::single_char_pattern)]
236    #![allow(clippy::unwrap_used)]
237    #![allow(clippy::unchecked_time_subtraction)]
238    #![allow(clippy::useless_vec)]
239    #![allow(clippy::needless_pass_by_value)]
240    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
241    use super::*;
242
243    #[test]
244    fn within() {
245        let e1 = ConfigBuildError::MissingField {
246            field: "lettuce".to_owned(),
247        };
248        let e2 = ConfigBuildError::Invalid {
249            field: "tomato".to_owned(),
250            problem: "too crunchy".to_owned(),
251        };
252        let e3 = ConfigBuildError::Inconsistent {
253            fields: vec!["mayo".to_owned(), "avocado".to_owned()],
254            problem: "pick one".to_owned(),
255        };
256
257        assert_eq!(
258            &e1.within("sandwich").to_string(),
259            "Field was not provided: sandwich.lettuce"
260        );
261        assert_eq!(
262            &e2.within("sandwich").to_string(),
263            "Value of sandwich.tomato was incorrect: too crunchy"
264        );
265        assert_eq!(
266            &e3.within("sandwich").to_string(),
267            r#"Fields ["sandwich.mayo", "sandwich.avocado"] are inconsistent: pick one"#
268        );
269    }
270
271    #[derive(derive_builder::Builder, Debug, Clone)]
272    #[builder(build_fn(error = "ConfigBuildError"))]
273    #[allow(dead_code)]
274    struct Cephalopod {
275        // arms have suction cups for their whole length
276        arms: u8,
277        // Tentacles have suction cups at the ends
278        tentacles: u8,
279    }
280
281    #[test]
282    fn build_err() {
283        let squid = CephalopodBuilder::default().arms(8).tentacles(2).build();
284        let octopus = CephalopodBuilder::default().arms(8).build();
285        assert!(squid.is_ok());
286        let squid = squid.unwrap();
287        assert_eq!(squid.arms, 8);
288        assert_eq!(squid.tentacles, 2);
289        assert!(octopus.is_err());
290        assert_eq!(
291            &octopus.unwrap_err().to_string(),
292            "Field was not provided: tentacles"
293        );
294    }
295
296    #[derive(derive_builder::Builder, Debug)]
297    #[builder(build_fn(error = "ConfigBuildError"))]
298    #[allow(dead_code)]
299    struct Pet {
300        #[builder(sub_builder)]
301        best_friend: Cephalopod,
302    }
303
304    #[test]
305    fn build_subfield_err() {
306        let mut petb = PetBuilder::default();
307        petb.best_friend().tentacles(3);
308        let pet = petb.build();
309        assert_eq!(
310            pet.unwrap_err().to_string(),
311            "Field was not provided: best_friend.arms"
312        );
313    }
314}