ic-host-artifacts 0.4.1

Bounded host artifact streams, identities, archives and Wasm inspection
Documentation
use super::HashingWriter;
use crate::artifact::{Sha256Digest, WriterError};
use std::io::{self, IoSlice, Write};

#[test]
fn empty_and_chunked_output_return_the_sink_and_exact_identity() {
    let writer = HashingWriter::new(Vec::<u8>::new(), 0);
    let (bytes, identity) = writer.into_parts();
    assert_eq!(bytes, [] as [u8; 0]);
    assert_eq!(identity.bytes, 0);
    assert_eq!(identity.sha256, Sha256Digest::compute(b""));
    let mut writer = HashingWriter::new(Vec::new(), 6);
    writer.write_all(b"ab").unwrap();
    writer.write_all(b"cdef").unwrap();
    assert_eq!(writer.bytes_written(), 6);
    let (bytes, identity) = writer.into_parts();
    assert_eq!(bytes, b"abcdef");
    assert_eq!(identity.bytes, 6);
    assert_eq!(identity.sha256, Sha256Digest::compute(&bytes));
}

#[derive(Default)]
struct ShortSink {
    bytes: Vec<u8>,
    interrupt: bool,
    fail_after: Option<usize>,
    flushes: usize,
}

impl Write for ShortSink {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        if std::mem::take(&mut self.interrupt) {
            return Err(io::ErrorKind::Interrupted.into());
        }
        if self
            .fail_after
            .is_some_and(|limit| self.bytes.len() >= limit)
        {
            return Err(io::ErrorKind::PermissionDenied.into());
        }
        let count = bytes.len().min(2);
        self.bytes.extend_from_slice(&bytes[..count]);
        Ok(count)
    }
    fn flush(&mut self) -> io::Result<()> {
        self.flushes += 1;
        Err(io::ErrorKind::BrokenPipe.into())
    }
}

#[test]
fn interrupted_short_and_vectored_writes_hash_only_accepted_bytes() {
    let sink = ShortSink {
        interrupt: true,
        ..ShortSink::default()
    };
    let mut writer = HashingWriter::new(sink, 6);
    writer.write_all(b"abcd").unwrap();
    let accepted = writer
        .write_vectored(&[IoSlice::new(b"ef"), IoSlice::new(b"g")])
        .unwrap();
    assert_eq!(accepted, 2);
    let (sink, identity) = writer.into_parts();
    assert_eq!(sink.bytes, b"abcdef");
    assert_eq!(identity.bytes, 6);
    assert_eq!(identity.sha256, Sha256Digest::compute(&sink.bytes));
    assert_eq!(sink.flushes, 0);
}

#[test]
fn failed_write_and_flush_keep_the_observed_prefix_without_finalization_effects() {
    let sink = ShortSink {
        fail_after: Some(2),
        ..ShortSink::default()
    };
    let mut writer = HashingWriter::new(sink, 9);
    assert_eq!(
        writer.write_all(b"abcdef").unwrap_err().kind(),
        io::ErrorKind::PermissionDenied
    );
    assert_eq!(
        writer.flush().unwrap_err().kind(),
        io::ErrorKind::BrokenPipe
    );
    let (sink, identity) = writer.into_parts();
    assert_eq!(sink.bytes, b"ab");
    assert_eq!(sink.flushes, 1);
    assert_eq!(identity.bytes, 2);
    assert_eq!(identity.sha256, Sha256Digest::compute(b"ab"));
}

#[test]
fn allowance_rejection_keeps_sink_and_hash_unchanged() {
    let mut writer = HashingWriter::new(Vec::new(), 3);
    writer.write_all(b"abc").unwrap();
    let error = writer.write(b"d").unwrap_err();
    assert_eq!(
        error.get_ref().unwrap().downcast_ref::<WriterError>(),
        Some(&WriterError::LimitExceeded { limit: 3 })
    );
    assert!(writer.limit_exceeded());
    assert_eq!(writer.write(b"").unwrap(), 0);
    let (bytes, identity) = writer.into_parts();
    assert_eq!(bytes, b"abc");
    assert_eq!(identity.bytes, 3);
    assert_eq!(identity.sha256, Sha256Digest::compute(b"abc"));
}

struct InvalidSink;
impl Write for InvalidSink {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        Ok(bytes.len() + 1)
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
fn impossible_sink_counts_are_typed_errors_without_advancing_the_identity() {
    let mut writer = HashingWriter::new(InvalidSink, 2);
    let error = writer.write(b"ab").unwrap_err();
    assert_eq!(error.kind(), io::ErrorKind::InvalidData);
    assert_eq!(
        error.get_ref().unwrap().downcast_ref::<WriterError>(),
        Some(&WriterError::InvalidWriteCount {
            offered: 2,
            written: 3
        })
    );
    let (_, identity) = writer.into_parts();
    assert_eq!(identity.bytes, 0);
    assert_eq!(identity.sha256, Sha256Digest::compute(b""));
}

struct FailedProducer;
impl serde::Serialize for FailedProducer {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        use serde::ser::SerializeSeq as _;
        let mut output = serializer.serialize_seq(Some(2))?;
        output.serialize_element("partial")?;
        Err(serde::ser::Error::custom("producer failed"))
    }
}

#[test]
fn serialization_success_is_required_before_accepting_a_complete_artifact() {
    let mut published = b"previous artifact".to_vec();
    let mut staging = HashingWriter::new(Vec::new(), 100);
    let result = serde_json::to_writer(&mut staging, &FailedProducer);
    let (bytes, prefix) = staging.into_parts();
    if result.is_ok() {
        published.clone_from(&bytes);
    }
    assert!(result.is_err());
    assert_eq!(published, b"previous artifact");
    assert_ne!(bytes, [] as [u8; 0]);
    assert_eq!(prefix.bytes, bytes.len() as u64);
    assert_eq!(prefix.sha256, Sha256Digest::compute(&bytes));
}