Skip to content

Commit f228c6c

Browse files
authored
mcf: add McfHashRef (#1925)
Adds a reference type for MCF hashes which works in no-`alloc` environments. Some methods of `McfHash` are now directly delegated. It would be possible to make a fully borrowed reference type that works like `&McfHashRef`, which could be used with the standard `AsRef`/`Borrow`/`Deref` `ToOwned` traits, but it would require unsafe code, which this avoids for the time being.
1 parent bc0c972 commit f228c6c

File tree

2 files changed

+66
-21
lines changed

2 files changed

+66
-21
lines changed

mcf/src/fields.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ pub struct Fields<'a>(&'a str);
1515
impl<'a> Fields<'a> {
1616
/// Create a new field iterator from an MCF hash, returning an error in the event the hash
1717
/// doesn't start with a leading `$` prefix.
18-
pub fn new(s: &'a str) -> Result<Self> {
18+
///
19+
/// NOTE: this method is deliberately non-public because it doesn't first validate the fields
20+
/// are well-formed. Calling it with non-validated inputs can lead to invalid [`Field`] values.
21+
pub(crate) fn new(s: &'a str) -> Self {
1922
let mut ret = Self(s);
2023

21-
if ret.next() != Some(Field("")) {
22-
return Err(Error {});
23-
}
24+
let should_be_empty = ret.next().expect("shouldn't be empty");
25+
debug_assert_eq!(should_be_empty.as_str(), "");
2426

25-
Ok(ret)
27+
ret
2628
}
2729
}
2830

mcf/src/lib.rs

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,47 @@ use {
3131
};
3232

3333
/// Debug message used in panics when invariants aren't properly held.
34-
#[cfg(feature = "alloc")]
3534
const INVARIANT_MSG: &str = "should be ensured valid by constructor";
3635

36+
/// Zero-copy decoder for hashes in the Modular Crypt Format (MCF).
37+
///
38+
/// For more information, see [`McfHash`].
39+
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
40+
pub struct McfHashRef<'a>(&'a str);
41+
42+
impl<'a> McfHashRef<'a> {
43+
/// Parse the given input string, returning an [`McfHashRef`] if valid.
44+
pub fn new(s: &'a str) -> Result<Self> {
45+
validate(s)?;
46+
Ok(Self(s))
47+
}
48+
49+
/// Get the contained string as a `str`.
50+
pub fn as_str(self) -> &'a str {
51+
self.0
52+
}
53+
54+
/// Get the algorithm identifier for this MCF hash.
55+
pub fn id(self) -> &'a str {
56+
Fields::new(self.as_str())
57+
.next()
58+
.expect(INVARIANT_MSG)
59+
.as_str()
60+
}
61+
62+
/// Get an iterator over the parts of the password hash as delimited by `$`, excluding the
63+
/// initial identifier.
64+
pub fn fields(self) -> Fields<'a> {
65+
let mut fields = Fields::new(self.as_str());
66+
67+
// Remove the leading identifier
68+
let id = fields.next().expect(INVARIANT_MSG);
69+
debug_assert_eq!(self.id(), id.as_str());
70+
71+
fields
72+
}
73+
}
74+
3775
/// Modular Crypt Format (MCF) serialized password hash.
3876
///
3977
/// Password hashes in this format take the form `${id}$...`, where `{id}` is a short numeric or
@@ -49,6 +87,7 @@ const INVARIANT_MSG: &str = "should be ensured valid by constructor";
4987
/// $6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0
5088
/// ```
5189
#[cfg(feature = "alloc")]
90+
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5291
pub struct McfHash(String);
5392

5493
#[cfg(feature = "alloc")]
@@ -82,25 +121,20 @@ impl McfHash {
82121
&self.0
83122
}
84123

124+
/// Get an [`McfHashRef`] which corresponds to this owned [`McfHash`].
125+
pub fn as_mcf_hash_ref(&self) -> McfHashRef {
126+
McfHashRef(self.as_str())
127+
}
128+
85129
/// Get the algorithm identifier for this MCF hash.
86130
pub fn id(&self) -> &str {
87-
Fields::new(self.as_str())
88-
.expect(INVARIANT_MSG)
89-
.next()
90-
.expect(INVARIANT_MSG)
91-
.as_str()
131+
self.as_mcf_hash_ref().id()
92132
}
93133

94134
/// Get an iterator over the parts of the password hash as delimited by `$`, excluding the
95135
/// initial identifier.
96136
pub fn fields(&self) -> Fields {
97-
let mut fields = Fields::new(self.as_str()).expect(INVARIANT_MSG);
98-
99-
// Remove the leading identifier
100-
let id = fields.next().expect(INVARIANT_MSG);
101-
debug_assert_eq!(self.id(), id.as_str());
102-
103-
fields
137+
self.as_mcf_hash_ref().fields()
104138
}
105139

106140
/// Push an additional field onto the password hash string.
@@ -116,6 +150,12 @@ impl McfHash {
116150
}
117151
}
118152

153+
impl<'a> AsRef<str> for McfHashRef<'a> {
154+
fn as_ref(&self) -> &str {
155+
self.as_str()
156+
}
157+
}
158+
119159
#[cfg(feature = "alloc")]
120160
impl AsRef<str> for McfHash {
121161
fn as_ref(&self) -> &str {
@@ -140,15 +180,19 @@ impl str::FromStr for McfHash {
140180
}
141181

142182
/// Perform validations that the given string is well-formed MCF.
143-
#[cfg(feature = "alloc")]
144183
fn validate(s: &str) -> Result<()> {
184+
// Require leading `$`
185+
if !s.starts_with(fields::DELIMITER) {
186+
return Err(Error {});
187+
}
188+
145189
// Disallow trailing `$`
146190
if s.ends_with(fields::DELIMITER) {
147191
return Err(Error {});
148192
}
149193

150194
// Validates the hash begins with a leading `$`
151-
let mut fields = Fields::new(s)?;
195+
let mut fields = Fields::new(s);
152196

153197
// Validate characters in the identifier field
154198
let id = fields.next().ok_or(Error {})?;
@@ -166,7 +210,6 @@ fn validate(s: &str) -> Result<()> {
166210
///
167211
/// Allowed characters match the regex: `[a-z0-9\-]`, where the first and last characters do NOT
168212
/// contain a `-`.
169-
#[cfg(feature = "alloc")]
170213
fn validate_id(id: &str) -> Result<()> {
171214
let first = id.chars().next().ok_or(Error {})?;
172215
let last = id.chars().last().ok_or(Error {})?;

0 commit comments

Comments
 (0)