drawbridge_type/digest/
digests.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use super::{Algorithm, Error, Reader, Verifier, Writer};
4
5use std::collections::btree_map::IntoIter;
6use std::collections::BTreeMap;
7use std::ops::{Deref, DerefMut};
8use std::str::FromStr;
9
10use drawbridge_byte::Bytes;
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "headers")]
14use headers::{Error as HeadErr, Header, HeaderName, HeaderValue};
15
16/// A set of hashes for the same contents
17#[derive(Clone, Default, Debug, Serialize, Deserialize)]
18pub struct ContentDigest<H = Box<[u8]>>(BTreeMap<Algorithm, Bytes<H>>)
19where
20    H: AsRef<[u8]> + From<Vec<u8>>;
21
22impl<H> ContentDigest<H>
23where
24    H: AsRef<[u8]> + From<Vec<u8>>,
25{
26    /// Creates a reader instance
27    pub fn reader<T>(&self, reader: T) -> Reader<T> {
28        Reader::new(reader, self.iter().map(|x| *x.0))
29    }
30
31    /// Creates a writer instance
32    pub fn writer<T>(&self, writer: T) -> Writer<T> {
33        Writer::new(writer, self.iter().map(|x| *x.0))
34    }
35
36    /// Creates a verifier instance
37    pub fn verifier<T>(self, reader: T) -> Verifier<T, H> {
38        Verifier::new(self.reader(reader), self)
39    }
40}
41
42impl<H> From<BTreeMap<Algorithm, Bytes<H>>> for ContentDigest<H>
43where
44    H: AsRef<[u8]> + From<Vec<u8>>,
45{
46    fn from(value: BTreeMap<Algorithm, Bytes<H>>) -> Self {
47        Self(value)
48    }
49}
50
51impl<H> Eq for ContentDigest<H> where H: AsRef<[u8]> + From<Vec<u8>> {}
52impl<T, U> PartialEq<ContentDigest<U>> for ContentDigest<T>
53where
54    T: AsRef<[u8]> + From<Vec<u8>>,
55    U: AsRef<[u8]> + From<Vec<u8>>,
56{
57    fn eq(&self, other: &ContentDigest<U>) -> bool {
58        if self.len() != other.len() {
59            return false;
60        }
61
62        for (lhs, rhs) in self.0.iter().zip(other.0.iter()) {
63            if lhs.0 != rhs.0 {
64                return false;
65            }
66
67            if lhs.1.as_ref() != rhs.1.as_ref() {
68                return false;
69            }
70        }
71
72        true
73    }
74}
75
76impl<H> Deref for ContentDigest<H>
77where
78    H: AsRef<[u8]> + From<Vec<u8>>,
79{
80    type Target = BTreeMap<Algorithm, Bytes<H>>;
81
82    fn deref(&self) -> &Self::Target {
83        &self.0
84    }
85}
86
87impl<H> DerefMut for ContentDigest<H>
88where
89    H: AsRef<[u8]> + From<Vec<u8>>,
90{
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        &mut self.0
93    }
94}
95
96impl<H> std::fmt::Display for ContentDigest<H>
97where
98    H: AsRef<[u8]> + From<Vec<u8>>,
99{
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        let mut comma = "";
102
103        for (algo, hash) in self.iter() {
104            write!(f, "{comma}{algo}=:{hash}:")?;
105            comma = ",";
106        }
107
108        Ok(())
109    }
110}
111
112impl<H> FromStr for ContentDigest<H>
113where
114    H: AsRef<[u8]> + From<Vec<u8>>,
115{
116    type Err = Error;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        s.split(',')
120            .map(|s| {
121                let (key, val) = s.split_once('=').ok_or(Error::MissingEq)?;
122                let alg = key.parse()?;
123                let b64 = val
124                    .strip_prefix(':')
125                    .and_then(|val| val.strip_suffix(':'))
126                    .ok_or(Error::MissingColons)?
127                    .parse()?;
128                Ok((alg, b64))
129            })
130            .collect::<Result<_, _>>()
131            .map(Self)
132    }
133}
134
135impl<H> IntoIterator for ContentDigest<H>
136where
137    H: AsRef<[u8]> + From<Vec<u8>>,
138{
139    type Item = (Algorithm, Bytes<H>);
140    type IntoIter = IntoIter<Algorithm, Bytes<H>>;
141
142    fn into_iter(self) -> Self::IntoIter {
143        self.0.into_iter()
144    }
145}
146
147#[cfg(feature = "headers")]
148static CONTENT_DIGEST: HeaderName = HeaderName::from_static("content-digest");
149
150#[cfg(feature = "headers")]
151impl<H> Header for ContentDigest<H>
152where
153    H: Default + AsRef<[u8]> + From<Vec<u8>>,
154{
155    fn name() -> &'static HeaderName {
156        &CONTENT_DIGEST
157    }
158
159    #[allow(single_use_lifetimes)]
160    fn decode<'i, I>(values: &mut I) -> Result<Self, HeadErr>
161    where
162        Self: Sized,
163        I: Iterator<Item = &'i HeaderValue>,
164    {
165        let mut all = Self::default();
166
167        for value in values {
168            let digests: ContentDigest<H> = std::str::from_utf8(value.as_bytes())
169                .map_err(|_| HeadErr::invalid())?
170                .parse()
171                .map_err(|_| HeadErr::invalid())?;
172
173            for (algo, hash) in digests {
174                let _ = all.insert(algo, hash);
175            }
176        }
177
178        if all.is_empty() {
179            return Err(HeadErr::invalid());
180        }
181
182        Ok(all)
183    }
184
185    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
186        let value = HeaderValue::from_str(&self.to_string()).unwrap();
187        values.extend([value])
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[async_std::test]
196    async fn isomorphism() {
197        const STR: &str = "sha-224=:CAj2TmDViXn8tnbJbsk4Jw3qQkRa7vzTpOb42w==:,sha-256=:LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=:,sha-384=:mMEf/f3VQGdrGhN8saIrKnA1DJpEFx1rEYDGvly7LuP3nVMsih3Z7y6OCOdSo7q7:,sha-512=:9/u6bgY2+JDlb7vzKD5STG+jIErimDgtYkdB0NxmODJuKCxBvl5CVNiCB3LFUYosWowMf37aGVlKfrU5RT4e1w==:";
198        assert_eq!(STR.parse::<ContentDigest>().unwrap().to_string(), STR);
199    }
200}