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 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()))?;
}

View file

@ -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<SnixStoreIO>")]
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),
},
)
}

View file

@ -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())),

View file

@ -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"

View file

@ -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<Fetch> = 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())
}
});

View file

@ -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()

View file

@ -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<I> {
@ -84,11 +84,11 @@ fn from_algo_and_mode_and_digest<B: AsRef<[u8]>>(
digest: B,
) -> crate::nixhash::NixHashResult<CAHash> {
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(),
)?),

View file

@ -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(

View file

@ -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),
}
}
}

View file

@ -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()
)

View file

@ -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,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.
/// It can fail if the passed digest length doesn't match what's expected for
/// the passed algo.
@ -144,115 +125,37 @@ pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult<NixH
})
}
/// 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 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),
/// Constructs a new [NixHash] from the Nix default hash format,
/// the inverse of [Self::to_nix_nixbase32].
pub fn from_nix_nixbase32(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()
}
/// 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<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()))
}
/// Formats a [NixHash] in the Nix nixbase32 format.
pub fn to_nix_nixbase32(&self) -> String {
format!(
"{}:{}",
self.algo(),
nixbase32::encode(self.digest_as_bytes())
)
}
/// 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<NixHash> {
/// (See caveats in [Self] on the deviations from the SRI spec)
pub fn from_sri(s: &str) -> NixHashResult<NixHash> {
// split at the first occurence of "-"
let (algo_str, digest_str) = s
.split_once('-')
.ok_or_else(|| Error::InvalidSRI(s.to_string()))?;
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()?;
@ -286,7 +189,103 @@ pub fn from_sri_str(s: &str) -> NixHashResult<NixHash> {
.decode(digest_str.trim_end_with(|c| c == '='))
.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.
@ -309,14 +308,14 @@ fn decode_digest(s: &[u8], algo: HashAlgo) -> NixHashResult<NixHash> {
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"))
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"))
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]

View file

@ -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::<D>(digest_str);
return from_nix_nixbase32::<D>(digest_str);
}
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(
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<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
S: serde::Serializer,
{
let string = NixHash::Sha256(*v).to_nix_nixbase32_string();
let string = NixHash::Sha256(*v).to_nix_nixbase32();
string.serialize(serializer)
}

View file

@ -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],

View file

@ -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()
}