1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum HashAlgorithm {
13 Sha256,
15 Sha384,
17 Sha512,
19}
20
21impl HashAlgorithm {
22 fn as_str(self) -> &'static str {
23 match self {
24 HashAlgorithm::Sha256 => "sha256",
25 HashAlgorithm::Sha384 => "sha384",
26 HashAlgorithm::Sha512 => "sha512",
27 }
28 }
29
30 const ALL: [HashAlgorithm; 3] = [
31 HashAlgorithm::Sha256,
32 HashAlgorithm::Sha384,
33 HashAlgorithm::Sha512,
34 ];
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct HashExpression {
41 pub algorithm: HashAlgorithm,
43 pub value: String,
45}
46
47pub fn parse_hash_expression(input: &str) -> Option<HashExpression> {
52 for algorithm in HashAlgorithm::ALL {
53 if let Some(value) = input
54 .strip_prefix(algorithm.as_str())
55 .and_then(|rest| rest.strip_prefix('-'))
56 {
57 return if is_valid_base64_value(value) {
58 Some(HashExpression {
59 algorithm,
60 value: value.to_string(),
61 })
62 } else {
63 None
64 };
65 }
66 }
67 None
68}
69
70pub(crate) fn is_valid_base64_value(s: &str) -> bool {
73 let bytes = s.as_bytes();
74 let body_len = bytes
75 .iter()
76 .take_while(|&&b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'-' | b'_'))
77 .count();
78 if body_len == 0 {
79 return false;
80 }
81 let padding = &bytes[body_len..];
82 padding.len() <= 2 && padding.iter().all(|&b| b == b'=')
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn parses_each_algorithm() {
91 assert_eq!(
92 parse_hash_expression("sha256-abc123"),
93 Some(HashExpression {
94 algorithm: HashAlgorithm::Sha256,
95 value: "abc123".to_string(),
96 })
97 );
98 assert_eq!(
99 parse_hash_expression("sha384-abc123").map(|h| h.algorithm),
100 Some(HashAlgorithm::Sha384)
101 );
102 assert_eq!(
103 parse_hash_expression("sha512-abc123").map(|h| h.algorithm),
104 Some(HashAlgorithm::Sha512)
105 );
106 }
107
108 #[test]
109 fn accepts_base64_with_padding_and_url_safe_chars() {
110 assert!(parse_hash_expression("sha256-abc+/12==").is_some());
111 assert!(parse_hash_expression("sha256-abc-_12").is_some());
112 }
113
114 #[test]
115 fn rejects_unknown_algorithm() {
116 assert_eq!(parse_hash_expression("sha1-abc123"), None);
117 assert_eq!(parse_hash_expression("md5-abc123"), None);
118 }
119
120 #[test]
121 fn rejects_empty_or_invalid_base64_value() {
122 assert_eq!(parse_hash_expression("sha256-"), None);
123 assert_eq!(parse_hash_expression("sha256-abc def"), None);
124 assert_eq!(parse_hash_expression("sha256-abc==="), None); }
126}