feat(tvix/nix-compat): implement Serialize, Deserialize for NixHash
We use the (slightly more tolerant) from_str to deserialize, and serialize out as SRI. Change-Id: If76b0ed2d4e243904f02df34f6c90b976c0bab8c Reviewed-on: https://cl.tvl.fyi/c/depot/+/11393 Tested-by: BuildkiteCI Reviewed-by: raitobezarius <tvl@lahfa.xyz>
This commit is contained in:
parent
57fba1f167
commit
a2322d7c14
3 changed files with 50 additions and 9 deletions
|
@ -61,7 +61,7 @@ impl FetchArgs {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
sha256: Option<String>,
|
sha256: Option<String>,
|
||||||
mode: HashMode,
|
mode: HashMode,
|
||||||
) -> nixhash::Result<Self> {
|
) -> nixhash::NixHashResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: name.unwrap_or_else(|| url_basename(&url).to_owned()),
|
name: name.unwrap_or_else(|| url_basename(&url).to_owned()),
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub(crate) fn parse(i: &[u8]) -> Result<Derivation, Error<&[u8]>> {
|
||||||
fn from_algo_and_mode_and_digest<B: AsRef<[u8]>>(
|
fn from_algo_and_mode_and_digest<B: AsRef<[u8]>>(
|
||||||
algo_and_mode: &str,
|
algo_and_mode: &str,
|
||||||
digest: B,
|
digest: B,
|
||||||
) -> crate::nixhash::Result<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()?,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::nixbase32;
|
use crate::nixbase32;
|
||||||
use bstr::ByteSlice;
|
use bstr::ByteSlice;
|
||||||
use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER};
|
use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use thiserror;
|
use thiserror;
|
||||||
|
@ -50,7 +52,7 @@ impl Display for NixHash {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// convenience Result type for all nixhash parsing Results.
|
/// convenience Result type for all nixhash parsing Results.
|
||||||
pub type Result<V> = std::result::Result<V, Error>;
|
pub type NixHashResult<V> = std::result::Result<V, Error>;
|
||||||
|
|
||||||
impl NixHash {
|
impl NixHash {
|
||||||
/// returns the algo as [HashAlgo].
|
/// returns the algo as [HashAlgo].
|
||||||
|
@ -118,16 +120,39 @@ impl TryFrom<(HashAlgo, &[u8])> for NixHash {
|
||||||
/// 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.
|
||||||
fn try_from(value: (HashAlgo, &[u8])) -> Result<Self> {
|
fn try_from(value: (HashAlgo, &[u8])) -> NixHashResult<Self> {
|
||||||
let (algo, digest) = value;
|
let (algo, digest) = value;
|
||||||
from_algo_and_digest(algo, digest)
|
from_algo_and_digest(algo, digest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for NixHash {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let str: &'de str = Deserialize::deserialize(deserializer)?;
|
||||||
|
from_str(str, None).map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(str), &"NixHash")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for NixHash {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
// encode as SRI
|
||||||
|
let string = format!("{}-{}", self.algo(), BASE64.encode(self.digest_as_bytes()));
|
||||||
|
string.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> Result<NixHash> {
|
pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult<NixHash> {
|
||||||
if digest.len() != algo.digest_length() {
|
if digest.len() != algo.digest_length() {
|
||||||
return Err(Error::InvalidEncodedDigestLength(digest.len(), algo));
|
return Err(Error::InvalidEncodedDigestLength(digest.len(), algo));
|
||||||
}
|
}
|
||||||
|
@ -180,7 +205,7 @@ pub enum Error {
|
||||||
/// The hash is communicated out-of-band, but might also be in-band (in the
|
/// 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
|
/// case of a nix hash string or SRI), in which it needs to be consistent with the
|
||||||
/// one communicated out-of-band.
|
/// one communicated out-of-band.
|
||||||
pub fn from_str(s: &str, algo_str: Option<&str>) -> Result<NixHash> {
|
pub fn from_str(s: &str, algo_str: Option<&str>) -> NixHashResult<NixHash> {
|
||||||
// if algo_str is some, parse or bail out
|
// if algo_str is some, parse or bail out
|
||||||
let algo: Option<HashAlgo> = if let Some(algo_str) = algo_str {
|
let algo: Option<HashAlgo> = if let Some(algo_str) = algo_str {
|
||||||
Some(algo_str.try_into()?)
|
Some(algo_str.try_into()?)
|
||||||
|
@ -230,7 +255,7 @@ pub fn from_str(s: &str, algo_str: Option<&str>) -> Result<NixHash> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a Nix hash string ($algo:$digest) to a NixHash.
|
/// Parses a Nix hash string ($algo:$digest) to a NixHash.
|
||||||
pub fn from_nix_str(s: &str) -> Result<NixHash> {
|
pub fn from_nix_str(s: &str) -> NixHashResult<NixHash> {
|
||||||
if let Some(rest) = s.strip_prefix("sha1:") {
|
if let Some(rest) = s.strip_prefix("sha1:") {
|
||||||
decode_digest(rest.as_bytes(), HashAlgo::Sha1)
|
decode_digest(rest.as_bytes(), HashAlgo::Sha1)
|
||||||
} else if let Some(rest) = s.strip_prefix("sha256:") {
|
} else if let Some(rest) = s.strip_prefix("sha256:") {
|
||||||
|
@ -250,7 +275,7 @@ pub fn from_nix_str(s: &str) -> Result<NixHash> {
|
||||||
/// It instead simply cuts everything off after the expected length for the
|
/// It instead simply cuts everything off after the expected length for the
|
||||||
/// specified algo, and tries to parse the rest in permissive base64 (allowing
|
/// specified algo, and tries to parse the rest in permissive base64 (allowing
|
||||||
/// missing padding).
|
/// missing padding).
|
||||||
pub fn from_sri_str(s: &str) -> Result<NixHash> {
|
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('-')
|
.split_once('-')
|
||||||
|
@ -294,7 +319,7 @@ pub fn from_sri_str(s: &str) -> Result<NixHash> {
|
||||||
/// Decode a plain digest depending on the hash algo specified externally.
|
/// Decode a plain digest depending on the hash algo specified externally.
|
||||||
/// hexlower, nixbase32 and base64 encodings are supported - the encoding is
|
/// hexlower, nixbase32 and base64 encodings are supported - the encoding is
|
||||||
/// inferred from the input length.
|
/// inferred from the input length.
|
||||||
fn decode_digest(s: &[u8], algo: HashAlgo) -> Result<NixHash> {
|
fn decode_digest(s: &[u8], algo: HashAlgo) -> NixHashResult<NixHash> {
|
||||||
// for the chosen hash algo, calculate the expected (decoded) digest length
|
// for the chosen hash algo, calculate the expected (decoded) digest length
|
||||||
// (as bytes)
|
// (as bytes)
|
||||||
let digest = if s.len() == HEXLOWER.encode_len(algo.digest_length()) {
|
let digest = if s.len() == HEXLOWER.encode_len(algo.digest_length()) {
|
||||||
|
@ -556,4 +581,20 @@ mod tests {
|
||||||
// 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("sha256")).expect_err("must fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_deserialize() {
|
||||||
|
let nixhash_actual = NixHash::Sha256(hex!(
|
||||||
|
"b3271e24c5049270430872bc786b3aad45372109fe1e741f5117c2ac3c583daf"
|
||||||
|
));
|
||||||
|
let nixhash_str_json = "\"sha256-syceJMUEknBDCHK8eGs6rUU3IQn+HnQfURfCrDxYPa8=\"";
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&nixhash_actual).expect("can serialize");
|
||||||
|
|
||||||
|
assert_eq!(nixhash_str_json, &serialized);
|
||||||
|
|
||||||
|
let deserialized: NixHash =
|
||||||
|
serde_json::from_str(nixhash_str_json).expect("must deserialize");
|
||||||
|
assert_eq!(&nixhash_actual, &deserialized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue