ic-host-artifacts 0.4.5

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

#[test]
fn limits_are_inclusive_and_rejected_buffers_do_not_reach_the_sink() {
    let mut writer = BoundedWriter::new(Vec::new(), 3);
    let error = writer.write(b"four").unwrap_err();
    assert_eq!(
        error.get_ref().unwrap().downcast_ref::<WriterError>(),
        Some(&WriterError::LimitExceeded { limit: 3 })
    );
    assert_eq!(writer.bytes_written(), 0);
    writer.write_all(b"abc").unwrap();
    assert_eq!(writer.bytes_written(), 3);
    assert_eq!(writer.write(b"").unwrap(), 0);
    assert!(writer.write(b"d").is_err());
    assert!(writer.limit_exceeded());
    assert_eq!(writer.into_inner(), b"abc");

    let mut zero = BoundedWriter::new(Vec::new(), 0);
    zero.write_all(b"").unwrap();
    assert!(zero.write(b"a").is_err());
    assert_eq!(zero.into_inner(), [] as [u8; 0]);
}

struct ShortWriter {
    bytes: Vec<u8>,
    fail: bool,
    zero: bool,
    flushed: bool,
}

impl Write for ShortWriter {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        if self.fail {
            return Err(io::Error::from(io::ErrorKind::PermissionDenied));
        }
        if self.zero {
            return Ok(0);
        }
        let count = bytes.len().min(2);
        self.bytes.extend_from_slice(&bytes[..count]);
        Ok(count)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.flushed = true;
        Err(io::Error::from(io::ErrorKind::BrokenPipe))
    }
}

#[test]
fn short_writes_and_failures_preserve_the_accepted_prefix() {
    let sink = ShortWriter {
        bytes: Vec::new(),
        fail: false,
        zero: false,
        flushed: false,
    };
    let mut writer = BoundedWriter::new(sink, 6);
    writer.write_all(b"abcdef").unwrap();
    assert_eq!(writer.bytes_written(), 6);
    assert!(!writer.limit_exceeded());
    assert_eq!(
        writer.flush().unwrap_err().kind(),
        io::ErrorKind::BrokenPipe
    );
    let mut sink = writer.into_inner();
    assert!(sink.flushed);
    assert_eq!(sink.bytes, b"abcdef");
    sink.fail = true;
    let mut writer = BoundedWriter::new(sink, 9);
    assert_eq!(
        writer.write(b"xyz").unwrap_err().kind(),
        io::ErrorKind::PermissionDenied
    );
    assert_eq!(writer.bytes_written(), 0);
    assert!(!writer.limit_exceeded());
    let mut sink = writer.into_inner();
    sink.fail = false;
    sink.zero = true;
    let mut writer = BoundedWriter::new(sink, 9);
    assert_eq!(
        writer.write_all(b"xyz").unwrap_err().kind(),
        io::ErrorKind::WriteZero
    );
    assert_eq!(writer.bytes_written(), 0);
    assert_eq!(writer.into_inner().bytes, b"abcdef");
}

#[test]
fn recovering_the_sink_does_not_flush_or_discard_it() {
    let sink = ShortWriter {
        bytes: b"prefix".to_vec(),
        fail: false,
        zero: false,
        flushed: false,
    };
    let writer = BoundedWriter::new(sink, 10);
    assert_eq!(writer.bytes_written(), 0);
    let sink = writer.into_inner();
    assert!(!sink.flushed);
    assert_eq!(sink.bytes, b"prefix");
}

#[test]
fn invalid_sink_write_counts_are_typed_failures_without_count_overflow() {
    struct InvalidWriter;
    impl Write for InvalidWriter {
        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
            Ok(bytes.len() + 1)
        }
        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }
    let mut writer = BoundedWriter::new(InvalidWriter, 1);
    let error = writer.write(b"a").unwrap_err();
    assert_eq!(error.kind(), io::ErrorKind::InvalidData);
    assert_eq!(
        error.get_ref().unwrap().downcast_ref::<WriterError>(),
        Some(&WriterError::InvalidWriteCount {
            offered: 1,
            written: 2
        })
    );
    assert_eq!(writer.bytes_written(), 0);
    assert!(!writer.limit_exceeded());
}

#[test]
fn vectored_writes_remain_bounded_by_the_standard_write_contract() {
    let mut writer = BoundedWriter::new(Vec::new(), 3);
    let slices = [
        IoSlice::new(b""),
        IoSlice::new(b"abc"),
        IoSlice::new(b"def"),
    ];
    assert_eq!(writer.write_vectored(&slices).unwrap(), 3);
    assert!(writer.write_vectored(&[IoSlice::new(b"d")]).is_err());
    assert_eq!(writer.bytes_written(), 3);
    assert_eq!(writer.into_inner(), b"abc");
}

#[test]
fn serialized_json_can_be_counted_or_hashed_without_an_encoded_copy() {
    use crate::artifact::Sha256Digest;
    use sha2::{Digest, Sha256};

    let value = serde_json::json!({"rows": ["a", "b"], "schema_version": 1});
    let expected = br#"{"rows":["a","b"],"schema_version":1}"#;
    let limit = u64::try_from(expected.len()).unwrap();
    let mut counter = BoundedWriter::new(io::sink(), limit);
    serde_json::to_writer(&mut counter, &value).unwrap();
    assert_eq!(counter.bytes_written(), limit);

    let mut hasher = BoundedWriter::new(Sha256::new(), limit);
    serde_json::to_writer(&mut hasher, &value).unwrap();
    assert_eq!(hasher.bytes_written(), limit);
    assert_eq!(
        Sha256Digest::from_bytes(hasher.into_inner().finalize().into()),
        Sha256Digest::compute(expected)
    );

    let mut rejected = BoundedWriter::new(Vec::new(), limit - 1);
    let error = serde_json::to_writer(&mut rejected, &value).unwrap_err();
    assert_eq!(error.io_error_kind(), Some(io::ErrorKind::Other));
    assert!(rejected.limit_exceeded());
    let count = rejected.bytes_written();
    let partial = rejected.into_inner();
    assert_eq!(u64::try_from(partial.len()).unwrap(), count);
    assert!(expected.starts_with(&partial));
}