1
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![warn(clippy::cognitive_complexity)]
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_duration_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
45

            
46
use std::collections::HashMap;
47
use std::path::{Path, PathBuf};
48

            
49
use serde::{Deserialize, Serialize};
50
use std::borrow::Cow;
51
#[cfg(feature = "expand-paths")]
52
use {directories::BaseDirs, once_cell::sync::Lazy};
53

            
54
use tor_error::{ErrorKind, HasKind};
55

            
56
#[cfg(all(test, feature = "expand-paths"))]
57
use std::ffi::OsStr;
58

            
59
#[cfg(feature = "address")]
60
pub mod addr;
61

            
62
#[cfg(feature = "arti-client")]
63
mod arti_client_paths;
64

            
65
#[cfg(feature = "arti-client")]
66
#[cfg_attr(docsrs, doc(cfg(feature = "arti-client")))]
67
pub use arti_client_paths::arti_client_base_resolver;
68

            
69
/// A path in a configuration file: tilde expansion is performed, along
70
/// with expansion of variables provided by a [`CfgPathResolver`].
71
///
72
/// The tilde expansion is performed using the home directory given by the
73
/// `directories` crate, which may be based on an environment variable. For more
74
/// information, see [`BaseDirs::home_dir`](directories::BaseDirs::home_dir).
75
///
76
/// Alternatively, a `CfgPath` can contain literal `PathBuf`, which will not be expanded.
77
564
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
78
#[serde(transparent)]
79
pub struct CfgPath(PathInner);
80

            
81
/// Inner implementation of CfgPath
82
///
83
/// `PathInner` exists to avoid making the variants part of the public Rust API
84
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
85
#[serde(untagged)]
86
enum PathInner {
87
    /// A path that should be used literally, with no expansion.
88
    Literal(LiteralPath),
89
    /// A path that should be expanded from a string using ShellExpand.
90
    Shell(String),
91
}
92

            
93
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
94
/// Inner implementation of PathInner:Literal
95
///
96
/// `LiteralPath` exists to arrange that `PathInner::Literal`'s (de)serialization
97
/// does not overlap with `PathInner::Shell`'s.
98
struct LiteralPath {
99
    /// The underlying `PathBuf`.
100
    literal: PathBuf,
101
}
102

            
103
/// An error that has occurred while expanding a path.
104
#[derive(thiserror::Error, Debug, Clone)]
105
#[non_exhaustive]
106
#[cfg_attr(test, derive(PartialEq))]
107
pub enum CfgPathError {
108
    /// The path contained a variable we didn't recognize.
109
    #[error("Unrecognized variable {0} in path")]
110
    UnknownVar(String),
111
    /// We couldn't construct a ProjectDirs object.
112
    #[error("Couldn't determine XDG Project Directories, needed to resolve a path; probably, unable to determine HOME directory")]
113
    NoProjectDirs,
114
    /// We couldn't construct a BaseDirs object.
115
    #[error("Can't construct base directories to resolve a path element")]
116
    NoBaseDirs,
117
    /// We couldn't find our current binary path.
118
    #[error("Can't find the path to the current binary")]
119
    NoProgramPath,
120
    /// We couldn't find the directory path containing the current binary.
121
    #[error("Can't find the directory of the current binary")]
122
    NoProgramDir,
123
    /// We couldn't convert a string to a valid path on the OS.
124
    //
125
    // NOTE: This is not currently generated. Shall we remove it?
126
    #[error("Invalid path string: {0:?}")]
127
    InvalidString(String),
128
    /// Variable interpolation (`$`) attempted, but not compiled in
129
    #[error("Variable interpolation $ is not supported (tor-config/expand-paths feature disabled)); $ must still be doubled")]
130
    VariableInterpolationNotSupported(String),
131
    /// Home dir interpolation (`~`) attempted, but not compiled in
132
    #[error("Home dir ~/ is not supported (tor-config/expand-paths feature disabled)")]
133
    HomeDirInterpolationNotSupported(String),
134
}
135

            
136
impl HasKind for CfgPathError {
137
    fn kind(&self) -> ErrorKind {
138
        use CfgPathError as E;
139
        use ErrorKind as EK;
140
        match self {
141
            E::UnknownVar(_) | E::InvalidString(_) => EK::InvalidConfig,
142
            E::NoProjectDirs | E::NoBaseDirs => EK::NoHomeDirectory,
143
            E::NoProgramPath | E::NoProgramDir => EK::InvalidConfig,
144
            E::VariableInterpolationNotSupported(_) | E::HomeDirInterpolationNotSupported(_) => {
145
                EK::FeatureDisabled
146
            }
147
        }
148
    }
149
}
150

            
151
/// A variable resolver for paths in a configuration file.
152
///
153
/// Typically there should be one resolver per application, and the application should share the
154
/// resolver throughout the application to have consistent path variable expansions. Typically the
155
/// application would create its own resolver with its application-specific variables, but note that
156
/// `TorClientConfig` is an exception which does not accept a resolver from the application and
157
/// instead generates its own. This is done for backwards compatibility reasons.
158
///
159
/// Once constructed, they are used during calls to [`CfgPath::path`] to expand variables in the
160
/// path.
161
#[derive(Clone, Debug, Default)]
162
pub struct CfgPathResolver {
163
    /// The variables and their values. The values can be an `Err` if the variable is expected but
164
    /// can't be expanded.
165
    vars: HashMap<String, Result<Cow<'static, Path>, CfgPathError>>,
166
}
167

            
168
impl CfgPathResolver {
169
    /// Get the value for a given variable name.
170
    #[cfg(feature = "expand-paths")]
171
1671
    fn get_var(&self, var: &str) -> Result<Cow<'static, Path>, CfgPathError> {
172
1671
        match self.vars.get(var) {
173
1618
            Some(val) => val.clone(),
174
53
            None => Err(CfgPathError::UnknownVar(var.to_owned())),
175
        }
176
1671
    }
177

            
178
    /// Set a variable `var` that will be replaced with `val` when a [`CfgPath`] is expanded.
179
    ///
180
    /// Setting an `Err` is useful when a variable is supported, but for whatever reason it can't be
181
    /// expanded, and you'd like to return a more-specific error. An example might be a `USER_HOME`
182
    /// variable for a user that doesn't have a `HOME` environment variable set.
183
    ///
184
    /// ```
185
    /// use std::path::Path;
186
    /// use tor_config_path::{CfgPath, CfgPathResolver};
187
    ///
188
    /// let mut path_resolver = CfgPathResolver::default();
189
    /// path_resolver.set_var("FOO", Ok(Path::new("/foo").to_owned().into()));
190
    ///
191
    /// let path = CfgPath::new("${FOO}/bar".into());
192
    ///
193
    /// #[cfg(feature = "expand-paths")]
194
    /// assert_eq!(path.path(&path_resolver).unwrap(), Path::new("/foo/bar"));
195
    /// #[cfg(not(feature = "expand-paths"))]
196
    /// assert!(path.path(&path_resolver).is_err());
197
    /// ```
198
22596
    pub fn set_var(
199
22596
        &mut self,
200
22596
        var: impl Into<String>,
201
22596
        val: Result<Cow<'static, Path>, CfgPathError>,
202
22596
    ) {
203
22596
        self.vars.insert(var.into(), val);
204
22596
    }
205

            
206
    /// Helper to create a `CfgPathResolver` from str `(name, value)` pairs.
207
    #[cfg(all(test, feature = "expand-paths"))]
208
24
    fn from_pairs<K, V>(vars: impl IntoIterator<Item = (K, V)>) -> CfgPathResolver
209
24
    where
210
24
        K: Into<String>,
211
24
        V: AsRef<OsStr>,
212
24
    {
213
24
        let mut path_resolver = CfgPathResolver::default();
214
24
        for (name, val) in vars.into_iter() {
215
24
            let val = Path::new(val.as_ref()).to_owned();
216
24
            path_resolver.set_var(name, Ok(val.into()));
217
24
        }
218
24
        path_resolver
219
24
    }
220
}
221

            
222
impl CfgPath {
223
    /// Create a new configuration path
224
8788
    pub fn new(s: String) -> Self {
225
8788
        CfgPath(PathInner::Shell(s))
226
8788
    }
227

            
228
    /// Construct a new `CfgPath` designating a literal not-to-be-expanded `PathBuf`
229
641
    pub fn new_literal<P: Into<PathBuf>>(path: P) -> Self {
230
641
        CfgPath(PathInner::Literal(LiteralPath {
231
641
            literal: path.into(),
232
641
        }))
233
641
    }
234

            
235
    /// Return the path on disk designated by this `CfgPath`.
236
    ///
237
    /// Variables may or may not be resolved using `path_resolver`, depending on whether the
238
    /// `expand-paths` feature is enabled or not.
239
3193
    pub fn path(&self, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
240
3193
        match &self.0 {
241
2151
            PathInner::Shell(s) => expand(s, path_resolver),
242
1042
            PathInner::Literal(LiteralPath { literal }) => Ok(literal.clone()),
243
        }
244
3193
    }
245

            
246
    /// If the `CfgPath` is a string that should be expanded, return the (unexpanded) string,
247
    ///
248
    /// Before use, this string would have be to expanded.  So if you want a path to actually use,
249
    /// call `path` instead.
250
    ///
251
    /// Returns `None` if the `CfgPath` is a literal `PathBuf` not intended for expansion.
252
12
    pub fn as_unexpanded_str(&self) -> Option<&str> {
253
12
        match &self.0 {
254
6
            PathInner::Shell(s) => Some(s),
255
6
            PathInner::Literal(_) => None,
256
        }
257
12
    }
258

            
259
    /// If the `CfgPath` designates a literal not-to-be-expanded `Path`, return a reference to it
260
    ///
261
    /// Returns `None` if the `CfgPath` is a string which should be expanded, which is the
262
    /// usual case.
263
12
    pub fn as_literal_path(&self) -> Option<&Path> {
264
12
        match &self.0 {
265
6
            PathInner::Shell(_) => None,
266
6
            PathInner::Literal(LiteralPath { literal }) => Some(literal),
267
        }
268
12
    }
269
}
270

            
271
impl std::fmt::Display for CfgPath {
272
306
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273
306
        match &self.0 {
274
2
            PathInner::Literal(LiteralPath { literal }) => write!(fmt, "{:?} [exactly]", literal),
275
304
            PathInner::Shell(s) => s.fmt(fmt),
276
        }
277
306
    }
278
}
279

            
280
/// Return the user's home directory used when expanding paths.
281
// This is public so that applications which want to support for example a `USER_HOME` variable can
282
// use the same home directory expansion that we use in this crate for `~` expansion.
283
#[cfg(feature = "expand-paths")]
284
3817
pub fn home() -> Result<&'static Path, CfgPathError> {
285
    /// Lazy cell holding the home directory.
286
    static HOME_DIR: Lazy<Option<PathBuf>> =
287
754
        Lazy::new(|| Some(BaseDirs::new()?.home_dir().to_owned()));
288
3817
    HOME_DIR
289
3817
        .as_ref()
290
3817
        .map(PathBuf::as_path)
291
3817
        .ok_or(CfgPathError::NoBaseDirs)
292
3817
}
293

            
294
/// Helper: expand a directory given as a string.
295
#[cfg(feature = "expand-paths")]
296
2151
fn expand(s: &str, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
297
2151
    let path = shellexpand::path::full_with_context(
298
2151
        s,
299
2152
        || home().ok(),
300
2199
        |x| path_resolver.get_var(x).map(Some),
301
2151
    );
302
2155
    Ok(path.map_err(|e| e.cause)?.into_owned())
303
2151
}
304

            
305
/// Helper: convert a string to a path without expansion.
306
#[cfg(not(feature = "expand-paths"))]
307
fn expand(input: &str, _: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
308
    // We must still de-duplicate `$` and reject `~/`,, so that the behaviour is a superset
309
    if input.starts_with('~') {
310
        return Err(CfgPathError::HomeDirInterpolationNotSupported(input.into()));
311
    }
312

            
313
    let mut out = String::with_capacity(input.len());
314
    let mut s = input;
315
    while let Some((lhs, rhs)) = s.split_once('$') {
316
        if let Some(rhs) = rhs.strip_prefix('$') {
317
            // deduplicate the $
318
            out += lhs;
319
            out += "$";
320
            s = rhs;
321
        } else {
322
            return Err(CfgPathError::VariableInterpolationNotSupported(
323
                input.into(),
324
            ));
325
        }
326
    }
327
    out += s;
328
    Ok(out.into())
329
}
330

            
331
#[cfg(all(test, feature = "expand-paths"))]
332
mod test {
333
    #![allow(clippy::unwrap_used)]
334
    use super::*;
335

            
336
    #[test]
337
    fn expand_no_op() {
338
        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
339

            
340
        let p = CfgPath::new("Hello/world".to_string());
341
        assert_eq!(p.to_string(), "Hello/world".to_string());
342
        assert_eq!(p.path(&r).unwrap().to_str(), Some("Hello/world"));
343

            
344
        let p = CfgPath::new("/usr/local/foo".to_string());
345
        assert_eq!(p.to_string(), "/usr/local/foo".to_string());
346
        assert_eq!(p.path(&r).unwrap().to_str(), Some("/usr/local/foo"));
347
    }
348

            
349
    #[cfg(not(target_family = "windows"))]
350
    #[test]
351
    fn expand_home() {
352
        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
353

            
354
        let p = CfgPath::new("~/.arti/config".to_string());
355
        assert_eq!(p.to_string(), "~/.arti/config".to_string());
356

            
357
        let expected = dirs::home_dir().unwrap().join(".arti/config");
358
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
359

            
360
        let p = CfgPath::new("${USER_HOME}/.arti/config".to_string());
361
        assert_eq!(p.to_string(), "${USER_HOME}/.arti/config".to_string());
362
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
363
    }
364

            
365
    #[cfg(target_family = "windows")]
366
    #[test]
367
    fn expand_home() {
368
        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
369

            
370
        let p = CfgPath::new("~\\.arti\\config".to_string());
371
        assert_eq!(p.to_string(), "~\\.arti\\config".to_string());
372

            
373
        let expected = dirs::home_dir().unwrap().join(".arti\\config");
374
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
375

            
376
        let p = CfgPath::new("${USER_HOME}\\.arti\\config".to_string());
377
        assert_eq!(p.to_string(), "${USER_HOME}\\.arti\\config".to_string());
378
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
379
    }
380

            
381
    #[test]
382
    fn expand_bogus() {
383
        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
384

            
385
        let p = CfgPath::new("${ARTI_WOMBAT}/example".to_string());
386
        assert_eq!(p.to_string(), "${ARTI_WOMBAT}/example".to_string());
387

            
388
        assert!(matches!(p.path(&r), Err(CfgPathError::UnknownVar(_))));
389
        assert_eq!(
390
            &p.path(&r).unwrap_err().to_string(),
391
            "Unrecognized variable ARTI_WOMBAT in path"
392
        );
393
    }
394

            
395
    #[test]
396
    fn literal() {
397
        let r = CfgPathResolver::from_pairs([("ARTI_CACHE", "foo")]);
398

            
399
        let p = CfgPath::new_literal(PathBuf::from("${ARTI_CACHE}/literally"));
400
        // This doesn't get expanded, since we're using a literal path.
401
        assert_eq!(
402
            p.path(&r).unwrap().to_str().unwrap(),
403
            "${ARTI_CACHE}/literally"
404
        );
405
        assert_eq!(p.to_string(), "\"${ARTI_CACHE}/literally\" [exactly]");
406
    }
407

            
408
    #[test]
409
    #[cfg(feature = "expand-paths")]
410
    fn program_dir() {
411
        let current_exe = std::env::current_exe().unwrap();
412
        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", current_exe.parent().unwrap())]);
413

            
414
        let p = CfgPath::new("${PROGRAM_DIR}/foo".to_string());
415

            
416
        let mut this_binary = current_exe;
417
        this_binary.pop();
418
        this_binary.push("foo");
419
        let expanded = p.path(&r).unwrap();
420
        assert_eq!(expanded, this_binary);
421
    }
422

            
423
    #[test]
424
    #[cfg(not(feature = "expand-paths"))]
425
    fn rejections() {
426
        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", std::env::current_exe().unwrap())]);
427

            
428
        let chk_err = |s: &str, mke: &dyn Fn(String) -> CfgPathError| {
429
            let p = CfgPath::new(s.to_string());
430
            assert_eq!(p.path(&r).unwrap_err(), mke(s.to_string()));
431
        };
432

            
433
        let chk_ok = |s: &str, exp| {
434
            let p = CfgPath::new(s.to_string());
435
            assert_eq!(p.path(&r), Ok(PathBuf::from(exp)));
436
        };
437

            
438
        chk_err(
439
            "some/${PROGRAM_DIR}/foo",
440
            &CfgPathError::VariableInterpolationNotSupported,
441
        );
442
        chk_err("~some", &CfgPathError::HomeDirInterpolationNotSupported);
443

            
444
        chk_ok("some$$foo$$bar", "some$foo$bar");
445
        chk_ok("no dollars", "no dollars");
446
    }
447
}
448

            
449
#[cfg(test)]
450
mod test_serde {
451
    // @@ begin test lint list maintained by maint/add_warning @@
452
    #![allow(clippy::bool_assert_comparison)]
453
    #![allow(clippy::clone_on_copy)]
454
    #![allow(clippy::dbg_macro)]
455
    #![allow(clippy::mixed_attributes_style)]
456
    #![allow(clippy::print_stderr)]
457
    #![allow(clippy::print_stdout)]
458
    #![allow(clippy::single_char_pattern)]
459
    #![allow(clippy::unwrap_used)]
460
    #![allow(clippy::unchecked_duration_subtraction)]
461
    #![allow(clippy::useless_vec)]
462
    #![allow(clippy::needless_pass_by_value)]
463
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
464

            
465
    use super::*;
466

            
467
    use std::ffi::OsString;
468
    use std::fmt::Debug;
469

            
470
    use derive_builder::Builder;
471
    use tor_config::load::TopLevel;
472
    use tor_config::{impl_standard_builder, ConfigBuildError};
473

            
474
    #[derive(Serialize, Deserialize, Builder, Eq, PartialEq, Debug)]
475
    #[builder(derive(Serialize, Deserialize, Debug))]
476
    #[builder(build_fn(error = "ConfigBuildError"))]
477
    struct TestConfigFile {
478
        p: CfgPath,
479
    }
480

            
481
    impl_standard_builder! { TestConfigFile: !Default }
482

            
483
    impl TopLevel for TestConfigFile {
484
        type Builder = TestConfigFileBuilder;
485
    }
486

            
487
    fn deser_json(json: &str) -> CfgPath {
488
        dbg!(json);
489
        let TestConfigFile { p } = serde_json::from_str(json).expect("deser json failed");
490
        p
491
    }
492
    fn deser_toml(toml: &str) -> CfgPath {
493
        dbg!(toml);
494
        let TestConfigFile { p } = toml::from_str(toml).expect("deser toml failed");
495
        p
496
    }
497
    fn deser_toml_cfg(toml: &str) -> CfgPath {
498
        dbg!(toml);
499
        let mut sources = tor_config::ConfigurationSources::new_empty();
500
        sources.push_source(
501
            tor_config::ConfigurationSource::from_verbatim(toml.to_string()),
502
            tor_config::sources::MustRead::MustRead,
503
        );
504
        let cfg = sources.load().unwrap();
505

            
506
        dbg!(&cfg);
507
        let TestConfigFile { p } = tor_config::load::resolve(cfg).expect("cfg resolution failed");
508
        p
509
    }
510

            
511
    #[test]
512
    fn test_parse() {
513
        fn desers(toml: &str, json: &str) -> Vec<CfgPath> {
514
            vec![deser_toml(toml), deser_toml_cfg(toml), deser_json(json)]
515
        }
516

            
517
        for cp in desers(r#"p = "string""#, r#"{ "p": "string" }"#) {
518
            assert_eq!(cp.as_unexpanded_str(), Some("string"));
519
            assert_eq!(cp.as_literal_path(), None);
520
        }
521

            
522
        for cp in desers(
523
            r#"p = { literal = "lit" }"#,
524
            r#"{ "p": {"literal": "lit"} }"#,
525
        ) {
526
            assert_eq!(cp.as_unexpanded_str(), None);
527
            assert_eq!(cp.as_literal_path(), Some(&*PathBuf::from("lit")));
528
        }
529
    }
530

            
531
    fn non_string_path() -> PathBuf {
532
        #[cfg(target_family = "unix")]
533
        {
534
            use std::os::unix::ffi::OsStringExt;
535
            return PathBuf::from(OsString::from_vec(vec![0x80_u8]));
536
        }
537

            
538
        #[cfg(target_family = "windows")]
539
        {
540
            use std::os::windows::ffi::OsStringExt;
541
            return PathBuf::from(OsString::from_wide(&[0xD800_u16]));
542
        }
543

            
544
        #[allow(unreachable_code)]
545
        // Cannot test non-Stringy Paths on this platform
546
        PathBuf::default()
547
    }
548

            
549
    fn test_roundtrip_cases<SER, S, DESER, E, F>(ser: SER, deser: DESER)
550
    where
551
        SER: Fn(&TestConfigFile) -> Result<S, E>,
552
        DESER: Fn(&S) -> Result<TestConfigFile, F>,
553
        S: Debug,
554
        E: Debug,
555
        F: Debug,
556
    {
557
        let case = |easy, p| {
558
            let input = TestConfigFile { p };
559
            let s = match ser(&input) {
560
                Ok(s) => s,
561
                Err(e) if easy => panic!("ser failed {:?} e={:?}", &input, &e),
562
                Err(_) => return,
563
            };
564
            dbg!(&input, &s);
565
            let output = deser(&s).expect("deser failed");
566
            assert_eq!(&input, &output, "s={:?}", &s);
567
        };
568

            
569
        case(true, CfgPath::new("string".into()));
570
        case(true, CfgPath::new_literal(PathBuf::from("nice path")));
571
        case(true, CfgPath::new_literal(PathBuf::from("path with ✓")));
572

            
573
        // Non-UTF-8 paths are really hard to serialize.  We allow the serializsaton
574
        // to fail, and if it does, we skip the rest of the round trip test.
575
        // But, if they did serialise, we want to make sure that we can deserialize.
576
        // Hence this test case.
577
        case(false, CfgPath::new_literal(non_string_path()));
578
    }
579

            
580
    #[test]
581
    fn roundtrip_json() {
582
        test_roundtrip_cases(
583
            |input| serde_json::to_string(&input),
584
            |json| serde_json::from_str(json),
585
        );
586
    }
587

            
588
    #[test]
589
    fn roundtrip_toml() {
590
        test_roundtrip_cases(|input| toml::to_string(&input), |toml| toml::from_str(toml));
591
    }
592

            
593
    #[test]
594
    fn roundtrip_mpack() {
595
        test_roundtrip_cases(
596
            |input| rmp_serde::to_vec(&input),
597
            |mpack| rmp_serde::from_slice(mpack),
598
        );
599
    }
600
}