mk_pass/
config.rs

1use crate::helpers::{DECIMAL, LOWERCASE, SPECIAL_CHARACTERS, UPPERCASE};
2
3#[cfg(feature = "clap")]
4use clap::{ArgAction, Parser};
5
6/// A structure to describe password requirements.
7#[cfg_attr(
8    feature = "clap",
9    derive(Parser),
10    command(about = "Generate a password comprehensively.", version, long_about = None)
11)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct PasswordRequirements {
14    /// The length of the password.
15    #[cfg_attr(feature = "clap", arg(long, short, default_value = "16"))]
16    pub length: u16,
17
18    /// How many decimal integer characters should the password contain?
19    #[cfg_attr(feature = "clap", arg(long, short, default_value = "1"))]
20    pub decimal: u16,
21
22    /// How many special characters should the password contain?
23    #[cfg_attr(feature = "clap", arg(long, short, default_value = "1"))]
24    pub specials: u16,
25
26    /// Should the first character always be a letter?
27    #[cfg_attr(
28        feature = "clap",
29        arg(
30            long = "no-first-is-letter",
31            short,
32            help = "Do not restrict the first character to only letters.",
33            long_help = "Do not restrict the first character to only letters.\
34            \n\nBy default, the first character is always a letter.",
35            action = ArgAction::SetFalse
36        )
37    )]
38    pub first_is_letter: bool,
39
40    /// Allow characters to be used more than once?
41    #[cfg_attr(
42        feature = "clap",
43        arg(
44            short = 'r',
45            long,
46            help = "Allow character to used more than once.",
47            long_help = "Allow character to used more than once.\
48            \n\nBy default, each generated character is only used once.\n\
49            Allowing repetitions also relaxes the maximum length.",
50            action = ArgAction::SetTrue
51        )
52    )]
53    pub allow_repeats: bool,
54}
55
56impl PasswordRequirements {
57    /// Validates the instance's values.
58    ///
59    /// This returns a mutated copy of the instance where the values satisfy
60    /// "sane minimum requirements" suitable for any password.
61    ///
62    /// The phrase "sane minimum requirements" implies
63    ///
64    /// 1. `length` is not less than 10
65    /// 2. To avoid repetitions, `length` is not more than
66    ///
67    ///    - 52 if only letters (no decimal integers or special characters) are used
68    ///    - 62 if only letters and decimal integers are used
69    ///    - 68 if only letters and special characters are used
70    ///    - 78 if letters, decimal integers, and special characters are used
71    ///    - [u16::MAX] if repeated characters are allowed
72    /// 3. `specials` character count does not overrule the required number of
73    ///
74    ///    - letters (2; 1 uppercase and 1 lowercase)
75    ///    - decimal integers (if `decimal` is specified as non-zero value)
76    /// 4. `decimal` character count does not overrule the required number of
77    ///
78    ///    - letters (2; 1 uppercase and 1 lowercase)
79    ///    - special characters (if `specials` is specified as non-zero value)
80    ///
81    /// # About resolving conflicts
82    ///
83    /// If this function finds a conflict between the specified number of
84    /// `specials` characters and `decimal`, then decimal integers takes precedence.
85    ///
86    /// For example:
87    ///
88    /// ```rust
89    /// use mk_pass::PasswordRequirements;
90    /// let req = PasswordRequirements {
91    ///     length: 16,
92    ///     specials: 16,
93    ///     decimal: 16,
94    ///     ..Default::default()
95    /// };
96    /// let expected = PasswordRequirements {
97    ///     length: 16,
98    ///     specials: 1,
99    ///     decimal: 13,
100    ///     ..Default::default()
101    /// };
102    /// assert_eq!(req.validate(), expected);
103    /// ```
104    pub fn validate(&self) -> Self {
105        let mut len = self.length.max(10);
106        if !self.allow_repeats {
107            len = len.min(
108                UPPERCASE.len() as u16
109                    + LOWERCASE.len() as u16
110                    + {
111                        if self.specials > 0 {
112                            SPECIAL_CHARACTERS.len() as u16
113                        } else {
114                            0
115                        }
116                    }
117                    + {
118                        if self.decimal > 0 {
119                            DECIMAL.len() as u16
120                        } else {
121                            0
122                        }
123                    },
124            );
125        }
126        let non_letter_max_len = len - 2;
127        let max_special = if self.specials > 0 {
128            non_letter_max_len - self.decimal.min(non_letter_max_len - 1)
129        } else {
130            0
131        };
132        let max_decimal = non_letter_max_len - max_special;
133        Self {
134            length: len,
135            decimal: self.decimal.min(max_decimal),
136            specials: self.specials.min(max_special),
137            first_is_letter: self.first_is_letter,
138            allow_repeats: self.allow_repeats,
139        }
140    }
141}
142
143impl Default for PasswordRequirements {
144    /// Create default password requirements.
145    fn default() -> Self {
146        Self {
147            length: 16,
148            decimal: 1,
149            specials: 1,
150            first_is_letter: true,
151            allow_repeats: false,
152        }
153    }
154}