Skip to main content

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_time_subtraction)]
157    #![allow(clippy::useless_vec)]
158    #![allow(clippy::needless_pass_by_value)]
159    #![allow(clippy::string_slice)] // See arti#2571
160    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
161    use super::*;
162
163    #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
164    struct Chum {
165        handle: TrustedUser,
166        team: TrustedGroup,
167    }
168
169    #[test]
170    fn round_trips() {
171        let examples: Vec<(&'static str, &'static str, Chum)> = vec![
172            (
173                r#"handle = "gardenGnostic"
174                   team = 413
175                  "#,
176                r#"{ "handle": "gardenGnostic", "team": 413 }"#,
177                Chum {
178                    handle: TrustedUser::Name("gardenGnostic".into()),
179                    team: TrustedGroup::Id(413),
180                },
181            ),
182            (
183                r#"handle = "413"
184                   team = false
185                  "#,
186                r#"{ "handle": "413", "team": false }"#,
187                Chum {
188                    handle: TrustedUser::Name("413".into()),
189                    team: TrustedGroup::None,
190                },
191            ),
192            (
193                r#"handle = { id = 8 }
194                   team = { name = "flarp" }
195                 "#,
196                r#"{ "handle": { "id": 8 }, "team" : { "name" : "flarp" } }"#,
197                Chum {
198                    handle: TrustedUser::Id(8),
199                    team: TrustedGroup::Name("flarp".into()),
200                },
201            ),
202            (
203                r#"handle = ":current"
204                   team = ":username"
205                 "#,
206                r#"{ "handle": ":current", "team" : ":username" }"#,
207                Chum {
208                    handle: TrustedUser::Current,
209                    team: TrustedGroup::SelfNamed,
210                },
211            ),
212            (
213                r#"handle = { special = ":none" }
214                   team = { special = ":none" }
215                 "#,
216                r#"{ "handle": {"special" : ":none"}, "team" : { "special" : ":none"} }"#,
217                Chum {
218                    handle: TrustedUser::None,
219                    team: TrustedGroup::None,
220                },
221            ),
222            (
223                r#"handle = { name = ":none" }
224                   team = { name = ":none" }
225                 "#,
226                r#"{ "handle": {"name" : ":none"}, "team" : { "name" : ":none"} }"#,
227                Chum {
228                    handle: TrustedUser::Name(":none".into()),
229                    team: TrustedGroup::Name(":none".into()),
230                },
231            ),
232        ];
233
234        for (toml_string, json_string, chum) in examples {
235            let toml_obj: Chum = toml::from_str(toml_string).unwrap();
236            let json_obj: Chum = serde_json::from_str(json_string).unwrap();
237            assert_eq!(&toml_obj, &chum);
238            assert_eq!(&json_obj, &chum);
239
240            let s = toml::to_string(&chum).unwrap();
241            let toml_obj2: Chum = toml::from_str(&s).unwrap();
242            assert_eq!(&toml_obj2, &chum);
243
244            let s = serde_json::to_string(&chum).unwrap();
245            let json_obj2: Chum = serde_json::from_str(&s).unwrap();
246            assert_eq!(&json_obj2, &chum);
247        }
248    }
249
250    #[cfg(target_family = "unix")]
251    #[test]
252    fn os_string() {
253        // Try round-tripping a username that isn't UTF8.
254        use std::os::unix::ffi::OsStringExt as _;
255        let not_utf8 = OsString::from_vec(vec![255, 254, 253, 252]);
256        assert!(not_utf8.to_str().is_none());
257        let chum = Chum {
258            handle: TrustedUser::Name(not_utf8.clone()),
259            team: TrustedGroup::Name(not_utf8),
260        };
261
262        // Alas, we cannot serialize an OsString in Toml. serde thinks that an
263        // OsString should be represented using `serialize_newtype_variant`, and
264        // the toml crate doesn't support that method.
265        //
266        //let toml_result = toml::to_string(&chum);
267        //assert!(toml_result.is_err());
268
269        let s = serde_json::to_string(&chum).unwrap();
270        let toml_obj: Chum = serde_json::from_str(&s).unwrap();
271        assert_eq!(&toml_obj, &chum);
272    }
273
274    #[test]
275    fn bad_names() {
276        let s = r#"handle = 413
277            team = false"#;
278        let r: Result<Chum, _> = toml::from_str(s);
279        assert!(r.is_ok());
280
281        let s = r#"handle = true
282            team = false"#;
283        let r: Result<Chum, _> = toml::from_str(s);
284        assert!(r.is_err());
285
286        let s = r#"handle = ":foo"
287            team = false"#;
288        let r: Result<Chum, _> = toml::from_str(s);
289        assert!(r.is_err());
290    }
291}