nix_compat/nixhash/
mod.rs

1use crate::nixbase32;
2use bstr::ByteSlice;
3use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER};
4use std::cmp::Ordering;
5use std::fmt::Display;
6use thiserror;
7
8mod algos;
9mod ca_hash;
10#[cfg(feature = "serde")]
11pub mod serde;
12
13pub use algos::HashAlgo;
14pub use ca_hash::CAHash;
15pub use ca_hash::HashMode as CAHashMode;
16
17/// NixHash represents hashes known by Nix (md5/sha1/sha256/sha512).
18///
19/// Internally, these are represented as an enum of 4 kinds (the latter being
20/// boxed for size reasons, as we rarely use sha512, having a pointer there
21/// is fine).
22///
23/// There's [Self::algo] and [Self::digest_as_bytes] accessors,
24/// as well as a [Self::from_algo_and_digest] constructor.
25///
26/// A few methods to parse (`from_$format_$encoding`) and emit
27/// (`to_$format_$encoding`) various formats and encodings Nix uses.
28///
29/// # Formats
30/// The following formats exist:
31///
32/// ## Nix Format
33/// Lowercase algo, followed by a colon, then the digest.
34///
35/// ## SRI Format
36/// Uses the lowercase algo, followed by a `-`, then the digest (base64-encoded).
37/// This is also used in the Display implementation.
38///
39/// Contrary to the SRI spec, Nix doesn't have an understanding of passing
40/// multiple hashes (with different algos) in SRI hashes.
41/// It instead simply cuts everything off after the expected length for the
42/// specified algo, and tries to parse the rest in permissive base64 (allowing
43/// missing padding).
44///
45/// ## Digest only
46/// It's possible to not specify the algo at all. In that case, the expected
47/// NixHash algo MUST be provided externally.
48///
49/// # Encodings
50/// For "Nix" and "Digest only" formats, the following encodings are supported:
51///
52/// - lowerhex,
53/// - nixbase32,
54/// - base64 (StdEncoding)
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub enum NixHash {
57    Md5([u8; 16]),
58    Sha1([u8; 20]),
59    Sha256([u8; 32]),
60    Sha512(Box<[u8; 64]>),
61}
62
63/// Same order as sorting the corresponding nixbase32 strings.
64///
65/// This order is used in the ATerm serialization of a derivation
66/// and thus affects the calculated output hash.
67impl Ord for NixHash {
68    fn cmp(&self, other: &NixHash) -> Ordering {
69        self.digest_as_bytes().cmp(other.digest_as_bytes())
70    }
71}
72
73// See Ord for reason to implement this manually.
74impl PartialOrd for NixHash {
75    fn partial_cmp(&self, other: &NixHash) -> Option<Ordering> {
76        Some(self.cmp(other))
77    }
78}
79
80// This provides a Display impl, which happens to be SRI right now.
81// If you explicitly care about the format, use [NixHash::to_sri_string]
82// or [NixHash::write_sri_str].
83impl Display for NixHash {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
85        self.write_sri_str(f)
86    }
87}
88
89impl NixHash {
90    /// returns the algo as [HashAlgo].
91    pub fn algo(&self) -> HashAlgo {
92        match self {
93            NixHash::Md5(_) => HashAlgo::Md5,
94            NixHash::Sha1(_) => HashAlgo::Sha1,
95            NixHash::Sha256(_) => HashAlgo::Sha256,
96            NixHash::Sha512(_) => HashAlgo::Sha512,
97        }
98    }
99
100    /// returns the digest as variable-length byte slice.
101    pub fn digest_as_bytes(&self) -> &[u8] {
102        match self {
103            NixHash::Md5(digest) => digest,
104            NixHash::Sha1(digest) => digest,
105            NixHash::Sha256(digest) => digest,
106            NixHash::Sha512(digest) => digest.as_ref(),
107        }
108    }
109
110    /// Constructs a new [NixHash] by specifying [HashAlgo] and digest.
111    /// It can fail if the passed digest length doesn't match what's expected for
112    /// the passed algo.
113    pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> Result<NixHash, Error> {
114        if digest.len() != algo.digest_length() {
115            return Err(Error::InvalidDigestLength(algo));
116        }
117
118        Ok(match algo {
119            HashAlgo::Md5 => NixHash::Md5(digest.try_into().unwrap()),
120            HashAlgo::Sha1 => NixHash::Sha1(digest.try_into().unwrap()),
121            HashAlgo::Sha256 => NixHash::Sha256(digest.try_into().unwrap()),
122            HashAlgo::Sha512 => NixHash::Sha512(Box::new(digest.try_into().unwrap())),
123        })
124    }
125
126    /// Constructs a new [NixHash] from the Nix default hash format,
127    /// the inverse of [Self::to_nix_nixbase32].
128    pub fn from_nix_nixbase32(s: &str) -> Option<Self> {
129        let (tag, digest) = s.split_once(':')?;
130
131        (match tag {
132            "md5" => nixbase32::decode_fixed(digest).map(NixHash::Md5),
133            "sha1" => nixbase32::decode_fixed(digest).map(NixHash::Sha1),
134            "sha256" => nixbase32::decode_fixed(digest).map(NixHash::Sha256),
135            "sha512" => nixbase32::decode_fixed(digest)
136                .map(Box::new)
137                .map(NixHash::Sha512),
138            _ => return None,
139        })
140        .ok()
141    }
142
143    /// Formats a [NixHash] in the Nix nixbase32 format.
144    pub fn to_nix_nixbase32(&self) -> String {
145        format!(
146            "{}:{}",
147            self.algo(),
148            nixbase32::encode(self.digest_as_bytes())
149        )
150    }
151
152    /// Parses a Nix SRI string to a NixHash.
153    /// (See caveats in [Self] on the deviations from the SRI spec)
154    pub fn from_sri(s: &str) -> Result<NixHash, Error> {
155        // split at the first occurence of "-"
156        let (algo_str, digest_str) = s.split_once('-').ok_or(Error::InvalidSRI)?;
157
158        // try to map the part before that `-` to a supported hash algo:
159        let algo: HashAlgo = algo_str.try_into()?;
160
161        // For the digest string, Nix ignores everything after the expected BASE64
162        // (with padding) length, to account for the fact SRI allows specifying more
163        // than one checksum, so shorten it.
164        let digest_str = {
165            let encoded_max_len = BASE64.encode_len(algo.digest_length());
166            if digest_str.len() > encoded_max_len {
167                &digest_str.as_bytes()[..encoded_max_len]
168            } else {
169                digest_str.as_bytes()
170            }
171        };
172
173        // if the digest string is too small to fit even the BASE64_NOPAD version, bail out.
174        if digest_str.len() < BASE64_NOPAD.encode_len(algo.digest_length()) {
175            return Err(Error::InvalidDigestLength(algo));
176        }
177
178        // trim potential padding, and use a version that does not do trailing bit
179        // checking.
180        let mut spec = BASE64_NOPAD.specification();
181        spec.check_trailing_bits = false;
182        let encoding = spec
183            .encoding()
184            .expect("Snix bug: failed to get the special base64 encoder for Nix SRI hashes");
185
186        let digest = encoding
187            .decode(digest_str.trim_end_with(|c| c == '='))
188            .map_err(Error::InvalidBase64Encoding)?;
189
190        Self::from_algo_and_digest(algo, &digest)
191    }
192
193    /// Writes a [NixHash] in SRI format to a [std::fmt::Write].
194    pub fn write_sri_str(&self, w: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
195        write!(
196            w,
197            "{}-{}",
198            self.algo(),
199            BASE64.encode(self.digest_as_bytes())
200        )
201    }
202
203    /// Formats a [NixHash] to an SRI string.
204    pub fn to_sri_string(&self) -> String {
205        let mut s = String::new();
206        self.write_sri_str(&mut s).unwrap();
207
208        s
209    }
210
211    /// Formats a [NixHash] in the Nix lowerhex format.
212    pub fn to_nix_lowerhex_string(&self) -> String {
213        format!(
214            "{}:{}",
215            self.algo(),
216            HEXLOWER.encode(self.digest_as_bytes())
217        )
218    }
219
220    /// This parses all known output formats for NixHash.
221    /// See [NixHash] for a list.
222    /// An optional algo needs to be provided, which is mandatory to be specified if
223    /// the "digest only" format is used.
224    /// In other cases, consistency of an optionally externally configured algo
225    /// with the one parsed is ensured.
226    pub fn from_str(s: &str, want_algo: Option<HashAlgo>) -> Result<NixHash, Error> {
227        // Check for SRI hashes.
228        if let Ok(parsed_nixhash) = Self::from_sri(s) {
229            // ensure the algo matches with what has been passed externally, if so.
230            if let Some(algo) = want_algo {
231                if algo != parsed_nixhash.algo() {
232                    return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo()));
233                }
234            }
235            return Ok(parsed_nixhash);
236        }
237
238        // Check for $algo:$digest style NixHash.
239        if let Some(parsed_nixhash) = {
240            if let Some(rest) = s.strip_prefix("sha1:") {
241                Some(decode_digest(rest.as_bytes(), HashAlgo::Sha1)?)
242            } else if let Some(rest) = s.strip_prefix("sha256:") {
243                Some(decode_digest(rest.as_bytes(), HashAlgo::Sha256)?)
244            } else if let Some(rest) = s.strip_prefix("sha512:") {
245                Some(decode_digest(rest.as_bytes(), HashAlgo::Sha512)?)
246            } else if let Some(rest) = s.strip_prefix("md5:") {
247                Some(decode_digest(rest.as_bytes(), HashAlgo::Md5)?)
248            } else {
249                None
250            }
251        } {
252            // ensure the algo matches with what has been passed externally, if so.
253            if let Some(algo) = want_algo {
254                if algo != parsed_nixhash.algo() {
255                    return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo()));
256                }
257            }
258
259            return Ok(parsed_nixhash);
260        }
261
262        // We're left with the bare digest case, so there MUST be an externally-passed algo.
263        let algo = want_algo.ok_or_else(|| Error::MissingInlineHashAlgo(s.to_string()))?;
264        decode_digest(s.as_bytes(), algo)
265    }
266}
267
268/// Errors related to NixHash construction.
269#[derive(Debug, Eq, PartialEq, thiserror::Error)]
270pub enum Error {
271    #[error("invalid hash algo")]
272    InvalidAlgo,
273    #[error("invalid SRI string")]
274    InvalidSRI,
275    #[error("invalid digest length for algo {0}")]
276    InvalidDigestLength(HashAlgo),
277    #[error("invalid base16 encoding: {0}")]
278    InvalidBase16Encoding(data_encoding::DecodeError),
279    #[error("invalid base32 encoding: {0}")]
280    InvalidBase32Encoding(data_encoding::DecodeError),
281    #[error("invalid base64 encoding: {0}")]
282    InvalidBase64Encoding(data_encoding::DecodeError),
283    #[error("conflicting hash algo: {0} (hash_algo) vs {1} (inline)")]
284    ConflictingHashAlgos(HashAlgo, HashAlgo),
285    #[error("missing inline hash algo, but no externally-specified algo: {0:?}")]
286    MissingInlineHashAlgo(String),
287}
288
289/// Decode a plain digest depending on the hash algo specified externally.
290/// hexlower, nixbase32 and base64 encodings are supported - the encoding is
291/// inferred from the input length.
292fn decode_digest(s: &[u8], algo: HashAlgo) -> Result<NixHash, Error> {
293    // for the chosen hash algo, calculate the expected (decoded) digest length
294    // (as bytes)
295    let digest = if s.len() == HEXLOWER.encode_len(algo.digest_length()) {
296        HEXLOWER
297            .decode(s.as_ref())
298            .map_err(Error::InvalidBase16Encoding)?
299    } else if s.len() == nixbase32::encode_len(algo.digest_length()) {
300        nixbase32::decode(s).map_err(Error::InvalidBase32Encoding)?
301    } else if s.len() == BASE64.encode_len(algo.digest_length()) {
302        BASE64
303            .decode(s.as_ref())
304            .map_err(Error::InvalidBase64Encoding)?
305    } else {
306        Err(Error::InvalidDigestLength(algo))?
307    };
308
309    Ok(NixHash::from_algo_and_digest(algo, &digest).unwrap())
310}
311
312#[cfg(test)]
313mod tests {
314    use crate::nixhash::{HashAlgo, NixHash};
315    use hex_literal::hex;
316    use rstest::rstest;
317    use std::sync::LazyLock;
318
319    const NIXHASH_SHA1: NixHash = NixHash::Sha1(hex!("6016777997c30ab02413cf5095622cd7924283ac"));
320    const NIXHASH_SHA256: NixHash = NixHash::Sha256(hex!(
321        "a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39"
322    ));
323    static NIXHASH_SHA512: LazyLock<NixHash> = LazyLock::new(|| {
324        NixHash::Sha512(Box::new(hex!(
325            "ab40d0be3541f0774bba7815d13d10b03252e96e95f7dbb4ee99a3b431c21662fd6971a020160e39848aa5f305b9be0f78727b2b0789e39f124d21e92b8f39ef"
326        )))
327    });
328    const NIXHASH_MD5: NixHash = NixHash::Md5(hex!("c4874a8897440b393d862d8fd459073f"));
329
330    /// Test parsing a hash string in various formats, and also when/how the out-of-band algo is needed.
331    #[rstest]
332    // regular SRI hashes. We test some funny encoding edge cases in a separate test.
333    #[case::sri_sha1("sha1-YBZ3eZfDCrAkE89QlWIs15JCg6w=", HashAlgo::Sha1, NIXHASH_SHA1)]
334    #[case::sri_sha256(
335        "sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=",
336        HashAlgo::Sha256,
337        NIXHASH_SHA256
338    )]
339    #[case::sri_sha512(
340        "sha512-q0DQvjVB8HdLungV0T0QsDJS6W6V99u07pmjtDHCFmL9aXGgIBYOOYSKpfMFub4PeHJ7KweJ458STSHpK4857w==",
341        HashAlgo::Sha512,
342        (*NIXHASH_SHA512).clone()
343    )]
344    // lowerhex
345    #[case::lowerhex_sha1(
346        "sha1:6016777997c30ab02413cf5095622cd7924283ac",
347        HashAlgo::Sha1,
348        NIXHASH_SHA1
349    )]
350    #[case::lowerhex_sha256(
351        "sha256:a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39",
352        HashAlgo::Sha256,
353        NIXHASH_SHA256
354    )]
355    #[case::lowerhex_sha512("sha512:ab40d0be3541f0774bba7815d13d10b03252e96e95f7dbb4ee99a3b431c21662fd6971a020160e39848aa5f305b9be0f78727b2b0789e39f124d21e92b8f39ef", HashAlgo::Sha512, (*NIXHASH_SHA512).clone())]
356    #[case::lowerhex_md5("md5:c4874a8897440b393d862d8fd459073f", HashAlgo::Md5, NIXHASH_MD5)]
357    #[case::lowerhex_md5("md5-xIdKiJdECzk9hi2P1FkHPw==", HashAlgo::Md5, NIXHASH_MD5)]
358    // base64
359    #[case::base64_sha1("sha1:YBZ3eZfDCrAkE89QlWIs15JCg6w=", HashAlgo::Sha1, NIXHASH_SHA1)]
360    #[case::base64_sha256(
361        "sha256:pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=",
362        HashAlgo::Sha256,
363        NIXHASH_SHA256
364    )]
365    #[case::base64_sha512("sha512:q0DQvjVB8HdLungV0T0QsDJS6W6V99u07pmjtDHCFmL9aXGgIBYOOYSKpfMFub4PeHJ7KweJ458STSHpK4857w==", HashAlgo::Sha512, (*NIXHASH_SHA512).clone())]
366    #[case::base64_md5("md5:xIdKiJdECzk9hi2P1FkHPw==", HashAlgo::Md5, NIXHASH_MD5)]
367    // nixbase32
368    #[case::nixbase32_sha1("sha1:mj1l54np5ii9al6g2cjb02n3jxwpf5k0", HashAlgo::Sha1, NIXHASH_SHA1)]
369    #[case::nixbase32_sha256(
370        "sha256:0fcxdvyzxr09shcbcxkv7l1b356dqxzp3ja68rhrg4yhbqarrkm5",
371        HashAlgo::Sha256,
372        NIXHASH_SHA256
373    )]
374    #[case::nixbase32_sha512("sha512:3pkk3rbx4hls4lzwf4hfavvf9w0zgmr0prsb2l47471c850f5lzsqhnq8qv98wrxssdpxwmdvlm4cmh20yx25bqp95pgw216nzd0h5b", HashAlgo::Sha512, (*NIXHASH_SHA512).clone())]
375    #[case::nixbase32_md5("md5:1z0xcx93rdhqykj2s4jy44m1y4", HashAlgo::Md5, NIXHASH_MD5)]
376    fn from_str(#[case] s: &str, #[case] algo: HashAlgo, #[case] expected: NixHash) {
377        assert_eq!(
378            expected,
379            NixHash::from_str(s, Some(algo)).expect("must parse"),
380            "should parse"
381        );
382
383        // We expect all s to contain an algo in-band, so expect it to parse without an algo too.
384        assert_eq!(
385            expected,
386            NixHash::from_str(s, None).expect("must parse without algo too"),
387            "should parse"
388        );
389
390        // Whenever we encounter a hash with a `$algo:` prefix, we pop that prefix
391        // and test it parses without it if the algo is passed in externally, but fails if not.
392        // We do this for a subset of inputs here in the testcase, rather than adding 12 new testcases (4 algos x 3 encodings)
393        if let Some(digest_str) = s
394            .strip_prefix("sha1:")
395            .or(s.strip_prefix("sha256:"))
396            .or(s.strip_prefix("sha512:"))
397            .or(s.strip_prefix("sha512:"))
398        {
399            assert_eq!(
400                expected,
401                NixHash::from_str(digest_str, Some(algo))
402                    .expect("must parse digest-only if algo specified")
403            );
404            NixHash::from_str(digest_str, None)
405                .expect_err("must fail parsing digest-only if algo not specified");
406        }
407    }
408
409    // Test parsing a hash specifying another algo than what's passed externally fails.
410    #[test]
411    fn test_want_algo() {
412        NixHash::from_str("sha1-YBZ3eZfDCrAkE89QlWIs15JCg6w=", Some(HashAlgo::Md5))
413            .expect_err("parsing with conflicting want_algo should fail");
414
415        NixHash::from_str("sha1:YBZ3eZfDCrAkE89QlWIs15JCg6w=", Some(HashAlgo::Md5))
416            .expect_err("parsing with conflicting want_algo should fail");
417    }
418
419    /// Test parsing an SRI hash via the [nixhash::from_sri_str] method.
420    #[test]
421    fn from_sri_str() {
422        let nix_hash = NixHash::from_sri("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=")
423            .expect("must succeed");
424
425        assert_eq!(HashAlgo::Sha256, nix_hash.algo());
426        assert_eq!(
427            &hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39"),
428            nix_hash.digest_as_bytes()
429        )
430    }
431
432    /// Test parsing sha512 SRI hash with various paddings, Nix accepts all of them.
433    #[rstest]
434    #[case::no_padding(
435        "sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ"
436    )]
437    #[case::too_little_padding(
438        "sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ="
439    )]
440    #[case::correct_padding(
441        "sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ=="
442    )]
443    #[case::too_much_padding(
444        "sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ==="
445    )]
446    #[case::additional_suffix_ignored(
447        "sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ== cheesecake"
448    )]
449    fn from_sri_str_sha512_paddings(#[case] sri_str: &str) {
450        let nix_hash = NixHash::from_sri(sri_str).expect("must succeed");
451
452        assert_eq!(HashAlgo::Sha512, nix_hash.algo());
453        assert_eq!(
454            &hex!(
455                "ee0f754c1bd8a18428ad14eaa3ead80ff8b96275af5012e7a8384f1f10490da056eec9ae3cc791a7a13a24e16e54df5bccdd109c7d53a14534bbd7360a300b11"
456            ),
457            nix_hash.digest_as_bytes()
458        )
459    }
460
461    /// Ensure we detect truncated base64 digests, where the digest size
462    /// doesn't match what's expected from that hash function.
463    #[test]
464    fn from_sri_str_truncated() {
465        NixHash::from_sri("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank").expect_err("must fail");
466    }
467
468    /// Ensure we fail on SRI hashes that Nix doesn't support.
469    #[test]
470    fn from_sri_str_unsupported() {
471        NixHash::from_sri(
472            "sha384-o4UVSl89mIB0sFUK+3jQbG+C9Zc9dRlV/Xd3KAvXEbhqxu0J5OAdg6b6VHKHwQ7U",
473        )
474        .expect_err("must fail");
475    }
476
477    /// Ensure we reject invalid base64 encoding
478    #[test]
479    fn from_sri_str_invalid_base64() {
480        NixHash::from_sri("sha256-invalid=base64").expect_err("must fail");
481    }
482
483    /// Nix also accepts SRI strings with missing padding, but only in case the
484    /// string is expressed as SRI, so it still needs to have a `sha256-` prefix.
485    ///
486    /// This both seems to work if it is passed with and without specifying the
487    /// hash algo out-of-band (hash = "sha256-…" or sha256 = "sha256-…")
488    ///
489    /// Passing the same broken base64 string, but not as SRI, while passing
490    /// the hash algo out-of-band does not work.
491    #[test]
492    fn sha256_broken_padding() {
493        let broken_base64 = "fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8";
494        // if padded with a trailing '='
495        let expected_digest =
496            hex!("7e022bdd3c851830173f9faaa006a230a0e0fdad4c953e85bff4bf0da036e12f");
497
498        // passing hash algo out of band should succeed
499        let nix_hash = NixHash::from_str(
500            &format!("sha256-{}", &broken_base64),
501            Some(HashAlgo::Sha256),
502        )
503        .expect("must succeed");
504        assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
505
506        // not passing hash algo out of band should succeed
507        let nix_hash =
508            NixHash::from_str(&format!("sha256-{}", &broken_base64), None).expect("must succeed");
509        assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
510
511        // not passing SRI, but hash algo out of band should fail
512        NixHash::from_str(broken_base64, Some(HashAlgo::Sha256)).expect_err("must fail");
513    }
514
515    /// As we decided to pass our hashes by trimming `=` completely,
516    /// we need to take into account hashes with padding requirements which
517    /// contains trailing bits which would be checked by `BASE64_NOPAD` and would
518    /// make the verification crash.
519    ///
520    /// This base64 has a trailing non-zero bit at bit 42.
521    #[test]
522    fn sha256_weird_base64() {
523        let weird_base64 = "syceJMUEknBDCHK8eGs6rUU3IQn+HnQfURfCrDxYPa9=";
524        let expected_digest =
525            hex!("b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf");
526
527        let nix_hash =
528            NixHash::from_str(&format!("sha256-{}", &weird_base64), Some(HashAlgo::Sha256))
529                .expect("must succeed");
530        assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
531
532        // not passing hash algo out of band should succeed
533        let nix_hash =
534            NixHash::from_str(&format!("sha256-{}", &weird_base64), None).expect("must succeed");
535        assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
536
537        // not passing SRI, but hash algo out of band should fail
538        NixHash::from_str(weird_base64, Some(HashAlgo::Sha256)).expect_err("must fail");
539    }
540
541    #[cfg(feature = "serde")]
542    #[test]
543    fn serialize_deserialize() {
544        let nixhash_actual = NixHash::Sha256(hex!(
545            "b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf"
546        ));
547        let nixhash_str_json = "\"sha256-syceJMUEknBDCHK8eGs6rUU3IQn+HnQfURfCrDxYPa8=\"";
548
549        let serialized = serde_json::to_string(&nixhash_actual).expect("can serialize");
550
551        assert_eq!(nixhash_str_json, &serialized);
552
553        let deserialized: NixHash =
554            serde_json::from_str(nixhash_str_json).expect("must deserialize");
555        assert_eq!(&nixhash_actual, &deserialized);
556    }
557}