1
//! Serde support for [`TrustedUser`] and [`TrustedGroup`].
2

            
3
use super::{TrustedGroup, TrustedUser};
4
use serde::{Deserialize, Serialize};
5
use 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)]
14
pub(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

            
55
impl 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
424
    fn disambiguate(self) -> Self {
61
366
        match self {
62
366
            Serde::Str(s) if s.starts_with(':') => Self::Special { special: s },
63
20
            Serde::Str(s) => Self::Name { name: s },
64
14
            Serde::Num(id) => Self::Id { id },
65
44
            other => other,
66
        }
67
424
    }
68
}
69

            
70
/// Helper: declare
71
macro_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
360
        fn from_special_str(s: &str) -> Result<Self, crate::Error> {
77
360
            match s {
78
334
                $( $str => Ok($struct::$case), )*
79
2
                _ => Err(crate::Error::$errcase(s.to_owned())),
80
            }
81
360
        }
82
8
        fn from_boolean(b: bool) -> Result<Self, crate::Error> {
83
8
            if b {
84
2
                Err(crate::Error::$errcase("'true'".into()))
85
            } else {
86
6
                Self::from_special_str(":none")
87
            }
88
8
        }
89
    }
90

            
91
    impl From<$struct> for Serde {
92
368
        fn from(value: $struct) -> Self {
93
368
            match value {
94
8
                $struct::Id(id) => Self::Num(id),
95
24
                $struct::Name(name) => {
96
24
                    if let Some(name) = name.to_str() {
97
20
                        let name = name.to_string();
98
20
                        if name.starts_with(':') {
99
8
                            Self::Name { name }
100
                        } else {
101
12
                            Self::Str(name)
102
                        }
103
                    } else {
104
4
                        Self::Raw { raw_name: name }
105
                    }
106
                }
107
                $(
108
12
                    $struct::$case => Self::Str($str.to_owned())
109
                ),*
110
            }
111
368
        }
112
    }
113

            
114
    impl TryFrom<Serde> for $struct {
115
        type Error = crate::Error;
116
424
        fn try_from(ent: Serde) -> Result<Self, Self::Error> {
117
424
            Ok(match ent.disambiguate() {
118
                Serde::Str(_) | Serde::Num(_) => {
119
                    panic!("These should have been caught by disambiguate.")
120
                }
121
8
                Serde::Bool(b) => $struct::from_boolean(b)?,
122
40
                Serde::Name { name } => $struct::Name(name.into()),
123
4
                Serde::Raw { raw_name } => $struct::Name(raw_name),
124
354
                Serde::Special { special } => {
125
354
                    $struct::from_special_str(special.as_ref())?
126
                }
127
18
                Serde::Id { id } => $struct::Id(id),
128
            })
129
424
        }
130
    }
131
}}
132

            
133
implement_serde! { TrustedUser {
134
    None => ":none",
135
    Current => ":current",
136
    [NoSuchUser]
137
}}
138

            
139
implement_serde! { TrustedGroup {
140
    None => ":none",
141
    SelfNamed => ":username",
142
    [NoSuchGroup]
143
}}
144

            
145
#[cfg(test)]
146
mod 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
}