orphanage 0.5.6

Random collection of stuff that is still searching for a home.
Documentation
/// Representation of passphrase policies.
pub struct PassPolicy {
  /// Minimum passphrase length.
  pub min_len: usize,

  /// Minimum number of passphrase character classes.
  ///
  /// `PaaPolicy` recognized the following character classes:
  /// - Lower case letters (`a`-`z`)
  /// - Upper case letters (`A`-`Z`)
  /// - Numeric digits (`0`-`9`)
  /// - Special characters (anything else)
  pub min_char_classes: usize
}

impl PassPolicy {
  #[must_use]
  pub const fn none() -> Self {
    Self {
      min_len: 0,
      min_char_classes: 0
    }
  }

  #[must_use]
  pub const fn strong() -> Self {
    Self {
      min_len: 12,
      min_char_classes: 3
    }
  }
}

impl PassPolicy {
  #[must_use]
  pub fn spec(&self) -> String {
    let minlen = if self.min_len > 1 {
      Some(format!("be at least {} characters", self.min_len))
    } else if self.min_len > 0 {
      Some("be at least 1 character".to_string())
    } else {
      None
    };

    let classes_str = "(upper-case letter, lower-case letter, digit, special)";

    let minclasses = if self.min_char_classes > 1 {
      Some(format!(
        "contain at least {} character classes {classes_str}",
        self.min_char_classes
      ))
    } else if self.min_char_classes > 0 {
      Some(format!("contain at least 1 character class {classes_str}"))
    } else {
      None
    };

    match (minlen, minclasses) {
      (Some(minlen), Some(minclasses)) => {
        format!("must {minlen} and {minclasses}")
      }
      (None, Some(minclasses)) => {
        format!("must {minclasses}")
      }
      (Some(minlen), None) => {
        format!("must {minlen}")
      }
      (None, None) => "has no restrictions".into()
    }
  }

  /// Validate a passphrase against policy settings.
  ///
  /// # Errors
  /// Returns a `String` describing the constraint violation.
  pub fn validate(&self, pass: &str) -> Result<(), String> {
    let mut lcase = 0;
    let mut ucase = 0;
    let mut digit = 0;
    let mut special = 0;
    let mut len = 0;
    for ch in pass.chars() {
      len += 1;
      if ch.is_ascii_lowercase() {
        lcase += 1;
      } else if ch.is_ascii_uppercase() {
        ucase += 1;
      } else if ch.is_ascii_digit() {
        digit += 1;
      } else {
        special += 1;
      }
    }
    let mut classes = 0;
    if lcase > 0 {
      classes += 1;
    }
    if ucase > 0 {
      classes += 1;
    }
    if digit > 0 {
      classes += 1;
    }
    if special > 0 {
      classes += 1;
    }

    let lenerr = if len < self.min_len {
      Some("too short")
    } else {
      None
    };
    let classerr = if classes < self.min_char_classes {
      Some("too few character classes")
    } else {
      None
    };

    match (lenerr, classerr) {
      (Some(lenerr), Some(classerr)) => {
        Err(format!("{lenerr} and {classerr}"))
      }
      (Some(lenerr), None) => Err(lenerr.to_string()),
      (None, Some(classerr)) => Err(classerr.to_string()),
      (None, None) => Ok(())
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn specs() {
    let pp = PassPolicy {
      min_len: 0,
      min_char_classes: 0
    };
    assert_eq!(pp.spec(), "has no restrictions");

    let pp = PassPolicy {
      min_len: 1,
      min_char_classes: 0
    };
    assert_eq!(pp.spec(), "must be at least 1 character");

    let pp = PassPolicy {
      min_len: 2,
      min_char_classes: 0
    };
    assert_eq!(pp.spec(), "must be at least 2 characters");

    let pp = PassPolicy {
      min_len: 0,
      min_char_classes: 1
    };
    assert_eq!(
      pp.spec(),
      "must contain at least 1 character class (upper-case letter, \
       lower-case letter, digit, special)"
    );

    let pp = PassPolicy {
      min_len: 0,
      min_char_classes: 2
    };
    assert_eq!(
      pp.spec(),
      "must contain at least 2 character classes (upper-case letter, \
       lower-case letter, digit, special)"
    );

    let pp = PassPolicy {
      min_len: 2,
      min_char_classes: 2
    };
    assert_eq!(
      pp.spec(),
      "must be at least 2 characters and contain at least 2 character \
       classes (upper-case letter, lower-case letter, digit, special)"
    );
  }

  #[test]
  fn validate() {
    PassPolicy {
      min_len: 0,
      min_char_classes: 0
    }
    .validate("")
    .unwrap();

    let Err(e) = PassPolicy {
      min_len: 1,
      min_char_classes: 0
    }
    .validate("") else {
      panic!("Unexpectedly not Err()");
    };
    assert_eq!(e, "too short");

    let Err(e) = PassPolicy {
      min_len: 0,
      min_char_classes: 1
    }
    .validate("") else {
      panic!("Unexpectedly not Err()");
    };
    assert_eq!(e, "too few character classes");

    let Err(e) = PassPolicy {
      min_len: 1,
      min_char_classes: 1
    }
    .validate("") else {
      panic!("Unexpectedly not Err()");
    };
    assert_eq!(e, "too short and too few character classes");
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :