Skip to main content

microsandbox_image/
digest.rs

1//! OCI content-addressable digest type.
2
3use std::{fmt, str::FromStr};
4
5use crate::error::ImageError;
6
7//--------------------------------------------------------------------------------------------------
8// Types
9//--------------------------------------------------------------------------------------------------
10
11/// OCI content-addressable digest (e.g., `sha256:e3b0c44298fc1c14...`).
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct Digest {
14    /// Hash algorithm (e.g., `sha256`).
15    algorithm: String,
16    /// Hex-encoded hash value.
17    hex: String,
18}
19
20//--------------------------------------------------------------------------------------------------
21// Methods
22//--------------------------------------------------------------------------------------------------
23
24impl Digest {
25    /// Create a new digest from algorithm and hex components.
26    pub fn new(algorithm: impl Into<String>, hex: impl Into<String>) -> Self {
27        Self {
28            algorithm: algorithm.into(),
29            hex: hex.into(),
30        }
31    }
32
33    /// Hash algorithm (e.g., `sha256`).
34    pub fn algorithm(&self) -> &str {
35        &self.algorithm
36    }
37
38    /// Hex-encoded hash value.
39    pub fn hex(&self) -> &str {
40        &self.hex
41    }
42
43    /// Filesystem-safe representation for use in paths.
44    ///
45    /// Replaces `:` with `_` (e.g., `sha256_abc123...`).
46    pub fn to_path_safe(&self) -> String {
47        format!("{}_{}", self.algorithm, self.hex)
48    }
49}
50
51//--------------------------------------------------------------------------------------------------
52// Trait Implementations
53//--------------------------------------------------------------------------------------------------
54
55impl FromStr for Digest {
56    type Err = ImageError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        let (algo, hex) = s.split_once(':').ok_or_else(|| {
60            ImageError::ManifestParse(format!("invalid digest (missing ':'): {s}"))
61        })?;
62
63        if algo.is_empty() || hex.is_empty() {
64            return Err(ImageError::ManifestParse(format!(
65                "invalid digest (empty component): {s}"
66            )));
67        }
68
69        Ok(Self {
70            algorithm: algo.to_string(),
71            hex: hex.to_string(),
72        })
73    }
74}
75
76impl fmt::Display for Digest {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}:{}", self.algorithm, self.hex)
79    }
80}
81
82//--------------------------------------------------------------------------------------------------
83// Tests
84//--------------------------------------------------------------------------------------------------
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_parse_valid_digest() {
92        let d: Digest = "sha256:abc123".parse().unwrap();
93        assert_eq!(d.algorithm(), "sha256");
94        assert_eq!(d.hex(), "abc123");
95    }
96
97    #[test]
98    fn test_display() {
99        let d = Digest::new("sha256", "abc123");
100        assert_eq!(d.to_string(), "sha256:abc123");
101    }
102
103    #[test]
104    fn test_path_safe() {
105        let d = Digest::new("sha256", "abc123");
106        assert_eq!(d.to_path_safe(), "sha256_abc123");
107    }
108
109    #[test]
110    fn test_parse_missing_colon() {
111        assert!("sha256abc123".parse::<Digest>().is_err());
112    }
113
114    #[test]
115    fn test_parse_empty_components() {
116        assert!(":abc123".parse::<Digest>().is_err());
117        assert!("sha256:".parse::<Digest>().is_err());
118    }
119}