1
1
//! `arti-config`: Tools for configuration management in Arti
2
//!
3
//! # Overview
4
//!
5
//! This crate is part of
6
//! [Arti](https://gitlab.torproject.org/tpo/core/arti/), a project to
7
//! implement [Tor](https://www.torproject.org/) in Rust.
8
//!
9
//! It provides a client configuration tool using using `serde` and `config`,
10
//! plus extra features defined here for convenience.
11
//!
12
//! # ⚠ Stability Warning ⚠
13
//!
14
//! The design of this crate, and of the configuration system for
15
//! Arti, is likely to change significantly before the release of Arti
16
//! 1.0.0.  For more information see ticket [#285].
17
//!
18
//! [#285]: https://gitlab.torproject.org/tpo/core/arti/-/issues/285
19

            
20
#![deny(missing_docs)]
21
#![warn(noop_method_call)]
22
#![deny(unreachable_pub)]
23
#![warn(clippy::all)]
24
#![deny(clippy::await_holding_lock)]
25
#![deny(clippy::cargo_common_metadata)]
26
#![deny(clippy::cast_lossless)]
27
#![deny(clippy::checked_conversions)]
28
#![warn(clippy::cognitive_complexity)]
29
#![deny(clippy::debug_assert_with_mut_call)]
30
#![deny(clippy::exhaustive_enums)]
31
#![deny(clippy::exhaustive_structs)]
32
#![deny(clippy::expl_impl_clone_on_copy)]
33
#![deny(clippy::fallible_impl_from)]
34
#![deny(clippy::implicit_clone)]
35
#![deny(clippy::large_stack_arrays)]
36
#![warn(clippy::manual_ok_or)]
37
#![deny(clippy::missing_docs_in_private_items)]
38
#![deny(clippy::missing_panics_doc)]
39
#![warn(clippy::needless_borrow)]
40
#![warn(clippy::needless_pass_by_value)]
41
#![warn(clippy::option_option)]
42
#![warn(clippy::rc_buffer)]
43
#![deny(clippy::ref_option_ref)]
44
#![warn(clippy::semicolon_if_nothing_returned)]
45
#![warn(clippy::trait_duplication_in_bounds)]
46
#![deny(clippy::unnecessary_wraps)]
47
#![warn(clippy::unseparated_literal_suffix)]
48
#![deny(clippy::unwrap_used)]
49

            
50
mod cmdline;
51
mod options;
52

            
53
pub use cmdline::CmdLine;
54
pub use options::{
55
    ApplicationConfig, ApplicationConfigBuilder, ArtiConfig, ArtiConfigBuilder, LogRotation,
56
    LogfileConfig, LogfileConfigBuilder, LoggingConfig, LoggingConfigBuilder, ProxyConfig,
57
    ProxyConfigBuilder,
58
};
59
use tor_config::CfgPath;
60

            
61
/// The synchronous configuration builder type we use.
62
///
63
/// (This is a type alias that config should really provide.)
64
type ConfigBuilder = config::builder::ConfigBuilder<config::builder::DefaultState>;
65

            
66
use std::path::{Path, PathBuf};
67

            
68
/// A description of where to find our configuration options.
69
#[derive(Clone, Debug, Default)]
70
pub struct ConfigurationSources {
71
    /// List of files to read (in order).
72
    files: Vec<(PathBuf, MustRead)>,
73
    /// A list of command-line options to apply after parsing the files.
74
    options: Vec<String>,
75
}
76

            
77
/// Rules for whether we should proceed if a configuration file is unreadable.
78
///
79
/// Some files (like the default configuration file) are okay to skip if they
80
/// aren't present. Others (like those specified on the command line) really
81
/// need to be there.
82
5
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
83
enum MustRead {
84
    /// This file is okay to skip if it isn't present,
85
    TolerateAbsence,
86

            
87
    /// This file must be present and readable.
88
    MustRead,
89
}
90

            
91
impl ConfigurationSources {
92
    /// Create a new empty [`ConfigurationSources`].
93
    pub fn new() -> Self {
94
        Self::default()
95
    }
96

            
97
    /// Add `p` to the list of files that we want to read configuration from.
98
    ///
99
    /// Configuration files are loaded and applied in the order that they are
100
    /// added to this object.
101
    ///
102
    /// If the listed file is absent, loading the configuration won't succeed.
103
    pub fn push_file<P: AsRef<Path>>(&mut self, p: P) {
104
        self.files.push((p.as_ref().to_owned(), MustRead::MustRead));
105
    }
106

            
107
    /// As `push_file`, but if the listed file can't be loaded, loading the
108
    /// configuration can still succeed.
109
    pub fn push_optional_file<P: AsRef<Path>>(&mut self, p: P) {
110
        self.files
111
            .push((p.as_ref().to_owned(), MustRead::TolerateAbsence));
112
    }
113

            
114
    /// Add `s` to the list of overridden options to apply to our configuration.
115
    ///
116
    /// Options are applied after all configuration files are loaded, in the
117
    /// order that they are added to this object.
118
    ///
119
    /// The format for `s` is as in [`CmdLine`].
120
    pub fn push_option<S: ToOwned<Owned = String> + ?Sized>(&mut self, option: &S) {
121
        self.options.push(option.to_owned());
122
    }
123

            
124
    /// Return an iterator over the files that we care about.
125
    pub fn files(&self) -> impl Iterator<Item = &Path> {
126
        self.files.iter().map(|(f, _)| f.as_path())
127
    }
128

            
129
    /// Load the configuration into a new [`config::Config`].
130
    pub fn load(&self) -> Result<config::Config, config::ConfigError> {
131
        let mut builder = config::Config::builder();
132
        builder = builder.add_source(config::File::from_str(
133
            options::ARTI_DEFAULTS,
134
            config::FileFormat::Toml,
135
        ));
136
        builder = add_sources(builder, &self.files, &self.options);
137
        builder.build()
138
    }
139
}
140

            
141
/// Add every file and commandline source to `builder`, returning a new
142
/// builder.
143
3
fn add_sources<P>(
144
3
    mut builder: ConfigBuilder,
145
3
    files: &[(P, MustRead)],
146
3
    opts: &[String],
147
3
) -> ConfigBuilder
148
3
where
149
3
    P: AsRef<Path>,
150
3
{
151
8
    for (path, must_read) in files {
152
5
        // Not going to use File::with_name here, since it doesn't
153
5
        // quite do what we want.
154
5
        let f: config::File<_, _> = path.as_ref().into();
155
5
        let required = must_read == &MustRead::MustRead;
156
5
        builder = builder.add_source(f.format(config::FileFormat::Toml).required(required));
157
5
    }
158

            
159
3
    let mut cmdline = CmdLine::new();
160
4
    for opt in opts {
161
1
        cmdline.push_toml_line(opt.clone());
162
1
    }
163
3
    builder = builder.add_source(cmdline);
164
3

            
165
3
    builder
166
3
}
167

            
168
/// Return a filename for the default user configuration file.
169
1
pub fn default_config_file() -> Option<PathBuf> {
170
1
    CfgPath::new("${ARTI_CONFIG}/arti.toml".into()).path().ok()
171
1
}
172

            
173
#[cfg(test)]
174
mod test {
175
    #![allow(clippy::unwrap_used)]
176
    use super::*;
177
    use tempfile::tempdir;
178

            
179
    static EX_TOML: &str = "
180
[hello]
181
world = \"stuff\"
182
friends = 4242
183
";
184

            
185
    /// Load from a set of files and option strings, without taking
186
    /// the arti defaults into account.
187
    fn load_nodefaults<P: AsRef<Path>>(
188
        files: &[(P, MustRead)],
189
        opts: &[String],
190
    ) -> Result<config::Config, config::ConfigError> {
191
        add_sources(config::Config::builder(), files, opts).build()
192
    }
193

            
194
    #[test]
195
    fn non_required_file() {
196
        let td = tempdir().unwrap();
197
        let dflt = td.path().join("a_file");
198
        let files = vec![(dflt, MustRead::TolerateAbsence)];
199
        load_nodefaults(&files, Default::default()).unwrap();
200
    }
201

            
202
    static EX2_TOML: &str = "
203
[hello]
204
world = \"nonsense\"
205
";
206

            
207
    #[test]
208
    fn both_required_and_not() {
209
        let td = tempdir().unwrap();
210
        let dflt = td.path().join("a_file");
211
        let cf = td.path().join("other_file");
212
        std::fs::write(&cf, EX2_TOML).unwrap();
213
        let files = vec![(dflt, MustRead::TolerateAbsence), (cf, MustRead::MustRead)];
214
        let c = load_nodefaults(&files, Default::default()).unwrap();
215

            
216
        assert!(c.get_string("hello.friends").is_err());
217
        assert_eq!(c.get_string("hello.world").unwrap(), "nonsense".to_string());
218
    }
219

            
220
    #[test]
221
    fn load_two_files_with_cmdline() {
222
        let td = tempdir().unwrap();
223
        let cf1 = td.path().join("a_file");
224
        let cf2 = td.path().join("other_file");
225
        std::fs::write(&cf1, EX_TOML).unwrap();
226
        std::fs::write(&cf2, EX2_TOML).unwrap();
227
        let v = vec![(cf1, MustRead::TolerateAbsence), (cf2, MustRead::MustRead)];
228
        let v2 = vec!["other.var=present".to_string()];
229
        let c = load_nodefaults(&v, &v2).unwrap();
230

            
231
        assert_eq!(c.get_string("hello.friends").unwrap(), "4242".to_string());
232
        assert_eq!(c.get_string("hello.world").unwrap(), "nonsense".to_string());
233
        assert_eq!(c.get_string("other.var").unwrap(), "present".to_string());
234
    }
235

            
236
    #[test]
237
    fn check_default() {
238
        // We don't want to second-guess the directories crate too much
239
        // here, so we'll just make sure it does _something_ plausible.
240

            
241
        let dflt = default_config_file().unwrap();
242
        assert!(dflt.ends_with("arti.toml"));
243
    }
244
}