Skip to content

Commit bf50c2e

Browse files
authored
mcf: add Field newtype (#1914)
Also extracts a `fields` submodule. The `Field` newtype is useful as a place to provide additional methods for operating on individual fields, such as encoding/decoding various flavors of Base64, or parsing `k=v,...` mappings. The functionality to do that is not yet implemented, but having a type where we can put such functionality is the starting place.
1 parent f7b2754 commit bf50c2e

File tree

3 files changed

+105
-66
lines changed

3 files changed

+105
-66
lines changed

mcf/src/fields.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Fields of an MCF password hash, delimited by `$`
2+
3+
use crate::{Error, Result};
4+
use core::fmt;
5+
6+
/// MCF field delimiter: `$`.
7+
pub const DELIMITER: char = '$';
8+
9+
/// Iterator over the `$`-delimited fields of an MCF hash.
10+
pub struct Fields<'a>(&'a str);
11+
12+
impl<'a> Fields<'a> {
13+
/// Create a new field iterator from an MCF hash, returning an error in the event the hash
14+
/// doesn't start with a leading `$` prefix.
15+
pub(crate) fn new(s: &'a str) -> Result<Self> {
16+
let mut ret = Self(s);
17+
18+
if ret.next() != Some(Field("")) {
19+
return Err(Error {});
20+
}
21+
22+
Ok(ret)
23+
}
24+
}
25+
26+
impl<'a> Iterator for Fields<'a> {
27+
type Item = Field<'a>;
28+
29+
fn next(&mut self) -> Option<Field<'a>> {
30+
if self.0.is_empty() {
31+
return None;
32+
}
33+
34+
match self.0.split_once(DELIMITER) {
35+
Some((field, rest)) => {
36+
self.0 = rest;
37+
Some(Field(field))
38+
}
39+
None => {
40+
let ret = self.0;
41+
self.0 = "";
42+
Some(Field(ret))
43+
}
44+
}
45+
}
46+
}
47+
48+
/// Individual field of an MCF hash, delimited by `$`.
49+
///
50+
/// Fields are constrained to characters in the regexp range `[A-Za-z0-9./+=,\-]`.
51+
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
52+
pub struct Field<'a>(&'a str);
53+
54+
impl<'a> Field<'a> {
55+
/// Create a new [`Field`], validating the provided characters are in the allowed range.
56+
pub fn new(s: &'a str) -> Result<Self> {
57+
let field = Field(s);
58+
field.validate()?;
59+
Ok(field)
60+
}
61+
62+
/// Borrow the field's contents as a `str`.
63+
pub fn as_str(&self) -> &'a str {
64+
self.0
65+
}
66+
67+
/// Validate a field in the password hash is well-formed.
68+
pub(crate) fn validate(&self) -> Result<()> {
69+
if self.0.is_empty() {
70+
return Err(Error {});
71+
}
72+
73+
for c in self.0.chars() {
74+
match c {
75+
'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '/' | '+' | '=' | ',' | '-' => (),
76+
_ => return Err(Error {}),
77+
}
78+
}
79+
80+
Ok(())
81+
}
82+
}
83+
84+
impl AsRef<str> for Field<'_> {
85+
fn as_ref(&self) -> &str {
86+
self.as_str()
87+
}
88+
}
89+
90+
impl fmt::Display for Field<'_> {
91+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92+
f.write_str(self.0)
93+
}
94+
}

mcf/src/lib.rs

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
extern crate alloc;
1717

18+
mod fields;
19+
20+
pub use fields::{Field, Fields};
21+
1822
use alloc::string::String;
1923
use core::{fmt, str};
2024

21-
/// MCF field delimiter: `$`.
22-
pub const DELIMITER: char = '$';
23-
2425
/// Debug message used in panics when invariants aren't properly held.
2526
const INVARIANT_MSG: &str = "should be ensured valid by constructor";
2627

@@ -59,6 +60,7 @@ impl McfHash {
5960
.expect(INVARIANT_MSG)
6061
.next()
6162
.expect(INVARIANT_MSG)
63+
.as_str()
6264
}
6365

6466
/// Get an iterator over the parts of the password hash as delimited by `$`, excluding the
@@ -68,7 +70,7 @@ impl McfHash {
6870

6971
// Remove the leading identifier
7072
let id = fields.next().expect(INVARIANT_MSG);
71-
debug_assert_eq!(id, self.id());
73+
debug_assert_eq!(self.id(), id.as_str());
7274

7375
fields
7476
}
@@ -101,13 +103,13 @@ fn validate(s: &str) -> Result<()> {
101103

102104
// Validate characters in the identifier field
103105
let id = fields.next().ok_or(Error {})?;
104-
validate_id(id)?;
106+
validate_id(id.as_str())?;
105107

106108
// Validate the remaining fields have an appropriate format
107109
let mut any = false;
108110
for field in fields {
109111
any = true;
110-
validate_field(field)?;
112+
field.validate()?;
111113
}
112114

113115
// Must have at least one field.
@@ -143,63 +145,6 @@ fn validate_id(id: &str) -> Result<()> {
143145
Ok(())
144146
}
145147

146-
/// Validate a field in the password hash is well-formed.
147-
///
148-
/// Fields include characters in the regexp range `[A-Za-z0-9./+=,\-]`.
149-
fn validate_field(field: &str) -> Result<()> {
150-
if field.is_empty() {
151-
return Err(Error {});
152-
}
153-
154-
for c in field.chars() {
155-
match c {
156-
'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '/' | '+' | '=' | ',' | '-' => (),
157-
_ => return Err(Error {}),
158-
}
159-
}
160-
161-
Ok(())
162-
}
163-
164-
/// Iterator over the `$`-delimited fields of an MCF hash.
165-
pub struct Fields<'a>(&'a str);
166-
167-
impl<'a> Fields<'a> {
168-
/// Create a new field iterator from an MCF hash, returning an error in the event the hash
169-
/// doesn't start with a leading `$` prefix.
170-
fn new(s: &'a str) -> Result<Self> {
171-
let mut ret = Self(s);
172-
173-
if ret.next() != Some("") {
174-
return Err(Error {});
175-
}
176-
177-
Ok(ret)
178-
}
179-
}
180-
181-
impl<'a> Iterator for Fields<'a> {
182-
type Item = &'a str;
183-
184-
fn next(&mut self) -> Option<&'a str> {
185-
if self.0.is_empty() {
186-
return None;
187-
}
188-
189-
match self.0.split_once(DELIMITER) {
190-
Some((field, rest)) => {
191-
self.0 = rest;
192-
Some(field)
193-
}
194-
None => {
195-
let ret = self.0;
196-
self.0 = "";
197-
Some(ret)
198-
}
199-
}
200-
}
201-
}
202-
203148
/// Result type for `mcf`.
204149
pub type Result<T> = core::result::Result<T, Error>;
205150

mcf/tests/mcf.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ fn parse_sha512_hash() {
2222
assert_eq!("6", hash.id());
2323

2424
let mut fields = hash.fields();
25-
assert_eq!("rounds=100000", fields.next().unwrap());
26-
assert_eq!("exn6tVc2j/MZD8uG", fields.next().unwrap());
25+
assert_eq!("rounds=100000", fields.next().unwrap().as_str());
26+
assert_eq!("exn6tVc2j/MZD8uG", fields.next().unwrap().as_str());
2727
assert_eq!(
2828
"BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0",
29-
fields.next().unwrap()
29+
fields.next().unwrap().as_str()
3030
);
3131
assert_eq!(None, fields.next());
3232
}

0 commit comments

Comments
 (0)