Skip to main content

csp_parse/
hash.rs

1//! The `hash-algorithm "-" base64-value` grammar (CSP3 §2.3.1), kept as
2//! its own module deliberately: this exact sub-grammar, *without* CSP's
3//! surrounding `'...'` quotes, is what Subresource Integrity's unquoted
4//! `integrity=""` hash tokens (e.g. `sha256-...`) also use. `csp-parse`'s
5//! `hash-source` (the quoted `'sha256-...'` form used inside a
6//! `source-list`) is built on top of this module -- see
7//! `plan/DECISIONS.md`, 2026-08-21 and 2026-08-22 entries.
8
9/// One of the three hash algorithms CSP3 recognizes (`hash-algorithm`).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum HashAlgorithm {
13    /// `sha256`.
14    Sha256,
15    /// `sha384`.
16    Sha384,
17    /// `sha512`.
18    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/// A parsed `hash-algorithm "-" base64-value` pair.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct HashExpression {
41    /// The hash algorithm.
42    pub algorithm: HashAlgorithm,
43    /// The base64-value, exactly as it appeared (no decoding performed).
44    pub value: String,
45}
46
47/// Parses `hash-algorithm "-" base64-value` (CSP3 §2.3.1) from `input`,
48/// *without* CSP's surrounding `'...'` quotes -- callers parsing a
49/// `hash-source` token strip the quotes first and pass the inner string
50/// here; callers parsing an SRI `integrity=""` hash token pass it as-is.
51pub 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
70/// Whether `s` matches `base64-value = 1*( ALPHA / DIGIT / "+" / "/" /
71/// "-" / "_" ) *2( "=" )` (CSP3 §2.3.1).
72pub(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); // 3x '=' > *2
125    }
126}