Skip to main content

tor_config_path/
lib.rs

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#![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_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//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50use std::collections::HashMap;
51use std::path::{Path, PathBuf};
52
53use serde::{Deserialize, Serialize};
54use std::borrow::Cow;
55#[cfg(feature = "expand-paths")]
56use {directories::BaseDirs, std::sync::LazyLock};
57
58use tor_error::{ErrorKind, HasKind};
59
60#[cfg(all(test, feature = "expand-paths"))]
61use std::ffi::OsStr;
62
63#[cfg(feature = "address")]
64pub mod addr;
65
66#[cfg(feature = "arti-client")]
67mod arti_client_paths;
68
69#[cfg(feature = "arti-client")]
70pub use arti_client_paths::arti_client_base_resolver;
71
72/// A path in a configuration file: tilde expansion is performed, along
73/// with expansion of variables provided by a [`CfgPathResolver`].
74///
75/// The tilde expansion is performed using the home directory given by the
76/// `directories` crate, which may be based on an environment variable. For more
77/// information, see [`BaseDirs::home_dir`](directories::BaseDirs::home_dir).
78///
79/// Alternatively, a `CfgPath` can contain literal `PathBuf`, which will not be expanded.
80#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
81#[serde(transparent)]
82pub struct CfgPath(PathInner);
83
84/// Inner implementation of CfgPath
85///
86/// `PathInner` exists to avoid making the variants part of the public Rust API
87#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
88#[serde(untagged)]
89enum PathInner {
90    /// A path that should be used literally, with no expansion.
91    Literal(LiteralPath),
92    /// A path that should be expanded from a string using ShellExpand.
93    Shell(String),
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
97/// Inner implementation of PathInner:Literal
98///
99/// `LiteralPath` exists to arrange that `PathInner::Literal`'s (de)serialization
100/// does not overlap with `PathInner::Shell`'s.
101struct LiteralPath {
102    /// The underlying `PathBuf`.
103    literal: PathBuf,
104}
105
106/// An error that has occurred while expanding a path.
107#[derive(thiserror::Error, Debug, Clone)]
108#[non_exhaustive]
109#[cfg_attr(test, derive(PartialEq))]
110pub enum CfgPathError {
111    /// The path contained a variable we didn't recognize.
112    #[error("Unrecognized variable {0} in path")]
113    UnknownVar(String),
114    /// We couldn't construct a ProjectDirs object.
115    #[error(
116        "Couldn't determine XDG Project Directories, needed to resolve a path; probably, unable to determine HOME directory"
117    )]
118    NoProjectDirs,
119    /// We couldn't construct a BaseDirs object.
120    #[error("Can't construct base directories to resolve a path element")]
121    NoBaseDirs,
122    /// We couldn't find our current binary path.
123    #[error("Can't find the path to the current binary")]
124    NoProgramPath,
125    /// We couldn't find the directory path containing the current binary.
126    #[error("Can't find the directory of the current binary")]
127    NoProgramDir,
128    /// We couldn't convert a string to a valid path on the OS.
129    //
130    // NOTE: This is not currently generated. Shall we remove it?
131    #[error("Invalid path string: {0:?}")]
132    InvalidString(String),
133    /// Variable interpolation (`$`) attempted, but not compiled in
134    #[error(
135        "Variable interpolation $ is not supported (tor-config/expand-paths feature disabled)); $ must still be doubled"
136    )]
137    VariableInterpolationNotSupported(String),
138    /// Home dir interpolation (`~`) attempted, but not compiled in
139    #[error("Home dir ~/ is not supported (tor-config/expand-paths feature disabled)")]
140    HomeDirInterpolationNotSupported(String),
141}
142
143impl HasKind for CfgPathError {
144    fn kind(&self) -> ErrorKind {
145        use CfgPathError as E;
146        use ErrorKind as EK;
147        match self {
148            E::UnknownVar(_) | E::InvalidString(_) => EK::InvalidConfig,
149            E::NoProjectDirs | E::NoBaseDirs => EK::NoHomeDirectory,
150            E::NoProgramPath | E::NoProgramDir => EK::InvalidConfig,
151            E::VariableInterpolationNotSupported(_) | E::HomeDirInterpolationNotSupported(_) => {
152                EK::FeatureDisabled
153            }
154        }
155    }
156}
157
158/// A variable resolver for paths in a configuration file.
159///
160/// Typically there should be one resolver per application, and the application should share the
161/// resolver throughout the application to have consistent path variable expansions. Typically the
162/// application would create its own resolver with its application-specific variables, but note that
163/// `TorClientConfig` is an exception which does not accept a resolver from the application and
164/// instead generates its own. This is done for backwards compatibility reasons.
165///
166/// Once constructed, they are used during calls to [`CfgPath::path`] to expand variables in the
167/// path.
168#[derive(Clone, Debug, Default)]
169pub struct CfgPathResolver {
170    /// The variables and their values. The values can be an `Err` if the variable is expected but
171    /// can't be expanded.
172    vars: HashMap<String, Result<Cow<'static, Path>, CfgPathError>>,
173}
174
175impl CfgPathResolver {
176    /// Get the value for a given variable name.
177    #[cfg(feature = "expand-paths")]
178    fn get_var(&self, var: &str) -> Result<Cow<'static, Path>, CfgPathError> {
179        match self.vars.get(var) {
180            Some(val) => val.clone(),
181            None => Err(CfgPathError::UnknownVar(var.to_owned())),
182        }
183    }
184
185    /// Set a variable `var` that will be replaced with `val` when a [`CfgPath`] is expanded.
186    ///
187    /// Setting an `Err` is useful when a variable is supported, but for whatever reason it can't be
188    /// expanded, and you'd like to return a more-specific error. An example might be a `USER_HOME`
189    /// variable for a user that doesn't have a `HOME` environment variable set.
190    ///
191    /// ```
192    /// use std::path::Path;
193    /// use tor_config_path::{CfgPath, CfgPathResolver};
194    ///
195    /// let mut path_resolver = CfgPathResolver::default();
196    /// path_resolver.set_var("FOO", Ok(Path::new("/foo").to_owned().into()));
197    ///
198    /// let path = CfgPath::new("${FOO}/bar".into());
199    ///
200    /// #[cfg(feature = "expand-paths")]
201    /// assert_eq!(path.path(&path_resolver).unwrap(), Path::new("/foo/bar"));
202    /// #[cfg(not(feature = "expand-paths"))]
203    /// assert!(path.path(&path_resolver).is_err());
204    /// ```
205    pub fn set_var(
206        &mut self,
207        var: impl Into<String>,
208        val: Result<Cow<'static, Path>, CfgPathError>,
209    ) {
210        self.vars.insert(var.into(), val);
211    }
212
213    /// Helper to create a `CfgPathResolver` from str `(name, value)` pairs.
214    #[cfg(all(test, feature = "expand-paths"))]
215    fn from_pairs<K, V>(vars: impl IntoIterator<Item = (K, V)>) -> CfgPathResolver
216    where
217        K: Into<String>,
218        V: AsRef<OsStr>,
219    {
220        let mut path_resolver = CfgPathResolver::default();
221        for (name, val) in vars.into_iter() {
222            let val = Path::new(val.as_ref()).to_owned();
223            path_resolver.set_var(name, Ok(val.into()));
224        }
225        path_resolver
226    }
227}
228
229impl CfgPath {
230    /// Create a new configuration path
231    pub fn new(s: String) -> Self {
232        CfgPath(PathInner::Shell(s))
233    }
234
235    /// Construct a new `CfgPath` designating a literal not-to-be-expanded `PathBuf`
236    pub fn new_literal<P: Into<PathBuf>>(path: P) -> Self {
237        CfgPath(PathInner::Literal(LiteralPath {
238            literal: path.into(),
239        }))
240    }
241
242    /// Return the path on disk designated by this `CfgPath`.
243    ///
244    /// Variables may or may not be resolved using `path_resolver`, depending on whether the
245    /// `expand-paths` feature is enabled or not.
246    pub fn path(&self, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
247        match &self.0 {
248            PathInner::Shell(s) => expand(s, path_resolver),
249            PathInner::Literal(LiteralPath { literal }) => Ok(literal.clone()),
250        }
251    }
252
253    /// If the `CfgPath` is a string that should be expanded, return the (unexpanded) string,
254    ///
255    /// Before use, this string would have be to expanded.  So if you want a path to actually use,
256    /// call `path` instead.
257    ///
258    /// Returns `None` if the `CfgPath` is a literal `PathBuf` not intended for expansion.
259    pub fn as_unexpanded_str(&self) -> Option<&str> {
260        match &self.0 {
261            PathInner::Shell(s) => Some(s),
262            PathInner::Literal(_) => None,
263        }
264    }
265
266    /// If the `CfgPath` designates a literal not-to-be-expanded `Path`, return a reference to it
267    ///
268    /// Returns `None` if the `CfgPath` is a string which should be expanded, which is the
269    /// usual case.
270    pub fn as_literal_path(&self) -> Option<&Path> {
271        match &self.0 {
272            PathInner::Shell(_) => None,
273            PathInner::Literal(LiteralPath { literal }) => Some(literal),
274        }
275    }
276}
277
278impl std::fmt::Display for CfgPath {
279    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match &self.0 {
281            PathInner::Literal(LiteralPath { literal }) => write!(fmt, "{:?} [exactly]", literal),
282            PathInner::Shell(s) => s.fmt(fmt),
283        }
284    }
285}
286
287/// Return the user's home directory used when expanding paths.
288// This is public so that applications which want to support for example a `USER_HOME` variable can
289// use the same home directory expansion that we use in this crate for `~` expansion.
290#[cfg(feature = "expand-paths")]
291pub fn home() -> Result<&'static Path, CfgPathError> {
292    /// Lazy lock holding the home directory.
293    static HOME_DIR: LazyLock<Option<PathBuf>> =
294        LazyLock::new(|| Some(BaseDirs::new()?.home_dir().to_owned()));
295    HOME_DIR
296        .as_ref()
297        .map(PathBuf::as_path)
298        .ok_or(CfgPathError::NoBaseDirs)
299}
300
301/// Helper: expand a directory given as a string.
302#[cfg(feature = "expand-paths")]
303fn expand(s: &str, path_resolver: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
304    let path = shellexpand::path::full_with_context(
305        s,
306        || home().ok(),
307        |x| path_resolver.get_var(x).map(Some),
308    );
309    Ok(path.map_err(|e| e.cause)?.into_owned())
310}
311
312/// Helper: convert a string to a path without expansion.
313#[cfg(not(feature = "expand-paths"))]
314fn expand(input: &str, _: &CfgPathResolver) -> Result<PathBuf, CfgPathError> {
315    // We must still de-duplicate `$` and reject `~/`,, so that the behaviour is a superset
316    if input.starts_with('~') {
317        return Err(CfgPathError::HomeDirInterpolationNotSupported(input.into()));
318    }
319
320    let mut out = String::with_capacity(input.len());
321    let mut s = input;
322    while let Some((lhs, rhs)) = s.split_once('$') {
323        if let Some(rhs) = rhs.strip_prefix('$') {
324            // deduplicate the $
325            out += lhs;
326            out += "$";
327            s = rhs;
328        } else {
329            return Err(CfgPathError::VariableInterpolationNotSupported(
330                input.into(),
331            ));
332        }
333    }
334    out += s;
335    Ok(out.into())
336}
337
338#[cfg(all(test, feature = "expand-paths"))]
339mod test {
340    #![allow(clippy::unwrap_used)]
341    use super::*;
342
343    #[test]
344    fn expand_no_op() {
345        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
346
347        let p = CfgPath::new("Hello/world".to_string());
348        assert_eq!(p.to_string(), "Hello/world".to_string());
349        assert_eq!(p.path(&r).unwrap().to_str(), Some("Hello/world"));
350
351        let p = CfgPath::new("/usr/local/foo".to_string());
352        assert_eq!(p.to_string(), "/usr/local/foo".to_string());
353        assert_eq!(p.path(&r).unwrap().to_str(), Some("/usr/local/foo"));
354    }
355
356    #[cfg(not(target_family = "windows"))]
357    #[test]
358    fn expand_home() {
359        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
360
361        let p = CfgPath::new("~/.arti/config".to_string());
362        assert_eq!(p.to_string(), "~/.arti/config".to_string());
363
364        let expected = dirs::home_dir().unwrap().join(".arti/config");
365        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
366
367        let p = CfgPath::new("${USER_HOME}/.arti/config".to_string());
368        assert_eq!(p.to_string(), "${USER_HOME}/.arti/config".to_string());
369        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
370    }
371
372    #[cfg(target_family = "windows")]
373    #[test]
374    fn expand_home() {
375        let r = CfgPathResolver::from_pairs([("USER_HOME", home().unwrap())]);
376
377        let p = CfgPath::new("~\\.arti\\config".to_string());
378        assert_eq!(p.to_string(), "~\\.arti\\config".to_string());
379
380        let expected = dirs::home_dir().unwrap().join(".arti\\config");
381        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
382
383        let p = CfgPath::new("${USER_HOME}\\.arti\\config".to_string());
384        assert_eq!(p.to_string(), "${USER_HOME}\\.arti\\config".to_string());
385        assert_eq!(p.path(&r).unwrap().to_str(), expected.to_str());
386    }
387
388    #[test]
389    fn expand_bogus() {
390        let r = CfgPathResolver::from_pairs([("FOO", "foo")]);
391
392        let p = CfgPath::new("${ARTI_WOMBAT}/example".to_string());
393        assert_eq!(p.to_string(), "${ARTI_WOMBAT}/example".to_string());
394
395        assert!(matches!(p.path(&r), Err(CfgPathError::UnknownVar(_))));
396        assert_eq!(
397            &p.path(&r).unwrap_err().to_string(),
398            "Unrecognized variable ARTI_WOMBAT in path"
399        );
400    }
401
402    #[test]
403    fn literal() {
404        let r = CfgPathResolver::from_pairs([("ARTI_CACHE", "foo")]);
405
406        let p = CfgPath::new_literal(PathBuf::from("${ARTI_CACHE}/literally"));
407        // This doesn't get expanded, since we're using a literal path.
408        assert_eq!(
409            p.path(&r).unwrap().to_str().unwrap(),
410            "${ARTI_CACHE}/literally"
411        );
412        assert_eq!(p.to_string(), "\"${ARTI_CACHE}/literally\" [exactly]");
413    }
414
415    #[test]
416    #[cfg(feature = "expand-paths")]
417    fn program_dir() {
418        let current_exe = std::env::current_exe().unwrap();
419        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", current_exe.parent().unwrap())]);
420
421        let p = CfgPath::new("${PROGRAM_DIR}/foo".to_string());
422
423        let mut this_binary = current_exe;
424        this_binary.pop();
425        this_binary.push("foo");
426        let expanded = p.path(&r).unwrap();
427        assert_eq!(expanded, this_binary);
428    }
429
430    #[test]
431    #[cfg(not(feature = "expand-paths"))]
432    fn rejections() {
433        let r = CfgPathResolver::from_pairs([("PROGRAM_DIR", std::env::current_exe().unwrap())]);
434
435        let chk_err = |s: &str, mke: &dyn Fn(String) -> CfgPathError| {
436            let p = CfgPath::new(s.to_string());
437            assert_eq!(p.path(&r).unwrap_err(), mke(s.to_string()));
438        };
439
440        let chk_ok = |s: &str, exp| {
441            let p = CfgPath::new(s.to_string());
442            assert_eq!(p.path(&r), Ok(PathBuf::from(exp)));
443        };
444
445        chk_err(
446            "some/${PROGRAM_DIR}/foo",
447            &CfgPathError::VariableInterpolationNotSupported,
448        );
449        chk_err("~some", &CfgPathError::HomeDirInterpolationNotSupported);
450
451        chk_ok("some$$foo$$bar", "some$foo$bar");
452        chk_ok("no dollars", "no dollars");
453    }
454}
455
456#[cfg(test)]
457mod test_serde {
458    // @@ begin test lint list maintained by maint/add_warning @@
459    #![allow(clippy::bool_assert_comparison)]
460    #![allow(clippy::clone_on_copy)]
461    #![allow(clippy::dbg_macro)]
462    #![allow(clippy::mixed_attributes_style)]
463    #![allow(clippy::print_stderr)]
464    #![allow(clippy::print_stdout)]
465    #![allow(clippy::single_char_pattern)]
466    #![allow(clippy::unwrap_used)]
467    #![allow(clippy::unchecked_time_subtraction)]
468    #![allow(clippy::useless_vec)]
469    #![allow(clippy::needless_pass_by_value)]
470    #![allow(clippy::string_slice)] // See arti#2571
471    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
472
473    use super::*;
474
475    use std::ffi::OsString;
476    use std::fmt::Debug;
477
478    use derive_deftly::Deftly;
479    use tor_config::load::TopLevel;
480
481    #[derive(Serialize, Deserialize, Deftly, Eq, PartialEq, Debug)]
482    #[derive_deftly(tor_config::derive::TorConfig)]
483    #[deftly(tor_config(no_default_trait))]
484    struct TestConfigFile {
485        #[deftly(tor_config(no_default))]
486        p: CfgPath,
487    }
488
489    impl TopLevel for TestConfigFile {
490        type Builder = TestConfigFileBuilder;
491    }
492
493    fn deser_json(json: &str) -> CfgPath {
494        dbg!(json);
495        let TestConfigFile { p } = serde_json::from_str(json).expect("deser json failed");
496        p
497    }
498    fn deser_toml(toml: &str) -> CfgPath {
499        dbg!(toml);
500        let TestConfigFile { p } = toml::from_str(toml).expect("deser toml failed");
501        p
502    }
503    fn deser_toml_cfg(toml: &str) -> CfgPath {
504        dbg!(toml);
505        let mut sources = tor_config::ConfigurationSources::new_empty();
506        sources.push_source(
507            tor_config::ConfigurationSource::from_verbatim(toml.to_string()),
508            tor_config::sources::MustRead::MustRead,
509        );
510        let cfg = sources.load().unwrap();
511
512        dbg!(&cfg);
513        let TestConfigFile { p } = tor_config::load::resolve(cfg).expect("cfg resolution failed");
514        p
515    }
516
517    #[test]
518    fn test_parse() {
519        fn desers(toml: &str, json: &str) -> Vec<CfgPath> {
520            vec![deser_toml(toml), deser_toml_cfg(toml), deser_json(json)]
521        }
522
523        for cp in desers(r#"p = "string""#, r#"{ "p": "string" }"#) {
524            assert_eq!(cp.as_unexpanded_str(), Some("string"));
525            assert_eq!(cp.as_literal_path(), None);
526        }
527
528        for cp in desers(
529            r#"p = { literal = "lit" }"#,
530            r#"{ "p": {"literal": "lit"} }"#,
531        ) {
532            assert_eq!(cp.as_unexpanded_str(), None);
533            assert_eq!(cp.as_literal_path(), Some(&*PathBuf::from("lit")));
534        }
535    }
536
537    fn non_string_path() -> PathBuf {
538        #[cfg(target_family = "unix")]
539        {
540            use std::os::unix::ffi::OsStringExt;
541            return PathBuf::from(OsString::from_vec(vec![0x80_u8]));
542        }
543
544        #[cfg(target_family = "windows")]
545        {
546            use std::os::windows::ffi::OsStringExt;
547            return PathBuf::from(OsString::from_wide(&[0xD800_u16]));
548        }
549
550        #[allow(unreachable_code)]
551        // Cannot test non-Stringy Paths on this platform
552        PathBuf::default()
553    }
554
555    fn test_roundtrip_cases<SER, S, DESER, E, F>(ser: SER, deser: DESER)
556    where
557        SER: Fn(&TestConfigFile) -> Result<S, E>,
558        DESER: Fn(&S) -> Result<TestConfigFile, F>,
559        S: Debug,
560        E: Debug,
561        F: Debug,
562    {
563        let case = |easy, p| {
564            let input = TestConfigFile { p };
565            let s = match ser(&input) {
566                Ok(s) => s,
567                Err(e) if easy => panic!("ser failed {:?} e={:?}", &input, &e),
568                Err(_) => return,
569            };
570            dbg!(&input, &s);
571            let output = deser(&s).expect("deser failed");
572            assert_eq!(&input, &output, "s={:?}", &s);
573        };
574
575        case(true, CfgPath::new("string".into()));
576        case(true, CfgPath::new_literal(PathBuf::from("nice path")));
577        case(true, CfgPath::new_literal(PathBuf::from("path with ✓")));
578
579        // Non-UTF-8 paths are really hard to serialize.  We allow the serializsaton
580        // to fail, and if it does, we skip the rest of the round trip test.
581        // But, if they did serialise, we want to make sure that we can deserialize.
582        // Hence this test case.
583        case(false, CfgPath::new_literal(non_string_path()));
584    }
585
586    #[test]
587    fn roundtrip_json() {
588        test_roundtrip_cases(
589            |input| serde_json::to_string(&input),
590            |json| serde_json::from_str(json),
591        );
592    }
593
594    #[test]
595    fn roundtrip_toml() {
596        test_roundtrip_cases(|input| toml::to_string(&input), |toml| toml::from_str(toml));
597    }
598
599    #[test]
600    fn roundtrip_mpack() {
601        test_roundtrip_cases(
602            |input| rmp_serde::to_vec(&input),
603            |mpack| rmp_serde::from_slice(mpack),
604        );
605    }
606}