cyclonedx_bom/models/
hash.rs1use once_cell::sync::Lazy;
20use regex::Regex;
21
22use crate::validation::{Validate, ValidationContext, ValidationError, ValidationResult};
23
24use super::bom::SpecVersion;
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30pub struct Hash {
31 pub alg: HashAlgorithm,
32 pub content: HashValue,
33}
34
35impl Validate for Hash {
36 fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
37 ValidationContext::new()
38 .add_field("alg", &self.alg, validate_hash_algorithm)
39 .add_field("content", &self.content, validate_hash_value)
40 .into()
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45pub struct Hashes(pub Vec<Hash>);
46
47impl Validate for Hashes {
48 fn validate_version(&self, version: SpecVersion) -> ValidationResult {
49 ValidationContext::new()
50 .add_list("inner", &self.0, |hash| hash.validate_version(version))
51 .into()
52 }
53}
54
55pub fn validate_hash_algorithm(algorithm: &HashAlgorithm) -> Result<(), ValidationError> {
56 if matches!(algorithm, HashAlgorithm::UnknownHashAlgorithm(_)) {
57 return Err(ValidationError::new("Unknown HashAlgorithm"));
58 }
59 Ok(())
60}
61
62#[allow(non_camel_case_types)]
66#[derive(Clone, Debug, PartialEq, Eq, Hash, strum::Display)]
67#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
68pub enum HashAlgorithm {
69 MD5,
70 #[strum(serialize = "SHA-1")]
71 SHA1,
72 SHA_256,
73 SHA_384,
74 SHA_512,
75 SHA3_256,
76 SHA3_384,
77 SHA3_512,
78 #[strum(serialize = "BLAKE2b-256")]
79 BLAKE2b_256,
80 #[strum(serialize = "BLAKE2b-384")]
81 BLAKE2b_384,
82 #[strum(serialize = "BLAKE2b-512")]
83 BLAKE2b_512,
84 BLAKE3,
85 #[doc(hidden)]
86 #[strum(default)]
87 UnknownHashAlgorithm(String),
88}
89impl HashAlgorithm {
90 pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
91 match value.as_ref() {
92 "MD5" => Self::MD5,
93 "SHA-1" => Self::SHA1,
94 "SHA-256" => Self::SHA_256,
95 "SHA-384" => Self::SHA_384,
96 "SHA-512" => Self::SHA_512,
97 "SHA3-256" => Self::SHA3_256,
98 "SHA3-384" => Self::SHA3_384,
99 "SHA3-512" => Self::SHA3_512,
100 "BLAKE2b-256" => Self::BLAKE2b_256,
101 "BLAKE2b-384" => Self::BLAKE2b_384,
102 "BLAKE2b-512" => Self::BLAKE2b_512,
103 "BLAKE3" => Self::BLAKE3,
104 unknown => Self::UnknownHashAlgorithm(unknown.to_string()),
105 }
106 }
107}
108
109pub fn validate_hash_value(value: &HashValue) -> Result<(), ValidationError> {
110 static HASH_VALUE_REGEX: Lazy<Regex> = Lazy::new(|| {
111 Regex::new(
112 r"^([a-fA-F0-9]{32})|([a-fA-F0-9]{40})|([a-fA-F0-9]{64})|([a-fA-F0-9]{96})|([a-fA-F0-9]{128})$",
113 ).expect("Failed to compile regex.")
114 });
115
116 if !HASH_VALUE_REGEX.is_match(&value.0) {
117 return Err(ValidationError::new(
118 "HashValue does not match regular expression",
119 ));
120 }
121
122 Ok(())
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, Hash)]
127pub struct HashValue(pub String);
128
129#[cfg(test)]
130mod test {
131 use crate::validation::{self};
132
133 use super::*;
134 use pretty_assertions::assert_eq;
135
136 #[test]
137 fn it_should_pass_validation() {
138 let validation_result = Hashes(vec![Hash {
139 alg: HashAlgorithm::MD5,
140 content: HashValue("a3bf1f3d584747e2569483783ddee45b".to_string()),
141 }])
142 .validate_version(SpecVersion::V1_3);
143
144 assert!(validation_result.passed());
145 }
146
147 #[test]
148 fn it_should_fail_validation() {
149 let validation_result = Hashes(vec![Hash {
150 alg: HashAlgorithm::UnknownHashAlgorithm("unknown algorithm".to_string()),
151 content: HashValue("not a hash".to_string()),
152 }])
153 .validate_version(SpecVersion::V1_3);
154
155 assert_eq!(
156 validation_result,
157 validation::list(
158 "inner",
159 [(
160 0,
161 vec![
162 validation::field("alg", "Unknown HashAlgorithm"),
163 validation::field("content", "HashValue does not match regular expression")
164 ]
165 )]
166 )
167 );
168 }
169}