aster_forge_utils/
avatar.rs

1//! Avatar presentation helpers.
2//!
3//! This module keeps the product-neutral parts of Gravatar presentation: the
4//! normalized email hash and the conventional URL shape used by Aster services.
5//! Product crates still own avatar source policy, upload handling, route paths,
6//! cache headers, and which sizes they expose.
7
8use md5::{Digest, Md5};
9
10/// Returns the lowercase MD5 hash used by Gravatar for `email`.
11#[must_use]
12pub fn gravatar_hash(email: &str) -> String {
13    let normalized = email.trim().to_lowercase();
14    let mut hasher = Md5::new();
15    hasher.update(normalized.as_bytes());
16    hex_lower(&hasher.finalize())
17}
18
19/// Builds a Gravatar URL with Aster's default public query parameters.
20#[must_use]
21pub fn gravatar_url(email: &str, size: u32, base_url: &str) -> String {
22    let hash = gravatar_hash(email);
23    let base = base_url.trim_end_matches('/');
24    format!("{base}/{hash}?d=identicon&s={size}&r=g")
25}
26
27fn hex_lower(bytes: &[u8]) -> String {
28    const HEX: &[u8; 16] = b"0123456789abcdef";
29    let mut output = String::with_capacity(bytes.len() * 2);
30    for byte in bytes {
31        output.push(char::from(HEX[usize::from(byte >> 4)]));
32        output.push(char::from(HEX[usize::from(byte & 0x0f)]));
33    }
34    output
35}
36
37#[cfg(test)]
38mod tests {
39    use super::{gravatar_hash, gravatar_url};
40
41    #[test]
42    fn gravatar_hash_trims_and_lowercases_email() {
43        assert_eq!(
44            gravatar_hash("  MyEmailAddress@example.com "),
45            "0bc83cb571cd1c50ba6f3e8a78ef1346"
46        );
47    }
48
49    #[test]
50    fn gravatar_url_uses_default_query_parameters_and_trims_base_slashes() {
51        assert_eq!(
52            gravatar_url("user@example.com", 512, "https://www.gravatar.com/avatar"),
53            "https://www.gravatar.com/avatar/b58996c504c5638798eb6b511e6f49af?d=identicon&s=512&r=g"
54        );
55        assert_eq!(
56            gravatar_url("user@example.com", 1024, "https://mirror.example/avatar/"),
57            "https://mirror.example/avatar/b58996c504c5638798eb6b511e6f49af?d=identicon&s=1024&r=g"
58        );
59    }
60}