fs_mistrust/user/
serde_support.rs

1//! Serde support for [`TrustedUser`] and [`TrustedGroup`].
2
3use super::{TrustedGroup, TrustedUser};
4use serde::{Deserialize, Serialize};
5use std::{convert::TryFrom, ffi::OsString};
6
7/// Helper type: when encoding or decoding a group or user, we do so as one of
8/// these.
9///
10/// It's an `untagged` enumeration, so every case must be uniquely identifiable
11/// by type or by keywords.
12#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(untagged)]
14pub(super) enum Serde {
15    /// A boolean value.
16    ///
17    /// "false" means "no user", and is the same as "none".
18    ///
19    /// "true" is not allowed.
20    Bool(bool),
21    /// A string given in quotes.
22    ///
23    /// If this starts with ":" it will be interpreted as a special entity (e.g.
24    /// ":current" or ":username"). Otherwise, it will be interpreted as a name.
25    ///  
26    Str(String),
27    /// An integer provided without any identification.
28    ///
29    /// This will be interpreted as a UID or GID.
30    Num(u32),
31    /// A name, explicitly qualified as such.
32    Name {
33        /// The name in question.
34        ///
35        /// Even if this begins with ":", it is still interpreted as a name.
36        name: String,
37    },
38    /// A username that cannot be represented as a String.
39    Raw {
40        /// The username in question.
41        raw_name: OsString,
42    },
43    /// A special entity.
44    Special {
45        /// The name of the special entity. Starts with ":".
46        special: String,
47    },
48    /// A UID or GID, explicitly qualified as such.
49    Id {
50        /// The UID or GID.
51        id: u32,
52    },
53}
54
55impl Serde {
56    /// Convert this [`Serde`] into a less ambiguous form.
57    ///
58    /// Removes all Num and Str cases from the output, replacing them with
59    /// Special/Name/Id as appropriate.
60    fn disambiguate(self) -> Self {
61        match self {
62            Serde::Str(s) if s.starts_with(':') => Self::Special { special: s },
63            Serde::Str(s) => Self::Name { name: s },
64            Serde::Num(id) => Self::Id { id },
65            other => other,
66        }
67    }
68}
69
70/// Helper: declare
71macro_rules! implement_serde {
72   { $struct:ident { $( $case:ident => $str:expr, )* [ $errcase:ident ] } } => {
73
74    impl $struct {
75        /// Try to decode a "special-user" string from `s`, for serde.
76        fn from_special_str(s: &str) -> Result<Self, crate::Error> {
77            match s {
78                $( $str => Ok($struct::$case), )*
79                _ => Err(crate::Error::$errcase(s.to_owned())),
80            }
81        }
82        fn from_boolean(b: bool) -> Result<Self, crate::Error> {
83            if b {
84                Err(crate::Error::$errcase("'true'".into()))
85            } else {
86                Self::from_special_str(":none")
87            }
88        }
89    }
90
91    impl From<$struct> for Serde {
92        fn from(value: $struct) -> Self {
93            match value {
94                $struct::Id(id) => Self::Num(id),
95                $struct::Name(name) => {
96                    if let Some(name) = name.to_str() {
97                        let name = name.to_string();
98                        if name.starts_with(':') {
99                            Self::Name { name }
100                        } else {
101                            Self::Str(name)
102                        }
103                    } else {
104                        Self::Raw { raw_name: name }
105                    }
106                }
107                $(
108                    $struct::$case => Self::Str($str.to_owned())
109                ),*
110            }
111        }
112    }
113
114    impl TryFrom<Serde> for $struct {
115        type Error = crate::Error;
116        fn try_from(ent: Serde) -> Result<Self, Self::Error> {
117            Ok(match ent.disambiguate() {
118                Serde::Str(_) | Serde::Num(_) => {
119                    panic!("These should have been caught by disambiguate.")
120                }
121                Serde::Bool(b) => $struct::from_boolean(b)?,
122                Serde::Name { name } => $struct::Name(name.into()),
123                Serde::Raw { raw_name } => $struct::Name(raw_name),
124                Serde::Special { special } => {
125                    $struct::from_special_str(special.as_ref())?
126                }
127                Serde::Id { id } => $struct::Id(id),
128            })
129        }
130    }
131}}
132
133implement_serde! { TrustedUser {
134    None => ":none",
135    Current => ":current",
136    [NoSuchUser]
137}}
138
139implement_serde! { TrustedGroup {
140    None => ":none",
141    SelfNamed => ":username",
142    [NoSuchGroup]
143}}
144
145#[cfg(test)]
146mod test {
147    // @@ begin test lint list maintained by maint/add_warning @@
148    #![allow(clippy::bool_assert_comparison)]
149    #![allow(clippy::clone_on_copy)]
150    #![allow(clippy::dbg_macro)]
151    #![allow(clippy::mixed_attributes_style)]
152    #![allow(clippy::print_stderr)]
153    #![allow(clippy::print_stdout)]
154    #![allow(clippy::single_char_pattern)]
155    #![allow(clippy::unwrap_used)]
156    #![allow(clippy::unchecked_duration_subtraction)]
157    #![allow(clippy::useless_vec)]
158    #![allow(clippy::needless_pass_by_value)]
159    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
160    use super::*;
161
162    #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
163    struct Chum {
164        handle: TrustedUser,
165        team: TrustedGroup,
166    }
167
168    #[test]
169    fn round_trips() {
170        let examples: Vec<(&'static str, &'static str, Chum)> = vec![
171            (
172                r#"handle = "gardenGnostic"
173                   team = 413
174                  "#,
175                r#"{ "handle": "gardenGnostic", "team": 413 }"#,
176                Chum {
177                    handle: TrustedUser::Name("gardenGnostic".into()),
178                    team: TrustedGroup::Id(413),
179                },
180            ),
181            (
182                r#"handle = "413"
183                   team = false
184                  "#,
185                r#"{ "handle": "413", "team": false }"#,
186                Chum {
187                    handle: TrustedUser::Name("413".into()),
188                    team: TrustedGroup::None,
189                },
190            ),
191            (
192                r#"handle = { id = 8 }
193                   team = { name = "flarp" }
194                 "#,
195                r#"{ "handle": { "id": 8 }, "team" : { "name" : "flarp" } }"#,
196                Chum {
197                    handle: TrustedUser::Id(8),
198                    team: TrustedGroup::Name("flarp".into()),
199                },
200            ),
201            (
202                r#"handle = ":current"
203                   team = ":username"
204                 "#,
205                r#"{ "handle": ":current", "team" : ":username" }"#,
206                Chum {
207                    handle: TrustedUser::Current,
208                    team: TrustedGroup::SelfNamed,
209                },
210            ),
211            (
212                r#"handle = { special = ":none" }
213                   team = { special = ":none" }
214                 "#,
215                r#"{ "handle": {"special" : ":none"}, "team" : { "special" : ":none"} }"#,
216                Chum {
217                    handle: TrustedUser::None,
218                    team: TrustedGroup::None,
219                },
220            ),
221            (
222                r#"handle = { name = ":none" }
223                   team = { name = ":none" }
224                 "#,
225                r#"{ "handle": {"name" : ":none"}, "team" : { "name" : ":none"} }"#,
226                Chum {
227                    handle: TrustedUser::Name(":none".into()),
228                    team: TrustedGroup::Name(":none".into()),
229                },
230            ),
231        ];
232
233        for (toml_string, json_string, chum) in examples {
234            let toml_obj: Chum = toml::from_str(toml_string).unwrap();
235            let json_obj: Chum = serde_json::from_str(json_string).unwrap();
236            assert_eq!(&toml_obj, &chum);
237            assert_eq!(&json_obj, &chum);
238
239            let s = toml::to_string(&chum).unwrap();
240            let toml_obj2: Chum = toml::from_str(&s).unwrap();
241            assert_eq!(&toml_obj2, &chum);
242
243            let s = serde_json::to_string(&chum).unwrap();
244            let json_obj2: Chum = serde_json::from_str(&s).unwrap();
245            assert_eq!(&json_obj2, &chum);
246        }
247    }
248
249    #[cfg(target_family = "unix")]
250    #[test]
251    fn os_string() {
252        // Try round-tripping a username that isn't UTF8.
253        use std::os::unix::ffi::OsStringExt as _;
254        let not_utf8 = OsString::from_vec(vec![255, 254, 253, 252]);
255        assert!(not_utf8.to_str().is_none());
256        let chum = Chum {
257            handle: TrustedUser::Name(not_utf8.clone()),
258            team: TrustedGroup::Name(not_utf8),
259        };
260
261        // Alas, we cannot serialize an OsString in Toml. serde thinks that an
262        // OsString should be represented using `serialize_newtype_variant`, and
263        // the toml crate doesn't support that method.
264        //
265        //let toml_result = toml::to_string(&chum);
266        //assert!(toml_result.is_err());
267
268        let s = serde_json::to_string(&chum).unwrap();
269        let toml_obj: Chum = serde_json::from_str(&s).unwrap();
270        assert_eq!(&toml_obj, &chum);
271    }
272
273    #[test]
274    fn bad_names() {
275        let s = r#"handle = 413
276            team = false"#;
277        let r: Result<Chum, _> = toml::from_str(s);
278        assert!(r.is_ok());
279
280        let s = r#"handle = true
281            team = false"#;
282        let r: Result<Chum, _> = toml::from_str(s);
283        assert!(r.is_err());
284
285        let s = r#"handle = ":foo"
286            team = false"#;
287        let r: Result<Chum, _> = toml::from_str(s);
288        assert!(r.is_err());
289    }
290}