Skip to main content

detcore/
digest.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use std::fmt;
10use std::fs;
11use std::io;
12use std::ops;
13use std::path::Path;
14use std::str::FromStr;
15
16use hex::FromHex;
17use hex::FromHexError;
18use serde::de;
19use serde::de::Deserialize;
20use serde::de::Deserializer;
21use serde::de::Visitor;
22use serde::ser::Serialize;
23use serde::ser::Serializer;
24use sha2::Digest as _;
25use sha2::Sha256;
26
27const DIGEST_LENGTH: usize = 32;
28
29/// A SHA-256 content digest.
30#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default)]
31pub struct Digest([u8; DIGEST_LENGTH]);
32
33impl Digest {
34    /// Computes the digest from a byte slice.
35    pub fn new(data: &[u8]) -> Self {
36        Digest(Sha256::digest(data).into())
37    }
38
39    /// Computes the digest from a reader.
40    pub fn digest_reader<R: io::Read>(mut reader: R) -> io::Result<Self> {
41        let mut hasher = Sha256::default();
42        let mut buf = [0u8; 16384];
43
44        loop {
45            let n = reader.read(&mut buf)?;
46            if n == 0 {
47                break;
48            }
49            hasher.update(&buf[..n]);
50        }
51
52        Ok(Digest(hasher.finalize().into()))
53    }
54
55    /// Computes the digest by reading the file at the given path.
56    pub fn digest_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
57        Self::digest_reader(fs::File::open(path)?)
58    }
59}
60
61/// A SHA-256 digest computed piece by piece, for content too large to hold at
62/// once.
63#[derive(Default)]
64pub struct DigestBuilder(Sha256);
65
66impl DigestBuilder {
67    /// Adds `data` to the digest.
68    pub fn update(&mut self, data: &[u8]) {
69        self.0.update(data);
70    }
71
72    /// The digest of everything added.
73    pub fn finish(self) -> Digest {
74        Digest(self.0.finalize().into())
75    }
76}
77
78impl fmt::LowerHex for Digest {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        for byte in &self.0 {
81            write!(f, "{byte:02x}")?;
82        }
83        Ok(())
84    }
85}
86
87impl fmt::UpperHex for Digest {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        for byte in &self.0 {
90            write!(f, "{byte:02X}")?;
91        }
92        Ok(())
93    }
94}
95
96impl fmt::Display for Digest {
97    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98        <Self as fmt::LowerHex>::fmt(self, f)
99    }
100}
101
102impl fmt::Debug for Digest {
103    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104        <Self as fmt::Display>::fmt(self, f)
105    }
106}
107
108impl ops::Deref for Digest {
109    type Target = [u8; DIGEST_LENGTH];
110
111    fn deref(&self) -> &Self::Target {
112        &self.0
113    }
114}
115
116impl Serialize for Digest {
117    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118    where
119        S: Serializer,
120    {
121        if serializer.is_human_readable() {
122            serializer.serialize_str(&self.to_string())
123        } else {
124            serializer.serialize_bytes(self.0.as_ref())
125        }
126    }
127}
128
129impl<'de> Deserialize<'de> for Digest {
130    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131    where
132        D: Deserializer<'de>,
133    {
134        struct DigestVisitor;
135
136        impl<'de> Visitor<'de> for DigestVisitor {
137            type Value = Digest;
138
139            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
140                write!(f, "hex string or 32 bytes")
141            }
142
143            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
144            where
145                E: de::Error,
146            {
147                let v = <[u8; DIGEST_LENGTH]>::from_hex(v).map_err(|e| match e {
148                    FromHexError::InvalidHexCharacter { c, .. } => E::invalid_value(
149                        de::Unexpected::Char(c),
150                        &"string with only hexadecimal characters",
151                    ),
152                    FromHexError::InvalidStringLength => {
153                        E::invalid_length(v.len(), &"hex string with a valid length")
154                    }
155                    FromHexError::OddLength => {
156                        E::invalid_length(v.len(), &"hex string with an even length")
157                    }
158                })?;
159                Ok(Digest(v))
160            }
161
162            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
163            where
164                E: de::Error,
165            {
166                if v.len() != DIGEST_LENGTH {
167                    return Err(E::invalid_length(v.len(), &"32 bytes"));
168                }
169                let mut inner = [0; DIGEST_LENGTH];
170                inner.copy_from_slice(v);
171                Ok(Digest(inner))
172            }
173        }
174
175        if deserializer.is_human_readable() {
176            deserializer.deserialize_str(DigestVisitor)
177        } else {
178            deserializer.deserialize_bytes(DigestVisitor)
179        }
180    }
181}
182
183impl FromHex for Digest {
184    type Error = FromHexError;
185
186    fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
187        <[u8; DIGEST_LENGTH]>::from_hex(hex).map(Digest)
188    }
189}
190
191impl AsRef<[u8]> for Digest {
192    fn as_ref(&self) -> &[u8] {
193        &self.0
194    }
195}
196
197impl FromStr for Digest {
198    type Err = FromHexError;
199
200    fn from_str(s: &str) -> Result<Self, Self::Err> {
201        Digest::from_hex(s)
202    }
203}
204
205#[cfg(test)]
206mod test {
207    use super::*;
208
209    #[test]
210    fn test_from_hex() {
211        let s = "b1fbeefc23e6a149a6f7d0c2fb635bfc78f7ddc2da963ea9c6a63eb324260e6d";
212        assert_eq!(Digest::from_str(s).unwrap().to_string(), s);
213    }
214
215    /// Known-answer test pinning what `Digest::new` actually computes.
216    ///
217    /// The root `Cargo.toml` builds `sha2` at `opt-level = 3` even in a debug
218    /// build, because an unoptimised SHA-256 is 34.6x slower and dominates
219    /// `--detlog-io-buffers`. That is a build-configuration change, and the
220    /// hazard with build-configuration changes is that they are assumed not to
221    /// affect behaviour rather than shown not to. This pins the output against
222    /// the published SHA-256 vectors, so it fails if the digest of a fixed
223    /// input ever moves -- whatever the reason, optimisation or otherwise.
224    ///
225    /// Values are the standard NIST/FIPS-180 vectors for "" and "abc", not
226    /// numbers copied back out of a previous run of this code.
227    #[test]
228    fn digest_is_independent_of_optimisation_level() {
229        assert_eq!(
230            Digest::new(b"").to_string(),
231            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
232        );
233        assert_eq!(
234            Digest::new(b"abc").to_string(),
235            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
236        );
237        // A multi-block input, so the compress loop runs more than once: the
238        // optimised and unoptimised builds take different code paths through
239        // it, and a single-block vector would not exercise that.
240        let long = vec![0x61u8; 1_000_000];
241        assert_eq!(
242            Digest::new(&long).to_string(),
243            "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
244        );
245    }
246}