winbrew_models\shared/
hash.rs

1//! Hash algorithm metadata used by catalog, install, and inventory code.
2//!
3//! The hash model is small on purpose: it carries the algorithm identity, the
4//! display name used in user-facing messages, the expected hex length, and a
5//! flag for legacy algorithms that should be treated with extra caution.
6
7use core::str::FromStr;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11use super::error::ModelError;
12
13/// Checksum algorithms that Winbrew recognizes in persisted model data.
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum HashAlgorithm {
17    Md5,
18    Sha1,
19    #[default]
20    Sha256,
21    Sha512,
22}
23
24impl HashAlgorithm {
25    /// Return the lowercase storage form used in snapshots and raw payloads.
26    pub fn as_str(self) -> &'static str {
27        match self {
28            Self::Md5 => "md5",
29            Self::Sha1 => "sha1",
30            Self::Sha256 => "sha256",
31            Self::Sha512 => "sha512",
32        }
33    }
34
35    /// Return the canonical display name used in diagnostics and CLI output.
36    pub fn display_name(self) -> &'static str {
37        match self {
38            Self::Md5 => "MD5",
39            Self::Sha1 => "SHA1",
40            Self::Sha256 => "SHA256",
41            Self::Sha512 => "SHA512",
42        }
43    }
44
45    /// Return the expected lowercase hex length for the algorithm.
46    pub fn expected_len(self) -> usize {
47        match self {
48            Self::Md5 => 32,
49            Self::Sha1 => 40,
50            Self::Sha256 => 64,
51            Self::Sha512 => 128,
52        }
53    }
54
55    /// Return `true` when the algorithm should be treated as legacy.
56    pub fn is_legacy(self) -> bool {
57        matches!(self, Self::Md5 | Self::Sha1)
58    }
59
60    /// Detect the checksum algorithm encoded in a hash string.
61    pub fn detect(value: &str) -> Option<Self> {
62        let trimmed = value.trim();
63        if trimmed.is_empty() {
64            return None;
65        }
66
67        let lower = trimmed.to_ascii_lowercase();
68
69        for (prefix, algorithm) in [
70            ("sha512:", Self::Sha512),
71            ("sha256:", Self::Sha256),
72            ("sha1:", Self::Sha1),
73            ("md5:", Self::Md5),
74        ] {
75            if lower.starts_with(prefix) {
76                return Some(algorithm);
77            }
78        }
79
80        let candidate = lower
81            .strip_prefix("sha512:")
82            .or_else(|| lower.strip_prefix("sha256:"))
83            .or_else(|| lower.strip_prefix("sha1:"))
84            .or_else(|| lower.strip_prefix("md5:"))
85            .unwrap_or(lower.as_str());
86
87        [Self::Sha512, Self::Sha256, Self::Sha1, Self::Md5]
88            .into_iter()
89            .find(|algorithm| candidate.len() == algorithm.expected_len())
90    }
91}
92
93impl fmt::Display for HashAlgorithm {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.write_str(self.display_name())
96    }
97}
98
99impl FromStr for HashAlgorithm {
100    type Err = ModelError;
101
102    fn from_str(value: &str) -> Result<Self, Self::Err> {
103        match value.trim().to_ascii_lowercase().as_str() {
104            "md5" => Ok(Self::Md5),
105            "sha1" => Ok(Self::Sha1),
106            "sha256" => Ok(Self::Sha256),
107            "sha512" => Ok(Self::Sha512),
108            other => Err(ModelError::invalid_enum_value("hash.algorithm", other)),
109        }
110    }
111}