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
400
    fn disambiguate(self) -> Self {
61
342
        match self {
62
342
            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
400
    }
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
336
        fn from_special_str(s: &str) -> Result<Self, crate::Error> {
77
336
            match s {
78
308
                $( $str => Ok($struct::$case), )*
79
2
                _ => Err(crate::Error::$errcase(s.to_owned())),
80
            }
81
336
        }
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
12
                $struct::Id(id) => Self::Num(id),
95
34
                $struct::Name(name) => {
96
34
                    if let Some(name) = name.to_str() {
97
30
                        let name = name.to_string();
98
30
                        if name.starts_with(':') {
99
12
                            Self::Name { name }
100
                        } else {
101
18
                            Self::Str(name)
102
                        }
103
                    } else {
104
4
                        Self::Raw { raw_name: name }
105
                    }
106
                }
107
                $(
108
18
                    $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
400
        fn try_from(ent: Serde) -> Result<Self, Self::Error> {
117
400
            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
330
                Serde::Special { special } => {
125
330
                    $struct::from_special_str(special.as_ref())?
126
                }
127
18
                Serde::Id { id } => $struct::Id(id),
128
            })
129
400
        }
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_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
}