From a2322d7c142543224eeb9eebc1fedea7aea8b612 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Fri, 12 Apr 2024 13:17:07 +0300 Subject: 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 --- tvix/nix-compat/src/nixhash/mod.rs | 55 +++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) (limited to 'tvix/nix-compat/src/nixhash') diff --git a/tvix/nix-compat/src/nixhash/mod.rs b/tvix/nix-compat/src/nixhash/mod.rs index 97b40aa0b97a..7336831aa072 100644 --- a/tvix/nix-compat/src/nixhash/mod.rs +++ b/tvix/nix-compat/src/nixhash/mod.rs @@ -1,6 +1,8 @@ use crate::nixbase32; use bstr::ByteSlice; use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER}; +use serde::Deserialize; +use serde::Serialize; use std::cmp::Ordering; use std::fmt::Display; use thiserror; @@ -50,7 +52,7 @@ impl Display for NixHash { } /// convenience Result type for all nixhash parsing Results. -pub type Result = std::result::Result; +pub type NixHashResult = std::result::Result; impl NixHash { /// returns the algo as [HashAlgo]. @@ -118,16 +120,39 @@ impl TryFrom<(HashAlgo, &[u8])> for 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. - fn try_from(value: (HashAlgo, &[u8])) -> Result { + fn try_from(value: (HashAlgo, &[u8])) -> NixHashResult { let (algo, digest) = value; from_algo_and_digest(algo, digest) } } +impl<'de> Deserialize<'de> for NixHash { + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + 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. /// 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]) -> Result { +pub fn from_algo_and_digest(algo: HashAlgo, digest: &[u8]) -> NixHashResult { if digest.len() != algo.digest_length() { 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 /// 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>) -> Result { +pub fn from_str(s: &str, algo_str: Option<&str>) -> NixHashResult { // if algo_str is some, parse or bail out let algo: Option = if let Some(algo_str) = algo_str { Some(algo_str.try_into()?) @@ -230,7 +255,7 @@ pub fn from_str(s: &str, algo_str: Option<&str>) -> Result { } /// Parses a Nix hash string ($algo:$digest) to a NixHash. -pub fn from_nix_str(s: &str) -> Result { +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:") { @@ -250,7 +275,7 @@ pub fn from_nix_str(s: &str) -> Result { /// 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) -> Result { +pub fn from_sri_str(s: &str) -> NixHashResult { // split at the first occurence of "-" let (algo_str, digest_str) = s .split_once('-') @@ -294,7 +319,7 @@ pub fn from_sri_str(s: &str) -> Result { /// 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. -fn decode_digest(s: &[u8], algo: HashAlgo) -> Result { +fn decode_digest(s: &[u8], algo: HashAlgo) -> NixHashResult { // for the chosen hash algo, calculate the expected (decoded) digest length // (as bytes) 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 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); + } } -- cgit 1.4.1