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}