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<HashAlgo>, 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 <personal@ilanjoselevich.com>
Reviewed-by: edef <edef@edef.eu>
Tested-by: besadii
This commit is contained in:
Florian Klink 2025-06-03 22:47:47 +03:00
parent 6c1bfd778e
commit f6c66af33d
14 changed files with 280 additions and 265 deletions

View file

@ -4,7 +4,7 @@ use crate::known_paths::KnownPaths;
use crate::snix_store_io::SnixStoreIO; use crate::snix_store_io::SnixStoreIO;
use bstr::BString; use bstr::BString;
use nix_compat::derivation::{Derivation, Output}; 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 nix_compat::store_path::{StorePath, StorePathRef};
use snix_eval::builtin_macros::builtins; use snix_eval::builtin_macros::builtins;
use snix_eval::generators::{self, GenCo, emit_warning_kind}; use snix_eval::generators::{self, GenCo, emit_warning_kind};
@ -132,9 +132,14 @@ fn handle_fixed_output(
None => None, None => None,
}; };
// construct a NixHash. let hash_algo = hash_algo_str
let nixhash = nixhash::from_str(&hash_str, hash_algo_str.as_deref()) .map(|s| HashAlgo::try_from(s.as_str()))
.transpose()
.map_err(DerivationError::InvalidOutputHash)?; .map_err(DerivationError::InvalidOutputHash)?;
// construct a NixHash.
let nixhash =
NixHash::from_str(&hash_str, hash_algo).map_err(DerivationError::InvalidOutputHash)?;
let algo = nixhash.algo(); let algo = nixhash.algo();
// construct the fixed output. // construct the fixed output.
@ -143,8 +148,8 @@ fn handle_fixed_output(
Output { Output {
path: None, path: None,
ca_hash: match hash_mode_str.as_deref() { ca_hash: match hash_mode_str.as_deref() {
None | Some("flat") => Some(nixhash::CAHash::Flat(nixhash)), None | Some("flat") => Some(CAHash::Flat(nixhash)),
Some("recursive") => Some(nixhash::CAHash::Nar(nixhash)), Some("recursive") => Some(CAHash::Nar(nixhash)),
Some(other) => { Some(other) => {
return Err(DerivationError::InvalidOutputHashMode(other.to_string()))?; return Err(DerivationError::InvalidOutputHashMode(other.to_string()))?;
} }

View file

@ -5,7 +5,7 @@ use crate::{
fetchers::{Fetch, url_basename}, fetchers::{Fetch, url_basename},
snix_store_io::SnixStoreIO, snix_store_io::SnixStoreIO,
}; };
use nix_compat::nixhash; use nix_compat::nixhash::{HashAlgo, NixHash};
use snix_eval::builtin_macros::builtins; use snix_eval::builtin_macros::builtins;
use snix_eval::generators::Gen; use snix_eval::generators::Gen;
use snix_eval::generators::GenCo; use snix_eval::generators::GenCo;
@ -70,15 +70,13 @@ async fn extract_fetch_args(
} }
// parse the sha256 string into a digest. // parse the sha256 string into a digest.
let sha256 = match sha256_str { let sha256 = sha256_str
Some(sha256_str) => { .map(|x| {
let nixhash = nixhash::from_str(&sha256_str, Some("sha256")) NixHash::from_str(&x, Some(HashAlgo::Sha256))
.map_err(|e| ErrorKind::InvalidHash(e.to_string()))?; .map(|x| x.digest_as_bytes().try_into().expect("is sha256"))
.map_err(|e| ErrorKind::InvalidHash(e.to_string()))
Some(nixhash.digest_as_bytes().try_into().expect("is sha256")) })
} .transpose()?;
None => None,
};
// Parse the URL. // Parse the URL.
let url = Url::parse(&url_str).map_err(|e| ErrorKind::SnixError(Rc::new(e)))?; 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<SnixStoreIO>")] #[builtins(state = "Rc<SnixStoreIO>")]
pub(crate) mod fetcher_builtins { pub(crate) mod fetcher_builtins {
use bstr::ByteSlice; use bstr::ByteSlice;
use nix_compat::flakeref; use nix_compat::{flakeref, nixhash::NixHash};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use super::*; use super::*;
@ -154,7 +152,7 @@ pub(crate) mod fetcher_builtins {
name, name,
Fetch::URL { Fetch::URL {
url: args.url, url: args.url,
exp_hash: args.sha256.map(nixhash::NixHash::Sha256), exp_hash: args.sha256.map(NixHash::Sha256),
}, },
) )
} }

View file

@ -111,7 +111,7 @@ mod import_builtins {
use crate::builtins::ImportError; use crate::builtins::ImportError;
use crate::snix_store_io::SnixStoreIO; use crate::snix_store_io::SnixStoreIO;
use bstr::ByteSlice; 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 nix_compat::store_path::{StorePath, StorePathRef, build_ca_path};
use sha2::Digest; use sha2::Digest;
use snix_castore::blobservice::BlobService; use snix_castore::blobservice::BlobService;
@ -368,7 +368,7 @@ mod import_builtins {
.select("sha256") .select("sha256")
.map(|h| { .map(|h| {
h.to_str().and_then(|expected| { 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(NixHash::Sha256(digest)) => Ok(digest),
Ok(_) => unreachable!(), Ok(_) => unreachable!(),
Err(e) => Err(ErrorKind::InvalidHash(e.to_string())), Err(e) => Err(ErrorKind::InvalidHash(e.to_string())),

View file

@ -603,7 +603,7 @@ mod tests {
mod fetch { mod fetch {
use super::super::*; use super::super::*;
use crate::fetchers::Fetch; use crate::fetchers::Fetch;
use nix_compat::{nixbase32, nixhash}; use nix_compat::{nixbase32, nixhash::NixHash};
use rstest::rstest; use rstest::rstest;
#[rstest] #[rstest]
@ -618,7 +618,7 @@ mod tests {
#[case::url_sha256( #[case::url_sha256(
Fetch::URL{ Fetch::URL{
url: Url::parse("https://raw.githubusercontent.com/aaptel/notmuch-extract-patch/f732a53e12a7c91a06755ebfab2007adc9b3063b/notmuch-extract-patch").unwrap(), 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()), Some(StorePathRef::from_bytes(b"06qi00hylriyfm0nl827crgjvbax84mz-notmuch-extract-patch").unwrap()),
"notmuch-extract-patch" "notmuch-extract-patch"
@ -626,7 +626,7 @@ mod tests {
#[case::url_custom_name( #[case::url_custom_name(
Fetch::URL{ Fetch::URL{
url: Url::parse("https://test.example/owo").unwrap(), 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()), Some(StorePathRef::from_bytes(b"06qi00hylriyfm0nl827crgjvbax84mz-notmuch-extract-patch").unwrap()),
"notmuch-extract-patch" "notmuch-extract-patch"
@ -634,7 +634,7 @@ mod tests {
#[case::nar_sha256( #[case::nar_sha256(
Fetch::NAR{ Fetch::NAR{
url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), 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()), Some(StorePathRef::from_bytes(b"b40vjphshq4fdgv8s3yrp0bdlafi4920-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()),
"0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz"
@ -642,7 +642,7 @@ mod tests {
#[case::nar_sha1( #[case::nar_sha1(
Fetch::NAR{ Fetch::NAR{
url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), 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()), Some(StorePathRef::from_bytes(b"8kx7fdkdbzs4fkfb57xq0cbhs20ymq2n-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()),
"0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz"
@ -650,7 +650,7 @@ mod tests {
#[case::nar_sha1( #[case::nar_sha1(
Fetch::Executable{ Fetch::Executable{
url: Url::parse("https://cache.nixos.org/nar/0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap(), 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()), Some(StorePathRef::from_bytes(b"y92hm2xfk1009hrq0ix80j4m5k4j4w21-0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz").unwrap()),
"0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz" "0r8nqa1klm5v17ifc6z96m9wywxkjvgbnqq9pmy0sgqj53wj3n12.nar.xz"

View file

@ -147,7 +147,7 @@ mod tests {
use std::sync::LazyLock; use std::sync::LazyLock;
use hex_literal::hex; 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 url::Url;
use super::KnownPaths; use super::KnownPaths;
@ -186,7 +186,7 @@ mod tests {
static FETCH_URL: LazyLock<Fetch> = LazyLock::new(|| { static FETCH_URL: LazyLock<Fetch> = LazyLock::new(|| {
Fetch::URL { Fetch::URL {
url: Url::parse("https://raw.githubusercontent.com/aaptel/notmuch-extract-patch/f732a53e12a7c91a06755ebfab2007adc9b3063b/notmuch-extract-patch").unwrap(), 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())
} }
}); });

View file

@ -165,7 +165,7 @@ impl Derivation {
Sha256::new_with_prefix(format!( Sha256::new_with_prefix(format!(
"fixed:out:{}{}:{}", "fixed:out:{}{}:{}",
ca_kind_prefix(ca_hash), ca_kind_prefix(ca_hash),
ca_hash.hash().to_nix_hex_string(), ca_hash.hash().to_nix_lowerhex_string(),
out_output out_output
.path .path
.as_ref() .as_ref()

View file

@ -15,7 +15,7 @@ use thiserror;
use crate::derivation::parse_error::{into_nomerror, ErrorKind, NomError, NomResult}; use crate::derivation::parse_error::{into_nomerror, ErrorKind, NomError, NomResult};
use crate::derivation::{write, CAHash, Derivation, Output}; use crate::derivation::{write, CAHash, Derivation, Output};
use crate::store_path::{self, StorePath}; use crate::store_path::{self, StorePath};
use crate::{aterm, nixhash}; use crate::{aterm, nixhash, nixhash::NixHash};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error<I> { pub enum Error<I> {
@ -84,11 +84,11 @@ fn from_algo_and_mode_and_digest<B: AsRef<[u8]>>(
digest: B, digest: B,
) -> crate::nixhash::NixHashResult<CAHash> { ) -> crate::nixhash::NixHashResult<CAHash> {
Ok(match algo_and_mode.strip_prefix("r:") { 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()?, algo.try_into()?,
digest.as_ref(), 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()?, algo_and_mode.try_into()?,
digest.as_ref(), digest.as_ref(),
)?), )?),

View file

@ -326,7 +326,7 @@ fn output_path_construction() {
Output { Output {
path: None, // will be calculated path: None, // will be calculated
ca_hash: Some(crate::nixhash::CAHash::Nar( ca_hash: Some(crate::nixhash::CAHash::Nar(
crate::nixhash::from_algo_and_digest( crate::nixhash::NixHash::from_algo_and_digest(
crate::nixhash::HashAlgo::Sha256, crate::nixhash::HashAlgo::Sha256,
&data_encoding::HEXLOWER &data_encoding::HEXLOWER
.decode( .decode(

View file

@ -69,7 +69,7 @@ impl TryFrom<&str> for HashAlgo {
"sha1" => Ok(Self::Sha1), "sha1" => Ok(Self::Sha1),
"sha256" => Ok(Self::Sha256), "sha256" => Ok(Self::Sha256),
"sha512" => Ok(Self::Sha512), "sha512" => Ok(Self::Sha512),
_ => Err(Error::InvalidAlgo(algo_str.to_string())), _ => Err(Error::InvalidAlgo),
} }
} }
} }

View file

@ -85,9 +85,9 @@ impl CAHash {
} }
"fixed" => { "fixed" => {
if let Some(s) = s.strip_prefix("r:") { 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 { } else {
NixHash::from_nix_nixbase32_str(s).map(CAHash::Flat) NixHash::from_nix_nixbase32(s).map(CAHash::Flat)
} }
} }
_ => None, _ => None,
@ -218,7 +218,12 @@ impl<'de> Deserialize<'de> for CAHash {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{derivation::CAHash, nixhash}; use hex_literal::hex;
use crate::{
derivation::CAHash,
nixhash::{HashAlgo, NixHash},
};
#[test] #[test]
fn serialize_flat() { fn serialize_flat() {
@ -227,8 +232,9 @@ mod tests {
"hashAlgo": "sha256" "hashAlgo": "sha256"
}"#; }"#;
let hash = CAHash::Flat( let hash = CAHash::Flat(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
) )
.unwrap(), .unwrap(),
); );
@ -243,8 +249,9 @@ mod tests {
"hashAlgo": "r:sha256" "hashAlgo": "r:sha256"
}"#; }"#;
let hash = CAHash::Nar( let hash = CAHash::Nar(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
) )
.unwrap(), .unwrap(),
); );
@ -264,8 +271,9 @@ mod tests {
assert_eq!( assert_eq!(
hash, hash,
CAHash::Flat( CAHash::Flat(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")
) )
.unwrap() .unwrap()
) )
@ -284,8 +292,9 @@ mod tests {
assert_eq!( assert_eq!(
hash, hash,
CAHash::Nar( CAHash::Nar(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")
) )
.unwrap() .unwrap()
) )
@ -304,8 +313,9 @@ mod tests {
assert_eq!( assert_eq!(
hash, hash,
CAHash::Nar( CAHash::Nar(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
) )
.unwrap() .unwrap()
) )
@ -324,8 +334,9 @@ mod tests {
assert_eq!( assert_eq!(
hash, hash,
CAHash::Nar( CAHash::Nar(
nixhash::from_nix_str( NixHash::from_algo_and_digest(
"sha256:08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba" HashAlgo::Sha256,
&hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
) )
.unwrap() .unwrap()
) )

View file

@ -13,7 +13,44 @@ pub use algos::HashAlgo;
pub use ca_hash::CAHash; pub use ca_hash::CAHash;
pub use ca_hash::HashMode as CAHashMode; 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum NixHash { pub enum NixHash {
Md5([u8; 16]), Md5([u8; 16]),
@ -72,62 +109,6 @@ impl NixHash {
} }
} }
/// 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<Self> {
let (tag, digest) = s.split_once(':')?;
(match tag {
"md5" => nixbase32::decode_fixed(digest).map(NixHash::Md5),
"sha1" => nixbase32::decode_fixed(digest).map(NixHash::Sha1),
"sha256" => nixbase32::decode_fixed(digest).map(NixHash::Sha256),
"sha512" => nixbase32::decode_fixed(digest)
.map(Box::new)
.map(NixHash::Sha512),
_ => return None,
})
.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 {
format!(
"{}:{}",
self.algo(),
nixbase32::encode(self.digest_as_bytes())
)
}
/// 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!(
w,
"{}-{}",
self.algo(),
BASE64.encode(self.digest_as_bytes())
)
}
/// Formats a [NixHash] to an SRI string.
pub fn to_sri_string(&self) -> String {
let mut s = String::new();
self.write_sri_str(&mut s).unwrap();
s
}
}
/// Constructs a new [NixHash] by specifying [HashAlgo] and digest. /// Constructs a new [NixHash] by specifying [HashAlgo] and digest.
/// It can fail if the passed digest length doesn't match what's expected for /// It can fail if the passed digest length doesn't match what's expected for
/// the passed algo. /// the passed algo.
@ -144,115 +125,37 @@ pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult<NixH
}) })
} }
/// Errors related to NixHash construction. /// Constructs a new [NixHash] from the Nix default hash format,
#[derive(Debug, Eq, PartialEq, thiserror::Error)] /// the inverse of [Self::to_nix_nixbase32].
pub enum Error { pub fn from_nix_nixbase32(s: &str) -> Option<Self> {
#[error("invalid hash algo: {0}")] let (tag, digest) = s.split_once(':')?;
InvalidAlgo(String),
#[error("invalid SRI string: {0}")] (match tag {
InvalidSRI(String), "md5" => nixbase32::decode_fixed(digest).map(NixHash::Md5),
#[error("invalid encoded digest length '{0}' for algo {1}")] "sha1" => nixbase32::decode_fixed(digest).map(NixHash::Sha1),
InvalidDigestLength(usize, HashAlgo), "sha256" => nixbase32::decode_fixed(digest).map(NixHash::Sha256),
#[error("invalid base16 encoding: {0}")] "sha512" => nixbase32::decode_fixed(digest)
InvalidBase16Encoding(data_encoding::DecodeError), .map(Box::new)
#[error("invalid base32 encoding: {0}")] .map(NixHash::Sha512),
InvalidBase32Encoding(data_encoding::DecodeError), _ => return None,
#[error("invalid base64 encoding: {0}")] })
InvalidBase64Encoding(data_encoding::DecodeError), .ok()
#[error("conflicting hash algo: {0} (hash_algo) vs {1} (inline)")]
ConflictingHashAlgos(HashAlgo, HashAlgo),
#[error("missing inline hash algo, but no externally-specified algo: {0}")]
MissingInlineHashAlgo(String),
} }
/// Nix allows specifying hashes in various encodings, and magically just /// Formats a [NixHash] in the Nix nixbase32 format.
/// derives the encoding. pub fn to_nix_nixbase32(&self) -> String {
/// This function parses strings to a NixHash. format!(
/// "{}:{}",
/// Hashes can be: self.algo(),
/// - Nix hash strings nixbase32::encode(self.digest_as_bytes())
/// - 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<NixHash> {
// if algo_str is some, parse or bail out
let algo: Option<HashAlgo> = 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<NixHash> {
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. /// Parses a Nix SRI string to a NixHash.
/// Contrary to the SRI spec, Nix doesn't have an understanding of passing /// (See caveats in [Self] on the deviations from the SRI spec)
/// multiple hashes (with different algos) in SRI hashes. pub fn from_sri(s: &str) -> NixHashResult<NixHash> {
/// 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<NixHash> {
// split at the first occurence of "-" // split at the first occurence of "-"
let (algo_str, digest_str) = s let (algo_str, digest_str) = s.split_once('-').ok_or(Error::InvalidSRI)?;
.split_once('-')
.ok_or_else(|| Error::InvalidSRI(s.to_string()))?;
// try to map the part before that `-` to a supported hash algo: // try to map the part before that `-` to a supported hash algo:
let algo: HashAlgo = algo_str.try_into()?; let algo: HashAlgo = algo_str.try_into()?;
@ -286,7 +189,103 @@ pub fn from_sri_str(s: &str) -> NixHashResult<NixHash> {
.decode(digest_str.trim_end_with(|c| c == '=')) .decode(digest_str.trim_end_with(|c| c == '='))
.map_err(Error::InvalidBase64Encoding)?; .map_err(Error::InvalidBase64Encoding)?;
from_algo_and_digest(algo, &digest) 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!(
w,
"{}-{}",
self.algo(),
BASE64.encode(self.digest_as_bytes())
)
}
/// Formats a [NixHash] to an SRI string.
pub fn to_sri_string(&self) -> String {
let mut s = String::new();
self.write_sri_str(&mut s).unwrap();
s
}
/// 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())
)
}
/// 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<HashAlgo>) -> NixHashResult<NixHash> {
// 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")]
InvalidAlgo,
#[error("invalid SRI string")]
InvalidSRI,
#[error("invalid encoded digest length '{0}' for algo {1}")]
InvalidDigestLength(usize, HashAlgo),
#[error("invalid base16 encoding: {0}")]
InvalidBase16Encoding(data_encoding::DecodeError),
#[error("invalid base32 encoding: {0}")]
InvalidBase32Encoding(data_encoding::DecodeError),
#[error("invalid base64 encoding: {0}")]
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:?}")]
MissingInlineHashAlgo(String),
} }
/// Decode a plain digest depending on the hash algo specified externally. /// Decode a plain digest depending on the hash algo specified externally.
@ -309,14 +308,14 @@ fn decode_digest(s: &[u8], algo: HashAlgo) -> NixHashResult<NixHash> {
Err(Error::InvalidDigestLength(s.len(), algo))? 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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
nixbase32, nixbase32,
nixhash::{self, HashAlgo, NixHash}, nixhash::{HashAlgo, NixHash},
}; };
use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER}; use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER};
use hex_literal::hex; use hex_literal::hex;
@ -345,68 +344,67 @@ mod tests {
} }
// TODO // TODO
fn make_nixhash(algo: &HashAlgo, digest_encoded: String) -> String { fn make_nixhash(algo: HashAlgo, digest_encoded: String) -> String {
format!("{}:{}", algo, digest_encoded) 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) format!("{}-{}", algo, digest_encoded)
} }
/// Test parsing a hash string in various formats, and also when/how the out-of-band algo is needed. /// Test parsing a hash string in various formats, and also when/how the out-of-band algo is needed.
#[rstest] #[rstest]
#[case::sha1(&NixHash::Sha1(DIGEST_SHA1))] #[case::sha1(NixHash::Sha1(DIGEST_SHA1))]
#[case::sha256(&NixHash::Sha256(DIGEST_SHA256))] #[case::sha256(NixHash::Sha256(DIGEST_SHA256))]
#[case::sha512(&NixHash::Sha512(Box::new(DIGEST_SHA512)))] #[case::sha512(NixHash::Sha512(Box::new(DIGEST_SHA512)))]
#[case::md5(&NixHash::Md5(DIGEST_MD5))] #[case::md5(NixHash::Md5(DIGEST_MD5))]
fn from_str(#[case] expected_hash: &NixHash) { fn from_str(#[case] expected_hash: NixHash) {
let algo = &expected_hash.algo(); let algo = expected_hash.algo();
let digest = expected_hash.digest_as_bytes(); let digest = expected_hash.digest_as_bytes();
// parse SRI // parse SRI
{ {
// base64 without out-of-band algo // base64 without out-of-band algo
let s = make_sri_string(algo, to_base64(digest)); let s = make_sri_string(algo, to_base64(digest));
let h = nixhash::from_str(&s, None).expect("must succeed"); let h = NixHash::from_str(&s, None).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
// base64 with out-of-band-algo // base64 with out-of-band-algo
let s = make_sri_string(algo, to_base64(digest)); let s = make_sri_string(algo, to_base64(digest));
let h = nixhash::from_str(&s, Some(&expected_hash.algo().to_string())) let h = NixHash::from_str(&s, Some(expected_hash.algo())).expect("must succeed");
.expect("must succeed"); assert_eq!(expected_hash, h);
assert_eq!(expected_hash, &h);
// base64_nopad without out-of-band algo // base64_nopad without out-of-band algo
let s = make_sri_string(algo, to_base64_nopad(digest)); let s = make_sri_string(algo, to_base64_nopad(digest));
let h = nixhash::from_str(&s, None).expect("must succeed"); let h = NixHash::from_str(&s, None).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
// base64_nopad with out-of-band-algo // base64_nopad with out-of-band-algo
let s = make_sri_string(algo, to_base64_nopad(digest)); let s = make_sri_string(algo, to_base64_nopad(digest));
let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); let h = NixHash::from_str(&s, Some(algo)).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
} }
// parse plain base16. should succeed with algo out-of-band, but fail without. // parse plain base16. should succeed with algo out-of-band, but fail without.
{ {
let s = to_base16(digest); let s = to_base16(digest);
nixhash::from_str(&s, None).expect_err("must fail"); NixHash::from_str(&s, None).expect_err("must fail");
let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); let h = NixHash::from_str(&s, Some(algo)).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
} }
// parse plain nixbase32. should succeed with algo out-of-band, but fail without. // parse plain nixbase32. should succeed with algo out-of-band, but fail without.
{ {
let s = to_nixbase32(digest); let s = to_nixbase32(digest);
nixhash::from_str(&s, None).expect_err("must fail"); NixHash::from_str(&s, None).expect_err("must fail");
let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); let h = NixHash::from_str(&s, Some(algo)).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
} }
// parse plain base64. should succeed with algo out-of-band, but fail without. // parse plain base64. should succeed with algo out-of-band, but fail without.
{ {
let s = to_base64(digest); let s = to_base64(digest);
nixhash::from_str(&s, None).expect_err("must fail"); NixHash::from_str(&s, None).expect_err("must fail");
let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed"); let h = NixHash::from_str(&s, Some(algo)).expect("must succeed");
assert_eq!(expected_hash, &h); assert_eq!(expected_hash, h);
} }
// parse Nix hash strings // parse Nix hash strings
@ -416,11 +414,11 @@ mod tests {
let s = make_nixhash(algo, to_base16(digest)); let s = make_nixhash(algo, to_base16(digest));
assert_eq!( assert_eq!(
expected_hash, expected_hash,
&nixhash::from_str(&s, None).expect("must succeed") NixHash::from_str(&s, None).expect("must succeed")
); );
assert_eq!( assert_eq!(
expected_hash, 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. // 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)); let s = make_nixhash(algo, to_nixbase32(digest));
assert_eq!( assert_eq!(
expected_hash, expected_hash,
&nixhash::from_str(&s, None).expect("must succeed") NixHash::from_str(&s, None).expect("must succeed")
); );
assert_eq!( assert_eq!(
expected_hash, 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. // 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)); let s = make_nixhash(algo, to_base64(digest));
assert_eq!( assert_eq!(
expected_hash, expected_hash,
&nixhash::from_str(&s, None).expect("must succeed") NixHash::from_str(&s, None).expect("must succeed")
); );
assert_eq!( assert_eq!(
expected_hash, 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 parsing an SRI hash via the [nixhash::from_sri_str] method.
#[test] #[test]
fn from_sri_str() { 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"); .expect("must succeed");
assert_eq!(HashAlgo::Sha256, nix_hash.algo()); assert_eq!(HashAlgo::Sha256, nix_hash.algo());
@ -471,7 +469,7 @@ mod tests {
#[case::too_much_padding("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ===")] #[case::too_much_padding("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ===")]
#[case::additional_suffix_ignored("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ== cheesecake")] #[case::additional_suffix_ignored("sha512-7g91TBvYoYQorRTqo+rYD/i5YnWvUBLnqDhPHxBJDaBW7smuPMeRp6E6JOFuVN9bzN0QnH1ToUU0u9c2CjALEQ== cheesecake")]
fn from_sri_str_sha512_paddings(#[case] sri_str: &str) { 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!(HashAlgo::Sha512, nix_hash.algo());
assert_eq!( assert_eq!(
@ -484,14 +482,13 @@ mod tests {
/// doesn't match what's expected from that hash function. /// doesn't match what's expected from that hash function.
#[test] #[test]
fn from_sri_str_truncated() { fn from_sri_str_truncated() {
nixhash::from_sri_str("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank") NixHash::from_sri("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank").expect_err("must fail");
.expect_err("must fail");
} }
/// Ensure we fail on SRI hashes that Nix doesn't support. /// Ensure we fail on SRI hashes that Nix doesn't support.
#[test] #[test]
fn from_sri_str_unsupported() { fn from_sri_str_unsupported() {
nixhash::from_sri_str( NixHash::from_sri(
"sha384-o4UVSl89mIB0sFUK+3jQbG+C9Zc9dRlV/Xd3KAvXEbhqxu0J5OAdg6b6VHKHwQ7U", "sha384-o4UVSl89mIB0sFUK+3jQbG+C9Zc9dRlV/Xd3KAvXEbhqxu0J5OAdg6b6VHKHwQ7U",
) )
.expect_err("must fail"); .expect_err("must fail");
@ -500,7 +497,7 @@ mod tests {
/// Ensure we reject invalid base64 encoding /// Ensure we reject invalid base64 encoding
#[test] #[test]
fn from_sri_str_invalid_base64() { 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 /// Nix also accepts SRI strings with missing padding, but only in case the
@ -519,17 +516,20 @@ mod tests {
hex!("7e022bdd3c851830173f9faaa006a230a0e0fdad4c953e85bff4bf0da036e12f"); hex!("7e022bdd3c851830173f9faaa006a230a0e0fdad4c953e85bff4bf0da036e12f");
// passing hash algo out of band should succeed // passing hash algo out of band should succeed
let nix_hash = nixhash::from_str(&format!("sha256-{}", &broken_base64), Some("sha256")) let nix_hash = NixHash::from_str(
&format!("sha256-{}", &broken_base64),
Some(HashAlgo::Sha256),
)
.expect("must succeed"); .expect("must succeed");
assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
// not passing hash algo out of band should succeed // not passing hash algo out of band should succeed
let nix_hash = 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()); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
// not passing SRI, but hash algo out of band should fail // 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, /// As we decided to pass our hashes by trimming `=` completely,
@ -544,17 +544,18 @@ mod tests {
let expected_digest = let expected_digest =
hex!("b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf"); hex!("b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf");
let nix_hash = nixhash::from_str(&format!("sha256-{}", &weird_base64), Some("sha256")) let nix_hash =
NixHash::from_str(&format!("sha256-{}", &weird_base64), Some(HashAlgo::Sha256))
.expect("must succeed"); .expect("must succeed");
assert_eq!(&expected_digest, &nix_hash.digest_as_bytes()); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
// not passing hash algo out of band should succeed // not passing hash algo out of band should succeed
let nix_hash = 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()); assert_eq!(&expected_digest, &nix_hash.digest_as_bytes());
// not passing SRI, but hash algo out of band should fail // 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] #[test]

View file

@ -8,7 +8,7 @@ impl<'de> Deserialize<'de> for NixHash {
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let str: &'de str = Deserialize::deserialize(deserializer)?; 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") 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. /// The length of a sha256 digest, nixbase32-encoded.
const NIXBASE32_SHA256_ENCODE_LEN: usize = nixbase32::encode_len(32); 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 where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let str: &'de str = Deserialize::deserialize(deserializer)?; let str: &'de str = Deserialize::deserialize(deserializer)?;
if let Some(digest_str) = str.strip_prefix("sha256:") { if let Some(digest_str) = str.strip_prefix("sha256:") {
return from_nix_nixbase32_string::<D>(digest_str); return from_nix_nixbase32::<D>(digest_str);
} }
if let Some(digest_str) = str.strip_prefix("sha256-") { if let Some(digest_str) = str.strip_prefix("sha256-") {
return from_sri_string::<D>(digest_str); return from_sri::<D>(digest_str);
} }
Err(serde::de::Error::invalid_value( Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(str), 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 where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
@ -64,7 +64,7 @@ where
Ok(digest) 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 where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
@ -80,10 +80,10 @@ where
Ok(digest) Ok(digest)
} }
pub fn to_nix_nixbase32_string<S>(v: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error> pub fn to_nix_nixbase32<S>(v: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
let string = NixHash::Sha256(*v).to_nix_nixbase32_string(); let string = NixHash::Sha256(*v).to_nix_nixbase32();
string.serialize(serializer) string.serialize(serializer)
} }

View file

@ -14,8 +14,8 @@ pub struct ExportedPathInfo<'a> {
#[serde( #[serde(
rename = "narHash", rename = "narHash",
serialize_with = "nixhash::serde::to_nix_nixbase32_string", serialize_with = "nixhash::serde::to_nix_nixbase32",
deserialize_with = "nixhash::serde::from_nix_hash_string" deserialize_with = "nixhash::serde::from_nix_nixbase32_or_sri"
)] )]
pub nar_sha256: [u8; 32], pub nar_sha256: [u8; 32],

View file

@ -79,7 +79,7 @@ where
/// Helper function, used for the non-sha256 [CAHash::Nar] and all [CAHash::Flat]. /// Helper function, used for the non-sha256 [CAHash::Nar] and all [CAHash::Flat].
fn fixed_out_digest(prefix: &str, hash: &NixHash) -> [u8; 32] { 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() .finalize()
.into() .into()
} }