1use alloc::{format, string::String};
4use base64::{Engine as _, engine::general_purpose::STANDARD};
5use sha2::{Digest as _, Sha256};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct ContentDigest([u8; 32]);
14
15impl ContentDigest {
16 pub fn new(content: &[u8]) -> Self {
18 Self(Sha256::digest(content).into())
19 }
20
21 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
23 Self(bytes)
24 }
25
26 pub const fn as_bytes(&self) -> &[u8; 32] {
28 &self.0
29 }
30
31 pub fn to_field_value(self) -> String {
33 format!("sha-256=:{}:", STANDARD.encode(self.0))
34 }
35
36 #[cfg(feature = "integrity")]
41 pub fn parse(value: &str) -> Result<Self, crate::Error> {
42 Self::find(value)?.ok_or(crate::Error::ContentDigest)
43 }
44
45 #[cfg(feature = "integrity")]
46 fn find(value: &str) -> Result<Option<Self>, crate::Error> {
47 let dictionary = crate::fields::dictionary(value)?;
48 for entry in dictionary.values() {
49 if !matches!(entry, sfv::ListEntry::Item(item)
50 if item.bare_item.as_byte_sequence().is_some())
51 {
52 return Err(crate::Error::InvalidField(
53 "digest members must be byte sequences",
54 ));
55 }
56 }
57 let Some(sfv::ListEntry::Item(item)) = dictionary.get("sha-256") else {
58 return Ok(None);
59 };
60 let bytes = item
61 .bare_item
62 .as_byte_sequence()
63 .ok_or(crate::Error::ContentDigest)?;
64 Ok(Some(Self(
65 bytes.try_into().map_err(|_| crate::Error::ContentDigest)?,
66 )))
67 }
68
69 #[cfg(feature = "integrity")]
72 pub fn verify(
73 self,
74 headers: &http::HeaderMap,
75 trailers: &http::HeaderMap,
76 ) -> Result<(), crate::Error> {
77 let mut found = false;
78 for section in [headers, trailers] {
79 if let Some(value) = crate::fields::combined(&[section], "content-digest")?
80 && let Some(expected) = Self::find(&value)?
81 {
82 if expected != self {
83 return Err(crate::Error::ContentDigest);
84 }
85 found = true;
86 }
87 }
88 if found {
89 Ok(())
90 } else {
91 Err(crate::Error::ContentDigest)
92 }
93 }
94}
95
96#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
99pub struct DigestPreference(u8);
100
101impl DigestPreference {
102 pub const fn new(value: u8) -> Option<Self> {
104 if value <= 10 { Some(Self(value)) } else { None }
105 }
106
107 pub const fn value(self) -> u8 {
109 self.0
110 }
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub struct DigestOptions {
120 pub content: bool,
122 pub representation: bool,
124 pub want_content: Option<DigestPreference>,
126 pub want_representation: Option<DigestPreference>,
128}
129
130impl Default for DigestOptions {
131 fn default() -> Self {
132 Self {
133 content: true,
134 representation: false,
135 want_content: None,
136 want_representation: None,
137 }
138 }
139}
140
141impl DigestOptions {
142 #[cfg(feature = "integrity")]
145 pub fn headers(&self, body: &[u8]) -> http::HeaderMap {
146 let mut headers = http::HeaderMap::new();
147 if self.content || self.representation {
148 let value = http::HeaderValue::try_from(ContentDigest::new(body).to_field_value())
149 .expect("digest fields contain only visible ASCII");
150 if self.content {
151 headers.insert("content-digest", value.clone());
152 }
153 if self.representation {
154 headers.insert("repr-digest", value);
155 }
156 }
157 for (name, preference) in [
158 ("want-content-digest", self.want_content),
159 ("want-repr-digest", self.want_representation),
160 ] {
161 if let Some(preference) = preference {
162 headers.insert(
163 name,
164 http::HeaderValue::try_from(format!("sha-256={}", preference.value()))
165 .expect("digest preferences contain only visible ASCII"),
166 );
167 }
168 }
169 headers
170 }
171}