serde_support.rs
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 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 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 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 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 }