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