cyclonedx_bom/models/
hash.rs

1/*
2 * This file is part of CycloneDX Rust Cargo.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * SPDX-License-Identifier: Apache-2.0
17 */
18
19use once_cell::sync::Lazy;
20use regex::Regex;
21
22use crate::validation::{Validate, ValidationContext, ValidationError, ValidationResult};
23
24use super::bom::SpecVersion;
25
26/// Represents the hash of the component
27///
28/// Defined via the [CycloneDX XML schema](https://cyclonedx.org/docs/1.3/xml/#type_hashType)
29#[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/// Represents the algorithm used to create the hash
63///
64/// Defined via the [CycloneDX XML schema](https://cyclonedx.org/docs/1.3/xml/#type_hashAlg)
65#[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/// Defined via the [CycloneDX XML schema](https://cyclonedx.org/docs/1.3/xml/#type_hashValue)
126#[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}