1
#![cfg_attr(docsrs, feature(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
#![allow(clippy::cognitive_complexity)] // See arti#2556
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_time_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
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

            
51
use std::collections::HashMap;
52
use std::path::{Path, PathBuf};
53

            
54
use serde::{Deserialize, Serialize};
55
use std::borrow::Cow;
56
#[cfg(feature = "expand-paths")]
57
use {directories::BaseDirs, std::sync::LazyLock};
58

            
59
use tor_error::{ErrorKind, HasKind};
60

            
61
#[cfg(all(test, feature = "expand-paths"))]
62
use std::ffi::OsStr;
63

            
64
#[cfg(feature = "address")]
65
pub mod addr;
66

            
67
#[cfg(feature = "arti-client")]
68
mod arti_client_paths;
69

            
70
#[cfg(feature = "arti-client")]
71
pub use arti_client_paths::arti_client_base_resolver;
72

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

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

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

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

            
144
impl HasKind for CfgPathError {
145
    fn kind(&self) -> ErrorKind {
146
        use CfgPathError as E;
147
        use ErrorKind as EK;
148
        match self {
149
            E::UnknownVar(_) | E::InvalidString(_) => EK::InvalidConfig,
150
            E::NoProjectDirs | E::NoBaseDirs => EK::NoHomeDirectory,
151
            E::NoProgramPath | E::NoProgramDir => EK::InvalidConfig,
152
            E::VariableInterpolationNotSupported(_) | E::HomeDirInterpolationNotSupported(_) => {
153
                EK::FeatureDisabled
154
            }
155
        }
156
    }
157
}
158

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

            
176
impl CfgPathResolver {
177
    /// Get the value for a given variable name.
178
    #[cfg(feature = "expand-paths")]
179
6866
    fn get_var(&self, var: &str) -> Result<Cow<'static, Path>, CfgPathError> {
180
6866
        match self.vars.get(var) {
181
6803
            Some(val) => val.clone(),
182
63
            None => Err(CfgPathError::UnknownVar(var.to_owned())),
183
        }
184
6866
    }
185

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

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

            
230
impl CfgPath {
231
    /// Create a new configuration path
232
25582
    pub fn new(s: String) -> Self {
233
25582
        CfgPath(PathInner::Shell(s))
234
25582
    }
235

            
236
    /// Construct a new `CfgPath` designating a literal not-to-be-expanded `PathBuf`
237
1569
    pub fn new_literal<P: Into<PathBuf>>(path: P) -> Self {
238
1569
        CfgPath(PathInner::Literal(LiteralPath {
239
1569
            literal: path.into(),
240
1569
        }))
241
1569
    }
242

            
243
    /// Return the path on disk designated by this `CfgPath`.
244
    ///
245
    /// Variables may or may not be resolved using `path_resolver`, depending on whether the
246
    /// `expand-paths` feature is enabled or not.
247
12755
    pub fn path(&self, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
248
12755
        match &self.0 {
249
9669
            PathInner::Shell(s) => expand(s, path_resolver),
250
3086
            PathInner::Literal(LiteralPath { literal }) => Ok(literal.clone()),
251
        }
252
12755
    }
253

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

            
267
    /// If the `CfgPath` designates a literal not-to-be-expanded `Path`, return a reference to it
268
    ///
269
    /// Returns `None` if the `CfgPath` is a string which should be expanded, which is the
270
    /// usual case.
271
12
    pub fn as_literal_path(&self) -> Option<&Path> {
272
12
        match &self.0 {
273
6
            PathInner::Shell(_) => None,
274
6
            PathInner::Literal(LiteralPath { literal }) => Some(literal),
275
        }
276
12
    }
277
}
278

            
279
impl std::fmt::Display for CfgPath {
280
309
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281
309
        match &self.0 {
282
2
            PathInner::Literal(LiteralPath { literal }) => write!(fmt, "{:?} [exactly]", literal),
283
307
            PathInner::Shell(s) => s.fmt(fmt),
284
        }
285
309
    }
286
}
287

            
288
/// Return the user's home directory used when expanding paths.
289
// This is public so that applications which want to support for example a `USER_HOME` variable can
290
// use the same home directory expansion that we use in this crate for `~` expansion.
291
#[cfg(feature = "expand-paths")]
292
11410
pub fn home() -> Result<&'static Path, CfgPathError> {
293
    /// Lazy lock holding the home directory.
294
    static HOME_DIR: LazyLock<Option<PathBuf>> =
295
3365
        LazyLock::new(|| Some(BaseDirs::new()?.home_dir().to_owned()));
296
11410
    HOME_DIR
297
11410
        .as_ref()
298
11410
        .map(PathBuf::as_path)
299
11410
        .ok_or(CfgPathError::NoBaseDirs)
300
11410
}
301

            
302
/// Helper: expand a directory given as a string.
303
#[cfg(feature = "expand-paths")]
304
9669
fn expand(s: &str, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
305
9669
    let path = shellexpand::path::full_with_context(
306
9669
        s,
307
2
        || home().ok(),
308
6866
        |x| path_resolver.get_var(x).map(Some),
309
    );
310
9669
    Ok(path.map_err(|e| e.cause)?.into_owned())
311
9669
}
312

            
313
/// Helper: convert a string to a path without expansion.
314
#[cfg(not(feature = "expand-paths"))]
315
fn expand(input: &str, _: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
316
    // We must still de-duplicate `$` and reject `~/`,, so that the behaviour is a superset
317
    if input.starts_with('~') {
318
        return Err(CfgPathError::HomeDirInterpolationNotSupported(input.into()));
319
    }
320

            
321
    let mut out = String::with_capacity(input.len());
322
    let mut s = input;
323
    while let Some((lhs, rhs)) = s.split_once('$') {
324
        if let Some(rhs) = rhs.strip_prefix('$') {
325
            // deduplicate the $
326
            out += lhs;
327
            out += "$";
328
            s = rhs;
329
        } else {
330
            return Err(CfgPathError::VariableInterpolationNotSupported(
331
                input.into(),
332
            ));
333
        }
334
    }
335
    out += s;
336
    Ok(out.into())
337
}
338

            
339
#[cfg(all(test, feature = "expand-paths"))]
340
mod test {
341
    #![allow(clippy::unwrap_used)]
342
    use super::*;
343

            
344
    #[test]
345
    fn expand_no_op() {
346
        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
347

            
348
        let p = CfgPath::new("Hello/world".to_string());
349
        assert_eq!(p.to_string(), "Hello/world".to_string());
350
        assert_eq!(p.path(&r).unwrap().to_str(), Some("Hello/world"));
351

            
352
        let p = CfgPath::new("/usr/local/foo".to_string());
353
        assert_eq!(p.to_string(), "/usr/local/foo".to_string());
354
        assert_eq!(p.path(&r).unwrap().to_str(), Some("/usr/local/foo"));
355
    }
356

            
357
    #[cfg(not(target_family = "windows"))]
358
    #[test]
359
    fn expand_home() {
360
        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
361

            
362
        let p = CfgPath::new("~/.arti/config".to_string());
363
        assert_eq!(p.to_string(), "~/.arti/config".to_string());
364

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

            
368
        let p = CfgPath::new("${USER_HOME}/.arti/config".to_string());
369
        assert_eq!(p.to_string(), "${USER_HOME}/.arti/config".to_string());
370
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
371
    }
372

            
373
    #[cfg(target_family = "windows")]
374
    #[test]
375
    fn expand_home() {
376
        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
377

            
378
        let p = CfgPath::new("~\\.arti\\config".to_string());
379
        assert_eq!(p.to_string(), "~\\.arti\\config".to_string());
380

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

            
384
        let p = CfgPath::new("${USER_HOME}\\.arti\\config".to_string());
385
        assert_eq!(p.to_string(), "${USER_HOME}\\.arti\\config".to_string());
386
        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
387
    }
388

            
389
    #[test]
390
    fn expand_bogus() {
391
        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
392

            
393
        let p = CfgPath::new("${ARTI_WOMBAT}/example".to_string());
394
        assert_eq!(p.to_string(), "${ARTI_WOMBAT}/example".to_string());
395

            
396
        assert!(matches!(p.path(&r), Err(CfgPathError::UnknownVar(_))));
397
        assert_eq!(
398
            &p.path(&r).unwrap_err().to_string(),
399
            "Unrecognized variable ARTI_WOMBAT in path"
400
        );
401
    }
402

            
403
    #[test]
404
    fn literal() {
405
        let r = CfgPathResolver::from_pairs([("ARTI_CACHE", "foo")]);
406

            
407
        let p = CfgPath::new_literal(PathBuf::from("${ARTI_CACHE}/literally"));
408
        // This doesn't get expanded, since we're using a literal path.
409
        assert_eq!(
410
            p.path(&r).unwrap().to_str().unwrap(),
411
            "${ARTI_CACHE}/literally"
412
        );
413
        assert_eq!(p.to_string(), "\"${ARTI_CACHE}/literally\" [exactly]");
414
    }
415

            
416
    #[test]
417
    #[cfg(feature = "expand-paths")]
418
    fn program_dir() {
419
        let current_exe = std::env::current_exe().unwrap();
420
        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", current_exe.parent().unwrap())]);
421

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

            
424
        let mut this_binary = current_exe;
425
        this_binary.pop();
426
        this_binary.push("foo");
427
        let expanded = p.path(&r).unwrap();
428
        assert_eq!(expanded, this_binary);
429
    }
430

            
431
    #[test]
432
    #[cfg(not(feature = "expand-paths"))]
433
    fn rejections() {
434
        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", std::env::current_exe().unwrap())]);
435

            
436
        let chk_err = |s: &str, mke: &dyn Fn(String) -> CfgPathError| {
437
            let p = CfgPath::new(s.to_string());
438
            assert_eq!(p.path(&r).unwrap_err(), mke(s.to_string()));
439
        };
440

            
441
        let chk_ok = |s: &str, exp| {
442
            let p = CfgPath::new(s.to_string());
443
            assert_eq!(p.path(&r), Ok(PathBuf::from(exp)));
444
        };
445

            
446
        chk_err(
447
            "some/${PROGRAM_DIR}/foo",
448
            &CfgPathError::VariableInterpolationNotSupported,
449
        );
450
        chk_err("~some", &CfgPathError::HomeDirInterpolationNotSupported);
451

            
452
        chk_ok("some$$foo$$bar", "some$foo$bar");
453
        chk_ok("no dollars", "no dollars");
454
    }
455
}
456

            
457
#[cfg(test)]
458
mod test_serde {
459
    // @@ begin test lint list maintained by maint/add_warning @@
460
    #![allow(clippy::bool_assert_comparison)]
461
    #![allow(clippy::clone_on_copy)]
462
    #![allow(clippy::dbg_macro)]
463
    #![allow(clippy::mixed_attributes_style)]
464
    #![allow(clippy::print_stderr)]
465
    #![allow(clippy::print_stdout)]
466
    #![allow(clippy::single_char_pattern)]
467
    #![allow(clippy::unwrap_used)]
468
    #![allow(clippy::unchecked_time_subtraction)]
469
    #![allow(clippy::useless_vec)]
470
    #![allow(clippy::needless_pass_by_value)]
471
    #![allow(clippy::string_slice)] // See arti#2571
472
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
473

            
474
    use super::*;
475

            
476
    use std::ffi::OsString;
477
    use std::fmt::Debug;
478

            
479
    use derive_deftly::Deftly;
480
    use tor_config::load::TopLevel;
481

            
482
    #[derive(Serialize, Deserialize, Deftly, Eq, PartialEq, Debug)]
483
    #[derive_deftly(tor_config::derive::TorConfig)]
484
    #[deftly(tor_config(no_default_trait))]
485
    struct TestConfigFile {
486
        #[deftly(tor_config(no_default))]
487
        p: CfgPath,
488
    }
489

            
490
    impl TopLevel for TestConfigFile {
491
        type Builder = TestConfigFileBuilder;
492
    }
493

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

            
513
        dbg!(&cfg);
514
        let TestConfigFile { p } = tor_config::load::resolve(cfg).expect("cfg resolution failed");
515
        p
516
    }
517

            
518
    #[test]
519
    fn test_parse() {
520
        fn desers(toml: &str, json: &str) -> Vec<CfgPath> {
521
            vec![deser_toml(toml), deser_toml_cfg(toml), deser_json(json)]
522
        }
523

            
524
        for cp in desers(r#"p = "string""#, r#"{ "p": "string" }"#) {
525
            assert_eq!(cp.as_unexpanded_str(), Some("string"));
526
            assert_eq!(cp.as_literal_path(), None);
527
        }
528

            
529
        for cp in desers(
530
            r#"p = { literal = "lit" }"#,
531
            r#"{ "p": {"literal": "lit"} }"#,
532
        ) {
533
            assert_eq!(cp.as_unexpanded_str(), None);
534
            assert_eq!(cp.as_literal_path(), Some(&*PathBuf::from("lit")));
535
        }
536
    }
537

            
538
    fn non_string_path() -> PathBuf {
539
        #[cfg(target_family = "unix")]
540
        {
541
            use std::os::unix::ffi::OsStringExt;
542
            return PathBuf::from(OsString::from_vec(vec![0x80_u8]));
543
        }
544

            
545
        #[cfg(target_family = "windows")]
546
        {
547
            use std::os::windows::ffi::OsStringExt;
548
            return PathBuf::from(OsString::from_wide(&[0xD800_u16]));
549
        }
550

            
551
        #[allow(unreachable_code)]
552
        // Cannot test non-Stringy Paths on this platform
553
        PathBuf::default()
554
    }
555

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

            
576
        case(true, CfgPath::new("string".into()));
577
        case(true, CfgPath::new_literal(PathBuf::from("nice path")));
578
        case(true, CfgPath::new_literal(PathBuf::from("path with ✓")));
579

            
580
        // Non-UTF-8 paths are really hard to serialize.  We allow the serializsaton
581
        // to fail, and if it does, we skip the rest of the round trip test.
582
        // But, if they did serialise, we want to make sure that we can deserialize.
583
        // Hence this test case.
584
        case(false, CfgPath::new_literal(non_string_path()));
585
    }
586

            
587
    #[test]
588
    fn roundtrip_json() {
589
        test_roundtrip_cases(
590
            |input| serde_json::to_string(&input),
591
            |json| serde_json::from_str(json),
592
        );
593
    }
594

            
595
    #[test]
596
    fn roundtrip_toml() {
597
        test_roundtrip_cases(|input| toml::to_string(&input), |toml| toml::from_str(toml));
598
    }
599

            
600
    #[test]
601
    fn roundtrip_mpack() {
602
        test_roundtrip_cases(
603
            |input| rmp_serde::to_vec(&input),
604
            |mpack| rmp_serde::from_slice(mpack),
605
        );
606
    }
607
}