Skip to main content

nix_compat/nixhash/
digester.rs

1use std::io::Write;
2
3use sha2::Digest;
4
5use crate::nixhash::{HashAlgo, Sha256Digester};
6
7use super::NixHash;
8
9enum Inner {
10    Md5(md5::Md5),
11    Sha1(sha1::Sha1),
12    Sha256(Sha256Digester),
13    Sha512(sha2::Sha512),
14}
15
16/// A digester that takes in bytes and ultimately produces a [`NixHash`].
17///
18/// # Examples
19/// ```
20/// use nix_compat::nixhash::{HashAlgo, NixHashDigester};
21///
22/// let one_shot = HashAlgo::Sha256.digest_bytes("hello, world");
23///
24/// let mut ctx = NixHashDigester::new(HashAlgo::Sha256);
25/// ctx.update("hello");
26/// ctx.update(", ");
27/// ctx.update("world");
28/// let multi_path = ctx.finalize();
29///
30/// assert_eq!(one_shot, multi_path);
31/// ```
32pub struct NixHashDigester(Inner);
33
34impl NixHashDigester {
35    /// Returns a new digester for the specified [`HashAlgo`].
36    pub fn new(algo: HashAlgo) -> Self {
37        match algo {
38            HashAlgo::Md5 => Self(Inner::Md5(md5::Md5::new())),
39            HashAlgo::Sha1 => Self(Inner::Sha1(sha1::Sha1::new())),
40            HashAlgo::Sha256 => Self(Inner::Sha256(Sha256Digester::new())),
41            HashAlgo::Sha512 => Self(Inner::Sha512(sha2::Sha512::new())),
42        }
43    }
44
45    /// Returns the hash algorithm that this digester uses.
46    pub fn algorithm(&self) -> HashAlgo {
47        match self.0 {
48            Inner::Md5(_) => HashAlgo::Md5,
49            Inner::Sha1(_) => HashAlgo::Sha1,
50            Inner::Sha256(_) => HashAlgo::Sha256,
51            Inner::Sha512(_) => HashAlgo::Sha512,
52        }
53    }
54
55    /// Updates the digester with the provided bytes.
56    pub fn update<C: AsRef<[u8]>>(&mut self, data: C) {
57        match &mut self.0 {
58            Inner::Md5(d) => d.update(data),
59            Inner::Sha1(d) => d.update(data),
60            Inner::Sha256(d) => d.update(data),
61            Inner::Sha512(d) => d.update(data),
62        }
63    }
64
65    /// Finalize the digest and return the produced [`NixHash`].
66    pub fn finalize(self) -> NixHash {
67        match self.0 {
68            Inner::Md5(d) => {
69                let digest: [u8; HashAlgo::Md5.digest_length()] = d.finalize().into();
70                NixHash::Md5(digest)
71            }
72            Inner::Sha1(d) => {
73                let digest: [u8; HashAlgo::Sha1.digest_length()] = d.finalize().into();
74                NixHash::Sha1(digest)
75            }
76            Inner::Sha256(d) => d.finalize().into(),
77            Inner::Sha512(d) => {
78                let digest: [u8; HashAlgo::Sha512.digest_length()] = d.finalize().into();
79                NixHash::Sha512(Box::new(digest))
80            }
81        }
82    }
83}
84
85impl Write for NixHashDigester {
86    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
87        self.update(buf);
88        Ok(buf.len())
89    }
90
91    fn flush(&mut self) -> std::io::Result<()> {
92        Ok(())
93    }
94}
95
96impl HashAlgo {
97    /// Shorthand for digesting a byte slice.
98    ///
99    /// # Examples
100    /// ```
101    /// use nix_compat::nixhash::HashAlgo;
102    ///
103    /// let sha256 = HashAlgo::Sha256.digest_bytes("abc");
104    /// assert_eq!(sha256.to_nix_nixbase32(), "sha256:1b8m03r63zqhnjf7l5wnldhh7c134ap5vpj0850ymkq1iyzicy5s");
105    /// ```
106    pub fn digest_bytes<C: AsRef<[u8]>>(&self, content: C) -> NixHash {
107        let mut c = NixHashDigester::new(*self);
108        c.update(content);
109        c.finalize()
110    }
111
112    /// Hashes an agument implementing display with this algorithm, without an intermediate buffer.
113    ///
114    /// Analogous to [`std::fmt::format`].
115    ///
116    /// # Examples
117    /// ```
118    /// use nix_compat::nixhash::HashAlgo;
119    ///
120    /// let sha256 = HashAlgo::Sha256.digest_display(format_args!("{}bc", "a"));
121    /// assert_eq!(sha256.to_nix_nixbase32(), "sha256:1b8m03r63zqhnjf7l5wnldhh7c134ap5vpj0850ymkq1iyzicy5s");
122    /// ```
123    pub fn digest_display<D: std::fmt::Display>(&self, value: D) -> NixHash {
124        let mut c = NixHashDigester::new(*self);
125        write!(c, "{value}").unwrap();
126        c.finalize()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::sync::LazyLock;
133
134    use super::*;
135    use hex_literal::hex;
136
137    struct HashFormats {
138        input: &'static str,
139        algo: HashAlgo,
140        hash: NixHash,
141    }
142
143    /// value taken from: https://tools.ietf.org/html/rfc1321
144    const MD5_EMPTY: HashFormats = HashFormats {
145        input: "",
146        algo: HashAlgo::Md5,
147        hash: NixHash::Md5(hex!("d41d8cd98f00b204e9800998ecf8427e")),
148    };
149
150    /// value taken from: https://tools.ietf.org/html/rfc1321
151    const MD5_ABC: HashFormats = HashFormats {
152        input: "abc",
153        algo: HashAlgo::Md5,
154        hash: NixHash::Md5(hex!("900150983cd24fb0d6963f7d28e17f72")),
155    };
156
157    /// value taken from: https://tools.ietf.org/html/rfc3174
158    const SHA1_ABC: HashFormats = HashFormats {
159        input: "abc",
160        algo: HashAlgo::Sha1,
161        hash: NixHash::Sha1(hex!("a9993e364706816aba3e25717850c26c9cd0d89d")),
162    };
163
164    /// value taken from: https://tools.ietf.org/html/rfc3174
165    const SHA1_LONG: HashFormats = HashFormats {
166        input: "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
167        algo: HashAlgo::Sha1,
168        hash: NixHash::Sha1(hex!("84983e441c3bd26ebaae4aa1f95129e5e54670f1")),
169    };
170
171    /// value taken from: https://tools.ietf.org/html/rfc4634
172    const SHA256_ABC: HashFormats = HashFormats {
173        input: "abc",
174        algo: HashAlgo::Sha256,
175        hash: NixHash::Sha256(hex!(
176            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
177        )),
178    };
179
180    /// value taken from: https://tools.ietf.org/html/rfc4634
181    const SHA256_LONG: HashFormats = HashFormats {
182        input: "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
183        algo: HashAlgo::Sha256,
184        hash: NixHash::Sha256(hex!(
185            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
186        )),
187    };
188
189    /// value taken from: https://tools.ietf.org/html/rfc4634
190    static SHA512_ABC: LazyLock<HashFormats> = LazyLock::new(|| HashFormats {
191        input: "abc",
192        algo: HashAlgo::Sha512,
193        hash: NixHash::Sha512(Box::new(hex!(
194            "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
195        ))),
196    });
197
198    /// value taken from: https://tools.ietf.org/html/rfc4634
199    static SHA512_LONG: LazyLock<HashFormats> = LazyLock::new(|| HashFormats {
200        input: "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
201        algo: HashAlgo::Sha512,
202        hash: NixHash::Sha512(Box::new(hex!(
203            "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909"
204        ))),
205    });
206
207    #[rstest_reuse::template]
208    #[rstest::rstest]
209    #[case::md5_empty(&MD5_EMPTY)]
210    #[case::md5_abc(&MD5_ABC)]
211    #[case::sha1_abc(&SHA1_ABC)]
212    #[case::sha1_long(&SHA1_LONG)]
213    #[case::sha256_abc(&SHA256_ABC)]
214    #[case::sha256_long(&SHA256_LONG)]
215    #[case::sha512_abc(&*SHA512_ABC)]
216    #[case::sha512_long(&*SHA512_LONG)]
217    fn hash_formats(#[case] hash: &HashFormats) {}
218
219    /// Test `HashAlgo::digest_bytes` implementation
220    #[rstest_reuse::apply(hash_formats)]
221    fn digest_bytes(#[case] hash: &HashFormats) {
222        let actual = hash.algo.digest_bytes(hash.input);
223        assert_eq!(hash.hash, actual);
224    }
225
226    /// Test `HashAlgo::digest_display` implementation
227    #[rstest_reuse::apply(hash_formats)]
228    fn digest_display(#[case] hash: &HashFormats) {
229        let actual = hash.algo.digest_display(format_args!("{}", hash.input));
230        assert_eq!(hash.hash, actual);
231    }
232
233    /// Test `NixHashDigester` implementation
234    #[rstest_reuse::apply(hash_formats)]
235    fn digester(#[case] hash: &HashFormats) {
236        let mut ctx = NixHashDigester::new(hash.algo);
237        ctx.update(hash.input);
238        let actual = ctx.finalize();
239        assert_eq!(hash.hash, actual);
240    }
241
242    /// Test `NixHashDigester` `std::io::Write` implementation
243    #[rstest_reuse::apply(hash_formats)]
244    fn digester_write(#[case] hash: &HashFormats) {
245        let mut ctx = NixHashDigester::new(hash.algo);
246        ctx.write_all(hash.input.as_bytes()).unwrap();
247        let actual = ctx.finalize();
248        assert_eq!(hash.hash, actual);
249    }
250}