From f6c66af33dbf0cf669a2661ce0631d999259a3a7 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Tue, 3 Jun 2025 22:47:47 +0300 Subject: [PATCH] refactor(nix-compat/nixhash): move from_ and to_ to NixHash struct It was a bit confusing to construct NixHash, having them as separate functions in the module itself, rather than in the NixHash impl. Also the names were very inconsistent. This renames parsers to `from_$format_$encoding` and format methods to `to_$format_$encoding`. It also adds / moves around a few docstrings, explaining the formats and encodings in the struct docstring itself. from_str is changed to accept Option, not Option<&str>, and the otherwise unused `from_nix_hash_string` is folded into from_str. We also simply use from_sri in from_str, as the error path there doesn't allocate anymore. Similarly, the from_nix_str function was only a helper function used to parse a subset of the formats supported in the NixHash::from_str method. We shouldn't be using it outside of there, all usages (only in tests) have been replaced with NixHash::from_algo_and_digest. Change-Id: I36128839dbef19c58b55d5dc5817e38e37a483cc Reviewed-on: https://cl.snix.dev/c/snix/+/30554 Reviewed-by: Ilan Joselevich Reviewed-by: edef Tested-by: besadii --- snix/glue/src/builtins/derivation.rs | 15 +- snix/glue/src/builtins/fetchers.rs | 22 +- snix/glue/src/builtins/import.rs | 4 +- snix/glue/src/fetchers/mod.rs | 12 +- snix/glue/src/known_paths.rs | 4 +- snix/nix-compat/src/derivation/mod.rs | 2 +- snix/nix-compat/src/derivation/parser.rs | 6 +- snix/nix-compat/src/derivation/tests/mod.rs | 2 +- snix/nix-compat/src/nixhash/algos.rs | 2 +- snix/nix-compat/src/nixhash/ca_hash.rs | 41 +- snix/nix-compat/src/nixhash/mod.rs | 413 ++++++++++---------- snix/nix-compat/src/nixhash/serde.rs | 16 +- snix/nix-compat/src/path_info.rs | 4 +- snix/nix-compat/src/store_path/utils.rs | 2 +- 14 files changed, 280 insertions(+), 265 deletions(-) diff --git a/snix/glue/src/builtins/derivation.rs b/snix/glue/src/builtins/derivation.rs index a5a4811e9..0cdce4fc2 100644 --- a/snix/glue/src/builtins/derivation.rs +++ b/snix/glue/src/builtins/derivation.rs @@ -4,7 +4,7 @@ use crate::known_paths::KnownPaths; use crate::snix_store_io::SnixStoreIO; use bstr::BString; use nix_compat::derivation::{Derivation, Output}; -use nix_compat::nixhash; +use nix_compat::nixhash::{CAHash, HashAlgo, NixHash}; use nix_compat::store_path::{StorePath, StorePathRef}; use snix_eval::builtin_macros::builtins; use snix_eval::generators::{self, GenCo, emit_warning_kind}; @@ -132,9 +132,14 @@ fn handle_fixed_output( None => None, }; - // construct a NixHash. - let nixhash = nixhash::from_str(&hash_str, hash_algo_str.as_deref()) + let hash_algo = hash_algo_str + .map(|s| HashAlgo::try_from(s.as_str())) + .transpose() .map_err(DerivationError::InvalidOutputHash)?; + + // construct a NixHash. + let nixhash = + NixHash::from_str(&hash_str, hash_algo).map_err(DerivationError::InvalidOutputHash)?; let algo = nixhash.algo(); // construct the fixed output. @@ -143,8 +148,8 @@ fn handle_fixed_output( Output { path: None, ca_hash: match hash_mode_str.as_deref() { - None | Some("flat") => Some(nixhash::CAHash::Flat(nixhash)), - Some("recursive") => Some(nixhash::CAHash::Nar(nixhash)), + None | Some("flat") => Some(CAHash::Flat(nixhash)), + Some("recursive") => Some(CAHash::Nar(nixhash)), Some(other) => { return Err(DerivationError::InvalidOutputHashMode(other.to_string()))?; } diff --git a/snix/glue/src/builtins/fetchers.rs b/snix/glue/src/builtins/fetchers.rs index ebb44eee3..eb82f311a 100644 --- a/snix/glue/src/builtins/fetchers.rs +++ b/snix/glue/src/builtins/fetchers.rs @@ -5,7 +5,7 @@ use crate::{ fetchers::{Fetch, url_basename}, snix_store_io::SnixStoreIO, }; -use nix_compat::nixhash; +use nix_compat::nixhash::{HashAlgo, NixHash}; use snix_eval::builtin_macros::builtins; use snix_eval::generators::Gen; use snix_eval::generators::GenCo; @@ -70,15 +70,13 @@ async fn extract_fetch_args( } // parse the sha256 string into a digest. - let sha256 = match sha256_str { - Some(sha256_str) => { - let nixhash = nixhash::from_str(&sha256_str, Some("sha256")) - .map_err(|e| ErrorKind::InvalidHash(e.to_string()))?; - - Some(nixhash.digest_as_bytes().try_into().expect("is sha256")) - } - None => None, - }; + let sha256 = sha256_str + .map(|x| { + NixHash::from_str(&x, Some(HashAlgo::Sha256)) + .map(|x| x.digest_as_bytes().try_into().expect("is sha256")) + .map_err(|e| ErrorKind::InvalidHash(e.to_string())) + }) + .transpose()?; // Parse the URL. let url = Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Rc::new(e)))?; @@ -90,7 +88,7 @@ async fn extract_fetch_args( #[builtins(state = "Rc")] pub(crate) mod fetcher_builtins { use bstr::ByteSlice; - use nix_compat::flakeref; + use nix_compat::{flakeref, nixhash::NixHash}; use std::collections::BTreeMap; use super::*; @@ -154,7 +152,7 @@ pub(crate) mod fetcher_builtins { name, Fetch::URL { url: args.url, - exp_hash: args.sha256.map(nixhash::NixHash::Sha256), + exp_hash: args.sha256.map(NixHash::Sha256), }, ) } diff --git a/snix/glue/src/builtins/import.rs b/snix/glue/src/builtins/import.rs index b9d37c909..fcfdd3bc8 100644 --- a/snix/glue/src/builtins/import.rs +++ b/snix/glue/src/builtins/import.rs @@ -111,7 +111,7 @@ mod import_builtins { use crate::builtins::ImportError; use crate::snix_store_io::SnixStoreIO; use bstr::ByteSlice; - use nix_compat::nixhash::{CAHash, NixHash}; + use nix_compat::nixhash::{CAHash, HashAlgo, NixHash}; use nix_compat::store_path::{StorePath, StorePathRef, build_ca_path}; use sha2::Digest; use snix_castore::blobservice::BlobService; @@ -368,7 +368,7 @@ mod import_builtins { .select("sha256") .map(|h| { h.to_str().and_then(|expected| { - match nix_compat::nixhash::from_str(expected.to_str()?, Some("sha256")) { + match NixHash::from_str(expected.to_str()?, Some(HashAlgo::Sha256)) { Ok(NixHash::Sha256(digest)) => Ok(digest), Ok(_) => unreachable!(), Err(e) => Err(ErrorKind::InvalidHash(e.to_string())), diff --git a/snix/glue/src/fetchers/mod.rs b/snix/glue/src/fetchers/mod.rs index 5fa0d6863..0814a8efd 100644 --- a/snix/glue/src/fetchers/mod.rs +++ b/snix/glue/src/fetchers/mod.rs @@ -603,7 +603,7 @@ mod tests { mod fetch { use super::super::*; use crate::fetchers::Fetch; - use nix_compat::{nixbase32, nixhash}; + use nix_compat::{nixbase32, nixhash::NixHash}; use rstest::rstest; #[rstest] @@ -618,7 +618,7 @@ mod tests { #[case::url_sha256( Fetch::URL{ url: Url::parse("https://raw.githubusercontent.com/aaptel/notmuch-extract-patch/f732a53e12a7c91a06755ebfab2007adc9b3063b/notmuch-extract-patch").unwrap(), - exp_hash: Some(nixhash::from_sri_str("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()), + exp_hash: Some(NixHash::from_sri("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()), }, Some(StorePathRef::from_bytes(b"06qi00hylriyfm0nl827crgjvbax84mz-notmuch-extract-patch").unwrap()), "notmuch-extract-patch" @@ -626,7 +626,7 @@ mod tests { #[case::url_custom_name( Fetch::URL{ url: Url::parse("https://test.example/owo").unwrap(), - exp_hash: Some(nixhash::from_sri_str("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()), + exp_hash: Some(NixHash::from_sri("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()), }, Some(StorePathRef::from_bytes(b"06qi00hylriyfm0nl827crgjvbax84mz-notmuch-extract-patch").unwrap()), "notmuch-extract-patch" @@ -634,7 +634,7 @@ mod tests { #[case::nar_sha256( Fetch::NAR{ url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), - hash: nixhash::from_sri_str("sha256-oj6yfWKbcEerK8D9GdPJtIAOveNcsH1ztGeSARGypRA=").unwrap(), + hash: NixHash::from_sri("sha256-oj6yfWKbcEerK8D9GdPJtIAOveNcsH1ztGeSARGypRA=").unwrap(), }, Some(StorePathRef::from_bytes(b"b40vjphshq4fdgv8s3yrp0bdlafi4920-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()), "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" @@ -642,7 +642,7 @@ mod tests { #[case::nar_sha1( Fetch::NAR{ url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), - hash: nixhash::from_sri_str("sha1-F/fMsgwkXF8fPCg1v9zPZ4yOFIA=").unwrap(), + hash: NixHash::from_sri("sha1-F/fMsgwkXF8fPCg1v9zPZ4yOFIA=").unwrap(), }, Some(StorePathRef::from_bytes(b"8kx7fdkdbzs4fkfb57xq0cbhs20ymq2n-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()), "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" @@ -650,7 +650,7 @@ mod tests { #[case::nar_sha1( Fetch::Executable{ url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), - hash: nixhash::from_sri_str("sha1-NKNeU1csW5YJ4lCeWH3Z/apppNU=").unwrap(), + hash: NixHash::from_sri("sha1-NKNeU1csW5YJ4lCeWH3Z/apppNU=").unwrap(), }, Some(StorePathRef::from_bytes(b"y92hm2xfk1009hrq0ix80j4m5k4j4w21-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()), "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" diff --git a/snix/glue/src/known_paths.rs b/snix/glue/src/known_paths.rs index 3d7c8fee2..5566d9322 100644 --- a/snix/glue/src/known_paths.rs +++ b/snix/glue/src/known_paths.rs @@ -147,7 +147,7 @@ mod tests { use std::sync::LazyLock; use hex_literal::hex; - use nix_compat::{derivation::Derivation, nixbase32, nixhash, store_path::StorePath}; + use nix_compat::{derivation::Derivation, nixbase32, nixhash::NixHash, store_path::StorePath}; use url::Url; use super::KnownPaths; @@ -186,7 +186,7 @@ mod tests { static FETCH_URL: LazyLock = LazyLock::new(|| { Fetch::URL { url: Url::parse("https://raw.githubusercontent.com/aaptel/notmuch-extract-patch/f732a53e12a7c91a06755ebfab2007adc9b3063b/notmuch-extract-patch").unwrap(), - exp_hash: Some(nixhash::from_sri_str("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()) + exp_hash: Some(NixHash::from_sri("sha256-Xa1Jbl2Eq5+L0ww+Ph1osA3Z/Dxe/RkN1/dITQCdXFk=").unwrap()) } }); diff --git a/snix/nix-compat/src/derivation/mod.rs b/snix/nix-compat/src/derivation/mod.rs index acc6c46e4..7956c0084 100644 --- a/snix/nix-compat/src/derivation/mod.rs +++ b/snix/nix-compat/src/derivation/mod.rs @@ -165,7 +165,7 @@ impl Derivation { Sha256::new_with_prefix(format!( "fixed:out:{}{}:{}", ca_kind_prefix(ca_hash), - ca_hash.hash().to_nix_hex_string(), + ca_hash.hash().to_nix_lowerhex_string(), out_output .path .as_ref() diff --git a/snix/nix-compat/src/derivation/parser.rs b/snix/nix-compat/src/derivation/parser.rs index 189ea54cf..d8ce043d2 100644 --- a/snix/nix-compat/src/derivation/parser.rs +++ b/snix/nix-compat/src/derivation/parser.rs @@ -15,7 +15,7 @@ use thiserror; use crate::derivation::parse_error::{into_nomerror, ErrorKind, NomError, NomResult}; use crate::derivation::{write, CAHash, Derivation, Output}; use crate::store_path::{self, StorePath}; -use crate::{aterm, nixhash}; +use crate::{aterm, nixhash, nixhash::NixHash}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -84,11 +84,11 @@ fn from_algo_and_mode_and_digest>( digest: B, ) -> crate::nixhash::NixHashResult { Ok(match algo_and_mode.strip_prefix("r:") { - Some(algo) => nixhash::CAHash::Nar(nixhash::from_algo_and_digest( + Some(algo) => nixhash::CAHash::Nar(NixHash::from_algo_and_digest( algo.try_into()?, digest.as_ref(), )?), - None => nixhash::CAHash::Flat(nixhash::from_algo_and_digest( + None => nixhash::CAHash::Flat(NixHash::from_algo_and_digest( algo_and_mode.try_into()?, digest.as_ref(), )?), diff --git a/snix/nix-compat/src/derivation/tests/mod.rs b/snix/nix-compat/src/derivation/tests/mod.rs index 48d4e8926..17093f8f2 100644 --- a/snix/nix-compat/src/derivation/tests/mod.rs +++ b/snix/nix-compat/src/derivation/tests/mod.rs @@ -326,7 +326,7 @@ fn output_path_construction() { Output { path: None, // will be calculated ca_hash: Some(crate::nixhash::CAHash::Nar( - crate::nixhash::from_algo_and_digest( + crate::nixhash::NixHash::from_algo_and_digest( crate::nixhash::HashAlgo::Sha256, &data_encoding::HEXLOWER .decode( diff --git a/snix/nix-compat/src/nixhash/algos.rs b/snix/nix-compat/src/nixhash/algos.rs index ac8915314..1d98eba0a 100644 --- a/snix/nix-compat/src/nixhash/algos.rs +++ b/snix/nix-compat/src/nixhash/algos.rs @@ -69,7 +69,7 @@ impl TryFrom<&str> for HashAlgo { "sha1" => Ok(Self::Sha1), "sha256" => Ok(Self::Sha256), "sha512" => Ok(Self::Sha512), - _ => Err(Error::InvalidAlgo(algo_str.to_string())), + _ => Err(Error::InvalidAlgo), } } } diff --git a/snix/nix-compat/src/nixhash/ca_hash.rs b/snix/nix-compat/src/nixhash/ca_hash.rs index 1d4d65e4e..38816926b 100644 --- a/snix/nix-compat/src/nixhash/ca_hash.rs +++ b/snix/nix-compat/src/nixhash/ca_hash.rs @@ -85,9 +85,9 @@ impl CAHash { } "fixed" => { if let Some(s) = s.strip_prefix("r:") { - NixHash::from_nix_nixbase32_str(s).map(CAHash::Nar) + NixHash::from_nix_nixbase32(s).map(CAHash::Nar) } else { - NixHash::from_nix_nixbase32_str(s).map(CAHash::Flat) + NixHash::from_nix_nixbase32(s).map(CAHash::Flat) } } _ => None, @@ -218,7 +218,12 @@ impl<'de> Deserialize<'de> for CAHash { #[cfg(test)] mod tests { - use crate::{derivation::CAHash, nixhash}; + use hex_literal::hex; + + use crate::{ + derivation::CAHash, + nixhash::{HashAlgo, NixHash}, + }; #[test] fn serialize_flat() { @@ -227,8 +232,9 @@ mod tests { "hashAlgo": "sha256" }"#; let hash = CAHash::Flat( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"), ) .unwrap(), ); @@ -243,8 +249,9 @@ mod tests { "hashAlgo": "r:sha256" }"#; let hash = CAHash::Nar( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"), ) .unwrap(), ); @@ -264,8 +271,9 @@ mod tests { assert_eq!( hash, CAHash::Flat( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba") ) .unwrap() ) @@ -284,8 +292,9 @@ mod tests { assert_eq!( hash, CAHash::Nar( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba") ) .unwrap() ) @@ -304,8 +313,9 @@ mod tests { assert_eq!( hash, CAHash::Nar( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"), ) .unwrap() ) @@ -324,8 +334,9 @@ mod tests { assert_eq!( hash, CAHash::Nar( - nixhash::from_nix_str( - "sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" + NixHash::from_algo_and_digest( + HashAlgo::Sha256, + &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"), ) .unwrap() ) diff --git a/snix/nix-compat/src/nixhash/mod.rs b/snix/nix-compat/src/nixhash/mod.rs index a3c07f9b4..5da961768 100644 --- a/snix/nix-compat/src/nixhash/mod.rs +++ b/snix/nix-compat/src/nixhash/mod.rs @@ -13,7 +13,44 @@ pub use algos::HashAlgo; pub use ca_hash::CAHash; pub use ca_hash::HashMode as CAHashMode; -/// NixHash represents hashes known by Nix. +/// NixHash represents hashes known by Nix (md5/sha1/sha256/sha512). +/// +/// Internally, these are represented as an enum of 4 kinds (the latter being +/// boxed for size reasons, as we rarely use sha512, having a pointer there +/// is fine). +/// +/// There's [Self::algo] and [Self::digest_as_bytes] accessors, +/// as well as a [Self::from_algo_and_digest] constructor. +/// +/// A few methods to parse (`from_$format_$encoding`) and emit +/// (`to_$format_$encoding`) various formats and encodings Nix uses. +/// +/// # Formats +/// The following formats exist: +/// +/// ## Nix Format +/// Lowercase algo, followed by a colon, then the digest. +/// +/// ## SRI Format +/// Uses the lowercase algo, followed by a `-`, then the digest (base64-encoded). +/// This is also used in the Display implementation. +/// +/// Contrary to the SRI spec, Nix doesn't have an understanding of passing +/// multiple hashes (with different algos) in SRI hashes. +/// It instead simply cuts everything off after the expected length for the +/// specified algo, and tries to parse the rest in permissive base64 (allowing +/// missing padding). +/// +/// ## Digest only +/// It's possible to not specify the algo at all. In that case, the expected +/// NixHash algo MUST be provided externally. +/// +/// # Encodings +/// For "Nix" and "Digest only" formats, the following encodings are supported: +/// +/// - lowerhex, +/// - nixbase32, +/// - base64 (StdEncoding) #[derive(Clone, Debug, Eq, PartialEq)] pub enum NixHash { Md5([u8; 16]), @@ -72,9 +109,25 @@ impl NixHash { } } + /// Constructs a new [NixHash] by specifying [HashAlgo] and digest. + /// It can fail if the passed digest length doesn't match what's expected for + /// the passed algo. + pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult { + if digest.len() != algo.digest_length() { + return Err(Error::InvalidDigestLength(digest.len(), algo)); + } + + Ok(match algo { + HashAlgo::Md5 => NixHash::Md5(digest.try_into().unwrap()), + HashAlgo::Sha1 => NixHash::Sha1(digest.try_into().unwrap()), + HashAlgo::Sha256 => NixHash::Sha256(digest.try_into().unwrap()), + HashAlgo::Sha512 => NixHash::Sha512(Box::new(digest.try_into().unwrap())), + }) + } + /// Constructs a new [NixHash] from the Nix default hash format, - /// the inverse of [Self::to_nix_nixbase32_string]. - pub fn from_nix_nixbase32_str(s: &str) -> Option { + /// the inverse of [Self::to_nix_nixbase32]. + pub fn from_nix_nixbase32(s: &str) -> Option { let (tag, digest) = s.split_once(':')?; (match tag { @@ -89,19 +142,8 @@ impl NixHash { .ok() } - /// Formats a [NixHash] in the Nix default hash format, - /// which is the algo, followed by a colon, then the lower hex encoded digest. - pub fn to_nix_hex_string(&self) -> String { - format!( - "{}:{}", - self.algo(), - HEXLOWER.encode(self.digest_as_bytes()) - ) - } - - /// Formats a [NixHash] in the format that's used inside CAHash, - /// which is the algo, followed by a colon, then the nixbase32-encoded digest. - pub fn to_nix_nixbase32_string(&self) -> String { + /// Formats a [NixHash] in the Nix nixbase32 format. + pub fn to_nix_nixbase32(&self) -> String { format!( "{}:{}", self.algo(), @@ -109,6 +151,47 @@ impl NixHash { ) } + /// Parses a Nix SRI string to a NixHash. + /// (See caveats in [Self] on the deviations from the SRI spec) + pub fn from_sri(s: &str) -> NixHashResult { + // split at the first occurence of "-" + let (algo_str, digest_str) = s.split_once('-').ok_or(Error::InvalidSRI)?; + + // try to map the part before that `-` to a supported hash algo: + let algo: HashAlgo = algo_str.try_into()?; + + // For the digest string, Nix ignores everything after the expected BASE64 + // (with padding) length, to account for the fact SRI allows specifying more + // than one checksum, so shorten it. + let digest_str = { + let encoded_max_len = BASE64.encode_len(algo.digest_length()); + if digest_str.len() > encoded_max_len { + &digest_str.as_bytes()[..encoded_max_len] + } else { + digest_str.as_bytes() + } + }; + + // if the digest string is too small to fit even the BASE64_NOPAD version, bail out. + if digest_str.len() < BASE64_NOPAD.encode_len(algo.digest_length()) { + return Err(Error::InvalidDigestLength(digest_str.len(), algo)); + } + + // trim potential padding, and use a version that does not do trailing bit + // checking. + let mut spec = BASE64_NOPAD.specification(); + spec.check_trailing_bits = false; + let encoding = spec + .encoding() + .expect("Snix bug: failed to get the special base64 encoder for Nix SRI hashes"); + + let digest = encoding + .decode(digest_str.trim_end_with(|c| c == '=')) + .map_err(Error::InvalidBase64Encoding)?; + + Self::from_algo_and_digest(algo, &digest) + } + /// Writes a [NixHash] in SRI format to a [std::fmt::Write]. pub fn write_sri_str(&self, w: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> { write!( @@ -126,31 +209,71 @@ impl NixHash { s } -} -/// Constructs a new [NixHash] by specifying [HashAlgo] and digest. -/// It can fail if the passed digest length doesn't match what's expected for -/// the passed algo. -pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult { - if digest.len() != algo.digest_length() { - return Err(Error::InvalidDigestLength(digest.len(), algo)); + /// Formats a [NixHash] in the Nix lowerhex format. + pub fn to_nix_lowerhex_string(&self) -> String { + format!( + "{}:{}", + self.algo(), + HEXLOWER.encode(self.digest_as_bytes()) + ) } - Ok(match algo { - HashAlgo::Md5 => NixHash::Md5(digest.try_into().unwrap()), - HashAlgo::Sha1 => NixHash::Sha1(digest.try_into().unwrap()), - HashAlgo::Sha256 => NixHash::Sha256(digest.try_into().unwrap()), - HashAlgo::Sha512 => NixHash::Sha512(Box::new(digest.try_into().unwrap())), - }) + /// This parses all known output formats for NixHash. + /// See [NixHash] for a list. + /// An optional algo needs to be provided, which is mandatory to be specified if + /// the "digest only" format is used. + /// In other cases, consistency of an optionally externally configured algo + /// with the one parsed is ensured. + pub fn from_str(s: &str, want_algo: Option) -> NixHashResult { + // Check for SRI hashes. + if let Ok(parsed_nixhash) = Self::from_sri(s) { + // ensure the algo matches with what has been passed externally, if so. + if let Some(algo) = want_algo { + if algo != parsed_nixhash.algo() { + return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo())); + } + } + return Ok(parsed_nixhash); + } + + // Check for $algo:$digest style NixHash. + if let Some(parsed_nixhash) = { + if let Some(rest) = s.strip_prefix("sha1:") { + Some(decode_digest(rest.as_bytes(), HashAlgo::Sha1)?) + } else if let Some(rest) = s.strip_prefix("sha256:") { + Some(decode_digest(rest.as_bytes(), HashAlgo::Sha256)?) + } else if let Some(rest) = s.strip_prefix("sha512:") { + Some(decode_digest(rest.as_bytes(), HashAlgo::Sha512)?) + } else if let Some(rest) = s.strip_prefix("md5:") { + Some(decode_digest(rest.as_bytes(), HashAlgo::Md5)?) + } else { + None + } + } { + // ensure the algo matches with what has been passed externally, if so. + if let Some(algo) = want_algo { + if algo != parsed_nixhash.algo() { + return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo())); + } + } + + return Ok(parsed_nixhash); + } + + // We're left with the bare digest case, so there MUST be an externally-passed algo. + let algo = want_algo.ok_or_else(|| Error::MissingInlineHashAlgo(s.to_string()))?; + decode_digest(s.as_bytes(), algo) + } } /// Errors related to NixHash construction. #[derive(Debug, Eq, PartialEq, thiserror::Error)] pub enum Error { - #[error("invalid hash algo: {0}")] - InvalidAlgo(String), - #[error("invalid SRI string: {0}")] - InvalidSRI(String), + #[error("invalid hash algo")] + InvalidAlgo, + #[error("invalid SRI string")] + InvalidSRI, #[error("invalid encoded digest length '{0}' for algo {1}")] InvalidDigestLength(usize, HashAlgo), #[error("invalid base16 encoding: {0}")] @@ -161,134 +284,10 @@ pub enum Error { InvalidBase64Encoding(data_encoding::DecodeError), #[error("conflicting hash algo: {0} (hash_algo) vs {1} (inline)")] ConflictingHashAlgos(HashAlgo, HashAlgo), - #[error("missing inline hash algo, but no externally-specified algo: {0}")] + #[error("missing inline hash algo, but no externally-specified algo: {0:?}")] MissingInlineHashAlgo(String), } -/// Nix allows specifying hashes in various encodings, and magically just -/// derives the encoding. -/// This function parses strings to a NixHash. -/// -/// Hashes can be: -/// - Nix hash strings -/// - SRI hashes -/// - bare digests -/// -/// Encoding for Nix hash strings or bare digests can be: -/// - base16 (lowerhex), -/// - nixbase32, -/// - base64 (StdEncoding) -/// - sri string -/// -/// The encoding is derived from the length of the string and the hash type. -/// The hash is communicated out-of-band, but might also be in-band (in the -/// case of a nix hash string or SRI), in which it needs to be consistent with the -/// one communicated out-of-band. -pub fn from_str(s: &str, algo_str: Option<&str>) -> NixHashResult { - // if algo_str is some, parse or bail out - let algo: Option = algo_str.map(HashAlgo::try_from).transpose()?; - - // Peek at the beginning of the string to detect SRI hashes. - if s.starts_with("sha1-") - || s.starts_with("sha256-") - || s.starts_with("sha512-") - || s.starts_with("md5-") - { - let parsed_nixhash = from_sri_str(s)?; - - // ensure the algo matches with what has been passed externally, if so. - if let Some(algo) = algo { - if algo != parsed_nixhash.algo() { - return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo())); - } - } - return Ok(parsed_nixhash); - } - - // Peek at the beginning again to see if it's a Nix Hash - if s.starts_with("sha1:") - || s.starts_with("sha256:") - || s.starts_with("sha512:") - || s.starts_with("md5:") - { - let parsed_nixhash = from_nix_str(s)?; - - // ensure the algo matches with what has been passed externally, if so. - if let Some(algo) = algo { - if algo != parsed_nixhash.algo() { - return Err(Error::ConflictingHashAlgos(algo, parsed_nixhash.algo())); - } - } - return Ok(parsed_nixhash); - } - - // Neither of these, assume a bare digest, so there MUST be an externally-passed algo. - let algo = algo.ok_or_else(|| Error::MissingInlineHashAlgo(s.to_string()))?; - decode_digest(s.as_bytes(), algo) -} - -/// Parses a Nix hash string ($algo:$digest) to a NixHash. -pub fn from_nix_str(s: &str) -> NixHashResult { - if let Some(rest) = s.strip_prefix("sha1:") { - decode_digest(rest.as_bytes(), HashAlgo::Sha1) - } else if let Some(rest) = s.strip_prefix("sha256:") { - decode_digest(rest.as_bytes(), HashAlgo::Sha256) - } else if let Some(rest) = s.strip_prefix("sha512:") { - decode_digest(rest.as_bytes(), HashAlgo::Sha512) - } else if let Some(rest) = s.strip_prefix("md5:") { - decode_digest(rest.as_bytes(), HashAlgo::Md5) - } else { - Err(Error::InvalidAlgo(s.to_string())) - } -} - -/// Parses a Nix SRI string to a NixHash. -/// Contrary to the SRI spec, Nix doesn't have an understanding of passing -/// multiple hashes (with different algos) in SRI hashes. -/// It instead simply cuts everything off after the expected length for the -/// specified algo, and tries to parse the rest in permissive base64 (allowing -/// missing padding). -pub fn from_sri_str(s: &str) -> NixHashResult { - // split at the first occurence of "-" - let (algo_str, digest_str) = s - .split_once('-') - .ok_or_else(|| Error::InvalidSRI(s.to_string()))?; - - // try to map the part before that `-` to a supported hash algo: - let algo: HashAlgo = algo_str.try_into()?; - - // For the digest string, Nix ignores everything after the expected BASE64 - // (with padding) length, to account for the fact SRI allows specifying more - // than one checksum, so shorten it. - let digest_str = { - let encoded_max_len = BASE64.encode_len(algo.digest_length()); - if digest_str.len() > encoded_max_len { - &digest_str.as_bytes()[..encoded_max_len] - } else { - digest_str.as_bytes() - } - }; - - // if the digest string is too small to fit even the BASE64_NOPAD version, bail out. - if digest_str.len() < BASE64_NOPAD.encode_len(algo.digest_length()) { - return Err(Error::InvalidDigestLength(digest_str.len(), algo)); - } - - // trim potential padding, and use a version that does not do trailing bit - // checking. - let mut spec = BASE64_NOPAD.specification(); - spec.check_trailing_bits = false; - let encoding = spec - .encoding() - .expect("Snix bug: failed to get the special base64 encoder for Nix SRI hashes"); - - let digest = encoding - .decode(digest_str.trim_end_with(|c| c == '=')) - .map_err(Error::InvalidBase64Encoding)?; - - from_algo_and_digest(algo, &digest) -} - /// Decode a plain digest depending on the hash algo specified externally. /// hexlower, nixbase32 and base64 encodings are supported - the encoding is /// inferred from the input length. @@ -309,14 +308,14 @@ fn decode_digest(s: &[u8], algo: HashAlgo) -> NixHashResult { Err(Error::InvalidDigestLength(s.len(), algo))? }; - Ok(from_algo_and_digest(algo, &digest).unwrap()) + Ok(NixHash::from_algo_and_digest(algo, &digest).unwrap()) } #[cfg(test)] mod tests { use crate::{ nixbase32, - nixhash::{self, HashAlgo, NixHash}, + nixhash::{HashAlgo, NixHash}, }; use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER}; use hex_literal::hex; @@ -345,68 +344,67 @@ mod tests { } // TODO - fn make_nixhash(algo: &HashAlgo, digest_encoded: String) -> String { + fn make_nixhash(algo: HashAlgo, digest_encoded: String) -> String { format!("{}:{}", algo, digest_encoded) } - fn make_sri_string(algo: &HashAlgo, digest_encoded: String) -> String { + fn make_sri_string(algo: HashAlgo, digest_encoded: String) -> String { format!("{}-{}", algo, digest_encoded) } /// Test parsing a hash string in various formats, and also when/how the out-of-band algo is needed. #[rstest] - #[case::sha1(&NixHash::Sha1(DIGEST_SHA1))] - #[case::sha256(&NixHash::Sha256(DIGEST_SHA256))] - #[case::sha512(&NixHash::Sha512(Box::new(DIGEST_SHA512)))] - #[case::md5(&NixHash::Md5(DIGEST_MD5))] - fn from_str(#[case] expected_hash: &NixHash) { - let algo = &expected_hash.algo(); + #[case::sha1(NixHash::Sha1(DIGEST_SHA1))] + #[case::sha256(NixHash::Sha256(DIGEST_SHA256))] + #[case::sha512(NixHash::Sha512(Box::new(DIGEST_SHA512)))] + #[case::md5(NixHash::Md5(DIGEST_MD5))] + fn from_str(#[case] expected_hash: NixHash) { + let algo = expected_hash.algo(); let digest = expected_hash.digest_as_bytes(); // parse SRI { // base64 without out-of-band algo let s = make_sri_string(algo, to_base64(digest)); - let h = nixhash::from_str(&s, None).expect("must succeed"); - assert_eq!(expected_hash, &h); + let h = NixHash::from_str(&s, None).expect("must succeed"); + assert_eq!(expected_hash, h); // base64 with out-of-band-algo let s = make_sri_string(algo, to_base64(digest)); - let h = nixhash::from_str(&s, Some(&expected_hash.algo().to_string())) - .expect("must succeed"); - assert_eq!(expected_hash, &h); + let h = NixHash::from_str(&s, Some(expected_hash.algo())).expect("must succeed"); + assert_eq!(expected_hash, h); // base64_nopad without out-of-band algo let s = make_sri_string(algo, to_base64_nopad(digest)); - let h = nixhash::from_str(&s, None).expect("must succeed"); - assert_eq!(expected_hash, &h); + let h = NixHash::from_str(&s, None).expect("must succeed"); + assert_eq!(expected_hash, h); // base64_nopad with out-of-band-algo let s = make_sri_string(algo, to_base64_nopad(digest)); - let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); - assert_eq!(expected_hash, &h); + let h = NixHash::from_str(&s, Some(algo)).expect("must succeed"); + assert_eq!(expected_hash, h); } // parse plain base16. should succeed with algo out-of-band, but fail without. { let s = to_base16(digest); - nixhash::from_str(&s, None).expect_err("must fail"); - let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); - assert_eq!(expected_hash, &h); + NixHash::from_str(&s, None).expect_err("must fail"); + let h = NixHash::from_str(&s, Some(algo)).expect("must succeed"); + assert_eq!(expected_hash, h); } // parse plain nixbase32. should succeed with algo out-of-band, but fail without. { let s = to_nixbase32(digest); - nixhash::from_str(&s, None).expect_err("must fail"); - let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); - assert_eq!(expected_hash, &h); + NixHash::from_str(&s, None).expect_err("must fail"); + let h = NixHash::from_str(&s, Some(algo)).expect("must succeed"); + assert_eq!(expected_hash, h); } // parse plain base64. should succeed with algo out-of-band, but fail without. { let s = to_base64(digest); - nixhash::from_str(&s, None).expect_err("must fail"); - let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); - assert_eq!(expected_hash, &h); + NixHash::from_str(&s, None).expect_err("must fail"); + let h = NixHash::from_str(&s, Some(algo)).expect("must succeed"); + assert_eq!(expected_hash, h); } // parse Nix hash strings @@ -416,11 +414,11 @@ mod tests { let s = make_nixhash(algo, to_base16(digest)); assert_eq!( expected_hash, - &nixhash::from_str(&s, None).expect("must succeed") + NixHash::from_str(&s, None).expect("must succeed") ); assert_eq!( expected_hash, - &nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed") + NixHash::from_str(&s, Some(algo)).expect("must succeed") ); } // nixbase32. should succeed with both algo out-of-band and in-band. @@ -428,11 +426,11 @@ mod tests { let s = make_nixhash(algo, to_nixbase32(digest)); assert_eq!( expected_hash, - &nixhash::from_str(&s, None).expect("must succeed") + NixHash::from_str(&s, None).expect("must succeed") ); assert_eq!( expected_hash, - &nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed") + NixHash::from_str(&s, Some(algo)).expect("must succeed") ); } // base64. should succeed with both algo out-of-band and in-band. @@ -440,11 +438,11 @@ mod tests { let s = make_nixhash(algo, to_base64(digest)); assert_eq!( expected_hash, - &nixhash::from_str(&s, None).expect("must succeed") + NixHash::from_str(&s, None).expect("must succeed") ); assert_eq!( expected_hash, - &nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed") + NixHash::from_str(&s, Some(algo)).expect("must succeed") ); } } @@ -453,7 +451,7 @@ mod tests { /// Test parsing an SRI hash via the [nixhash::from_sri_str] method. #[test] fn from_sri_str() { - let nix_hash = nixhash::from_sri_str("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=") + let nix_hash = NixHash::from_sri("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=") .expect("must succeed"); assert_eq!(HashAlgo::Sha256, nix_hash.algo()); @@ -471,7 +469,7 @@ mod tests { #[case::too_much_padding("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ===")] #[case::additional_suffix_ignored("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ== cheesecake")] fn from_sri_str_sha512_paddings(#[case] sri_str: &str) { - let nix_hash = nixhash::from_sri_str(sri_str).expect("must succeed"); + let nix_hash = NixHash::from_sri(sri_str).expect("must succeed"); assert_eq!(HashAlgo::Sha512, nix_hash.algo()); assert_eq!( @@ -484,14 +482,13 @@ mod tests { /// doesn't match what's expected from that hash function. #[test] fn from_sri_str_truncated() { - nixhash::from_sri_str("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank") - .expect_err("must fail"); + NixHash::from_sri("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank").expect_err("must fail"); } /// Ensure we fail on SRI hashes that Nix doesn't support. #[test] fn from_sri_str_unsupported() { - nixhash::from_sri_str( + NixHash::from_sri( "sha384-o4UVSl89mIB0sFUK+3jQbG+C9Zc9dRlV/Xd3KAvXEbhqxu0J5OAdg6b6VHKHwQ7U", ) .expect_err("must fail"); @@ -500,7 +497,7 @@ mod tests { /// Ensure we reject invalid base64 encoding #[test] fn from_sri_str_invalid_base64() { - nixhash::from_sri_str("sha256-invalid=base64").expect_err("must fail"); + NixHash::from_sri("sha256-invalid=base64").expect_err("must fail"); } /// Nix also accepts SRI strings with missing padding, but only in case the @@ -519,17 +516,20 @@ mod tests { hex!("7e022bdd3c851830173f9faaa006a230a0e0fdad4c953e85bff4bf0da036e12f"); // passing hash algo out of band should succeed - let nix_hash = nixhash::from_str(&format!("sha256-{}", &broken_base64), Some("sha256")) - .expect("must succeed"); + let nix_hash = NixHash::from_str( + &format!("sha256-{}", &broken_base64), + Some(HashAlgo::Sha256), + ) + .expect("must succeed"); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); // not passing hash algo out of band should succeed let nix_hash = - nixhash::from_str(&format!("sha256-{}", &broken_base64), None).expect("must succeed"); + NixHash::from_str(&format!("sha256-{}", &broken_base64), None).expect("must succeed"); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); // not passing SRI, but hash algo out of band should fail - nixhash::from_str(broken_base64, Some("sha256")).expect_err("must fail"); + NixHash::from_str(broken_base64, Some(HashAlgo::Sha256)).expect_err("must fail"); } /// As we decided to pass our hashes by trimming `=` completely, @@ -544,17 +544,18 @@ mod tests { let expected_digest = hex!("b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf"); - let nix_hash = nixhash::from_str(&format!("sha256-{}", &weird_base64), Some("sha256")) - .expect("must succeed"); + let nix_hash = + NixHash::from_str(&format!("sha256-{}", &weird_base64), Some(HashAlgo::Sha256)) + .expect("must succeed"); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); // not passing hash algo out of band should succeed let nix_hash = - nixhash::from_str(&format!("sha256-{}", &weird_base64), None).expect("must succeed"); + NixHash::from_str(&format!("sha256-{}", &weird_base64), None).expect("must succeed"); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); // not passing SRI, but hash algo out of band should fail - nixhash::from_str(weird_base64, Some("sha256")).expect_err("must fail"); + NixHash::from_str(weird_base64, Some(HashAlgo::Sha256)).expect_err("must fail"); } #[test] diff --git a/snix/nix-compat/src/nixhash/serde.rs b/snix/nix-compat/src/nixhash/serde.rs index 8ea2975e7..08a656fea 100644 --- a/snix/nix-compat/src/nixhash/serde.rs +++ b/snix/nix-compat/src/nixhash/serde.rs @@ -8,7 +8,7 @@ impl<'de> Deserialize<'de> for NixHash { D: serde::Deserializer<'de>, { let str: &'de str = Deserialize::deserialize(deserializer)?; - super::from_str(str, None).map_err(|_| { + NixHash::from_str(str, None).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(str), &"NixHash") }) } @@ -27,16 +27,16 @@ impl Serialize for NixHash { /// The length of a sha256 digest, nixbase32-encoded. const NIXBASE32_SHA256_ENCODE_LEN: usize = nixbase32::encode_len(32); -pub fn from_nix_hash_string<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> +pub fn from_nix_nixbase32_or_sri<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> where D: serde::Deserializer<'de>, { let str: &'de str = Deserialize::deserialize(deserializer)?; if let Some(digest_str) = str.strip_prefix("sha256:") { - return from_nix_nixbase32_string::(digest_str); + return from_nix_nixbase32::(digest_str); } if let Some(digest_str) = str.strip_prefix("sha256-") { - return from_sri_string::(digest_str); + return from_sri::(digest_str); } Err(serde::de::Error::invalid_value( serde::de::Unexpected::Str(str), @@ -44,7 +44,7 @@ where )) } -pub fn from_sri_string<'de, D>(str: &str) -> Result<[u8; 32], D::Error> +pub fn from_sri<'de, D>(str: &str) -> Result<[u8; 32], D::Error> where D: serde::Deserializer<'de>, { @@ -64,7 +64,7 @@ where Ok(digest) } -pub fn from_nix_nixbase32_string<'de, D>(str: &str) -> Result<[u8; 32], D::Error> +pub fn from_nix_nixbase32<'de, D>(str: &str) -> Result<[u8; 32], D::Error> where D: serde::Deserializer<'de>, { @@ -80,10 +80,10 @@ where Ok(digest) } -pub fn to_nix_nixbase32_string(v: &[u8; 32], serializer: S) -> Result +pub fn to_nix_nixbase32(v: &[u8; 32], serializer: S) -> Result where S: serde::Serializer, { - let string = NixHash::Sha256(*v).to_nix_nixbase32_string(); + let string = NixHash::Sha256(*v).to_nix_nixbase32(); string.serialize(serializer) } diff --git a/snix/nix-compat/src/path_info.rs b/snix/nix-compat/src/path_info.rs index fce9f2b1e..4a6934484 100644 --- a/snix/nix-compat/src/path_info.rs +++ b/snix/nix-compat/src/path_info.rs @@ -14,8 +14,8 @@ pub struct ExportedPathInfo<'a> { #[serde( rename = "narHash", - serialize_with = "nixhash::serde::to_nix_nixbase32_string", - deserialize_with = "nixhash::serde::from_nix_hash_string" + serialize_with = "nixhash::serde::to_nix_nixbase32", + deserialize_with = "nixhash::serde::from_nix_nixbase32_or_sri" )] pub nar_sha256: [u8; 32], diff --git a/snix/nix-compat/src/store_path/utils.rs b/snix/nix-compat/src/store_path/utils.rs index 63b696946..1256ac43e 100644 --- a/snix/nix-compat/src/store_path/utils.rs +++ b/snix/nix-compat/src/store_path/utils.rs @@ -79,7 +79,7 @@ where /// Helper function, used for the non-sha256 [CAHash::Nar] and all [CAHash::Flat]. fn fixed_out_digest(prefix: &str, hash: &NixHash) -> [u8; 32] { - Sha256::new_with_prefix(format!("{}:{}:", prefix, hash.to_nix_hex_string())) + Sha256::new_with_prefix(format!("{}:{}:", prefix, hash.to_nix_lowerhex_string())) .finalize() .into() }