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
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
475
    use super::*;
476

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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