Skip to main content

phc/
value.rs

1//! Algorithm parameter value as defined by the [PHC string format].
2//!
3//! Implements the following parts of the specification:
4//!
5//! > The value for each parameter consists in characters in: `[a-zA-Z0-9/+.-]`
6//! > (lowercase letters, uppercase letters, digits, `/`, `+`, `.` and `-`). No other
7//! > character is allowed. Interpretation of the value depends on the
8//! > parameter and the function. The function specification MUST unambiguously
9//! > define the set of valid parameter values. The function specification MUST
10//! > define a maximum length (in characters) for each parameter. For numerical
11//! > parameters, functions SHOULD use plain decimal encoding (other encodings
12//! > are possible as long as they are clearly defined).
13//!
14//! [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
15
16use crate::{B64, Error, Result};
17use base64ct::Encoding;
18use core::{fmt, str};
19
20/// Type used to represent decimal (i.e. integer) values.
21pub type Decimal = u32;
22
23/// Algorithm parameter value string.
24///
25/// Parameter values are defined in the [PHC string format specification][1].
26///
27/// # Constraints
28/// - ASCII-encoded string consisting of the characters `[a-zA-Z0-9/+.-]`
29///   (lowercase letters, digits, and the minus sign)
30/// - Minimum length: 0 (i.e. empty values are allowed)
31/// - Maximum length: 64 ASCII characters (i.e. 64-bytes)
32///
33/// # Additional Notes
34/// The PHC spec allows for algorithm-defined maximum lengths for parameter
35/// values, however this library defines a [`Value::MAX_LENGTH`] of 64 ASCII
36/// characters.
37///
38/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
39/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
40#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
41pub struct Value<'a>(&'a str);
42
43impl<'a> Value<'a> {
44    /// Maximum length of an [`Value`] - 64 ASCII characters (i.e. 64-bytes).
45    pub const MAX_LENGTH: usize = 64;
46
47    /// Parse a [`Value`] from the provided `str`, validating it according to
48    /// the PHC string format's rules.
49    pub fn new(input: &'a str) -> Result<Self> {
50        if input.len() > Self::MAX_LENGTH {
51            return Err(Error::ParamValueTooLong);
52        }
53
54        // Check that the characters are permitted in a PHC parameter value.
55        assert_valid_value(input)?;
56        Ok(Self(input))
57    }
58
59    /// Attempt to decode a B64-encoded [`Value`], writing the decoded
60    /// result into the provided buffer, and returning a slice of the buffer
61    /// containing the decoded result on success.
62    ///
63    /// Examples of "B64"-encoded parameters in practice are the `keyid` and
64    /// `data` parameters used by the [Argon2 Encoding][1] as described in the
65    /// PHC string format specification.
66    ///
67    /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
68    pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> {
69        Ok(B64::decode(self.as_str(), buf)?)
70    }
71
72    /// Borrow this value as a `str`.
73    pub fn as_str(&self) -> &'a str {
74        self.0
75    }
76
77    /// Borrow this value as bytes.
78    pub fn as_bytes(&self) -> &'a [u8] {
79        self.as_str().as_bytes()
80    }
81
82    /// Get the length of this value in ASCII characters.
83    pub fn len(&self) -> usize {
84        self.as_str().len()
85    }
86
87    /// Is this value empty?
88    pub fn is_empty(&self) -> bool {
89        self.as_str().is_empty()
90    }
91
92    /// Attempt to parse this [`Value`] as a PHC-encoded decimal (i.e. integer).
93    ///
94    /// Decimal values are integers which follow the rules given in the
95    /// ["Decimal Encoding" section of the PHC string format specification][1].
96    ///
97    /// The decimal encoding rules are as follows:
98    /// > For an integer value x, its decimal encoding consist in the following:
99    /// >
100    /// > - If x < 0, then its decimal encoding is the minus sign - followed by the decimal
101    /// >   encoding of -x.
102    /// > - If x = 0, then its decimal encoding is the single character 0.
103    /// > - If x > 0, then its decimal encoding is the smallest sequence of ASCII digits that
104    /// >   matches its value (i.e. there is no leading zero).
105    /// >
106    /// > Thus, a value is a valid decimal for an integer x if and only if all of the following hold true:
107    /// >
108    /// > - The first character is either a - sign, or an ASCII digit.
109    /// > - All characters other than the first are ASCII digits.
110    /// > - If the first character is - sign, then there is at least another character, and the
111    /// >   second character is not a 0.
112    /// > - If the string consists in more than one character, then the first one cannot be a 0.
113    ///
114    /// Note: this implementation does not support negative decimals despite
115    /// them being allowed per the spec above. If you need to parse a negative
116    /// number, please parse it from the string representation directly e.g.
117    /// `value.as_str().parse::<i32>()`
118    ///
119    /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#decimal-encoding
120    pub fn decimal(&self) -> Result<Decimal> {
121        let value = self.as_str();
122
123        // Empty strings aren't decimals
124        if value.is_empty() {
125            return Err(Error::ParamValueInvalid);
126        }
127
128        // Ensure all characters are digits
129        for c in value.chars() {
130            if !c.is_ascii_digit() {
131                return Err(Error::ParamValueInvalid);
132            }
133        }
134
135        // Disallow leading zeroes
136        if value.starts_with('0') && value.len() > 1 {
137            return Err(Error::ParamValueInvalid);
138        }
139
140        value.parse().map_err(|_| Error::ParamValueInvalid)
141    }
142
143    /// Does this value parse successfully as a decimal?
144    pub fn is_decimal(&self) -> bool {
145        self.decimal().is_ok()
146    }
147}
148
149impl AsRef<str> for Value<'_> {
150    fn as_ref(&self) -> &str {
151        self.as_str()
152    }
153}
154
155impl<'a> TryFrom<&'a str> for Value<'a> {
156    type Error = Error;
157
158    fn try_from(input: &'a str) -> Result<Self> {
159        Self::new(input)
160    }
161}
162
163impl<'a> TryFrom<Value<'a>> for Decimal {
164    type Error = Error;
165
166    fn try_from(value: Value<'a>) -> Result<Decimal> {
167        Decimal::try_from(&value)
168    }
169}
170
171impl<'a> TryFrom<&Value<'a>> for Decimal {
172    type Error = Error;
173
174    fn try_from(value: &Value<'a>) -> Result<Decimal> {
175        value.decimal()
176    }
177}
178
179impl fmt::Display for Value<'_> {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.write_str(self.as_str())
182    }
183}
184
185/// Are all of the given bytes allowed in a [`Value`]?
186fn assert_valid_value(input: &str) -> Result<()> {
187    for c in input.chars() {
188        if !is_char_valid(c) {
189            return Err(Error::ParamValueInvalid);
190        }
191    }
192
193    Ok(())
194}
195
196/// Ensure the given ASCII character (i.e. byte) is allowed in a [`Value`].
197fn is_char_valid(c: char) -> bool {
198    matches!(c, 'A' ..= 'Z' | 'a'..='z' | '0'..='9' | '/' | '+' | '.' | '-')
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used)]
203mod tests {
204    use super::{Error, Value};
205
206    // Invalid value examples
207    const INVALID_CHAR: &str = "x;y";
208    const INVALID_TOO_LONG: &str =
209        "01234567891123456789212345678931234567894123456785234567896234567";
210    const INVALID_CHAR_AND_TOO_LONG: &str =
211        "0!234567891123456789212345678931234567894123456785234567896234567";
212
213    //
214    // Decimal parsing tests
215    //
216
217    #[test]
218    fn decimal_value() {
219        let valid_decimals = &[("0", 0u32), ("1", 1u32), ("4294967295", u32::MAX)];
220
221        for &(s, i) in valid_decimals {
222            let value = Value::new(s).unwrap();
223            assert!(value.is_decimal());
224            assert_eq!(value.decimal().unwrap(), i)
225        }
226    }
227
228    #[test]
229    fn reject_decimal_with_leading_zero() {
230        let value = Value::new("01").unwrap();
231        let err = u32::try_from(value).err().unwrap();
232        assert_eq!(err, Error::ParamValueInvalid);
233    }
234
235    #[test]
236    fn reject_overlong_decimal() {
237        let value = Value::new("4294967296").unwrap();
238        let err = u32::try_from(value).err().unwrap();
239        assert_eq!(err, Error::ParamValueInvalid);
240    }
241
242    #[test]
243    fn reject_negative() {
244        let value = Value::new("-1").unwrap();
245        let err = u32::try_from(value).err().unwrap();
246        assert_eq!(err, Error::ParamValueInvalid);
247    }
248
249    //
250    // String parsing tests
251    //
252
253    #[test]
254    fn string_value() {
255        let valid_examples = [
256            "",
257            "X",
258            "x",
259            "xXx",
260            "a+b.c-d",
261            "1/2",
262            "01234567891123456789212345678931",
263        ];
264
265        for &example in &valid_examples {
266            let value = Value::new(example).unwrap();
267            assert_eq!(value.as_str(), example);
268        }
269    }
270
271    #[test]
272    fn reject_invalid_char() {
273        let err = Value::new(INVALID_CHAR).err().unwrap();
274        assert_eq!(err, Error::ParamValueInvalid);
275    }
276
277    #[test]
278    fn reject_too_long() {
279        let err = Value::new(INVALID_TOO_LONG).err().unwrap();
280        assert_eq!(err, Error::ParamValueTooLong);
281    }
282
283    #[test]
284    fn reject_invalid_char_and_too_long() {
285        let err = Value::new(INVALID_CHAR_AND_TOO_LONG).err().unwrap();
286        assert_eq!(err, Error::ParamValueTooLong);
287    }
288}