/ crates / fs-mistrust / src / user / serde_support.rs
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  }