posy 0.0.1

[wip] Python installer and package manager
use crate::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub struct ArtifactHash {
    pub mode: String,
    pub raw_data: Vec<u8>,
}

impl ArtifactHash {
    pub fn from_hex(mode: &str, hex: &str) -> Result<ArtifactHash> {
        Ok(ArtifactHash {
            mode: mode.into(),
            raw_data: data_encoding::HEXLOWER_PERMISSIVE.decode(hex.as_bytes())?,
        })
    }

    pub fn checker<'a, T: Write>(&'a self, inner: T) -> Result<HashChecker<'a, T>> {
        let algorithm = match self.mode.as_str() {
            "sha256" => &ring::digest::SHA256,
            _ => bail!("unknown hash algorithm {}", self.mode),
        };
        let state = ring::digest::Context::new(algorithm);
        Ok(HashChecker {
            inner,
            state,
            expected: self,
        })
    }
}

impl Display for ArtifactHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}={}",
            self.mode,
            data_encoding::HEXLOWER.encode(&self.raw_data),
        )
    }
}

impl TryFrom<&str> for ArtifactHash {
    type Error = eyre::Report;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let (mode, data) = value
            .split_once('=')
            .ok_or(eyre!("expected = in hash string {:?}", value))?;
        Ok(ArtifactHash::from_hex(mode, data)?)
    }
}

try_from_str_boilerplate!(ArtifactHash);

pub struct HashChecker<'a, T: Write> {
    inner: T,
    state: ring::digest::Context,
    expected: &'a ArtifactHash,
}

impl<'a, T: Write> HashChecker<'a, T> {
    pub fn finish(self) -> Result<T> {
        let digest = self.state.finish();
        if &self.expected.raw_data != digest.as_ref() {
            bail!("hash mismatch: {:?} != {:?}", self.expected, digest);
        }
        Ok(self.inner)
    }
}

impl<'a, T: Write> Write for HashChecker<'a, T> {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let written = self.inner.write(&buf)?;
        self.state.update(&buf[..written]);
        Ok(written)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.inner.flush()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_artifact_hash() {
        let value = ArtifactHash::from_hex(
            "sha256",
            "c27c231e66336183c484fbfe080fa6cc954149366c15dc21db8b7290081ec7b8",
        )
        .unwrap();
        assert_eq!(
            value.to_string(),
            "sha256=c27c231e66336183c484fbfe080fa6cc954149366c15dc21db8b7290081ec7b8"
        );

        let json = serde_json::to_string(&value).unwrap();
        let new_value: ArtifactHash = serde_json::from_str(&json).unwrap();
        assert_eq!(value, new_value);
    }

    #[test]
    fn test_hash_checker() {
        let gold_data = b"a drop of golden sun";
        let good_hash = ArtifactHash::from_hex(
            "sha256",
            "9c7ed1509d1809656c86aa1201fde2650ec056ab79f6546ba8205f6e42cff949",
        )
        .unwrap();
        let bad_hash = ArtifactHash::from_hex(
            "sha256",
            "007ed1509d1809656c86aa1201fde2650ec056ab79f6546ba8205f6e42cff949",
        )
        .unwrap();

        let buf = Vec::<u8>::new();
        let mut good_checker = good_hash.checker(buf).unwrap();
        assert!(good_checker.write_all(gold_data).is_ok());
        assert!(good_checker.flush().is_ok());
        let unwrapped = good_checker.finish().unwrap();
        assert_eq!(unwrapped.as_slice(), gold_data);

        let buf = Vec::<u8>::new();
        let mut bad_checker = bad_hash.checker(buf).unwrap();
        assert!(bad_checker.write_all(gold_data).is_ok());
        assert!(bad_checker.flush().is_ok());
        assert!(bad_checker.finish().is_err());
    }
}