girt 0.6.1

An incremental, idiomatic Rust implementation of Git
Documentation
//! Owned transfer artifacts. Disk-backed values remove files on drop and open independent cursors.
use std::io::{Read, Seek, SeekFrom, Write};

use super::{FetchError, check_cancelled};

#[derive(Debug)]
pub(crate) enum Bytes {
    Memory(Vec<u8>),
    File(tempfile::NamedTempFile, usize),
}

impl Bytes {
    pub(crate) fn temporary() -> Result<Self, FetchError> {
        Ok(Self::File(tempfile::NamedTempFile::new()?, 0))
    }

    pub(crate) fn path(&self) -> Option<&std::path::Path> {
        match self {
            Self::File(file, _) => Some(file.path()),
            Self::Memory(_) => None,
        }
    }

    pub(crate) fn len(&self) -> usize {
        match self {
            Self::Memory(bytes) => bytes.len(),
            Self::File(_, len) => *len,
        }
    }

    pub(crate) fn reader(&self) -> Result<Box<dyn Read + Send + '_>, FetchError> {
        match self {
            Self::Memory(bytes) => Ok(Box::new(bytes.as_slice())),
            Self::File(file, _) => Ok(Box::new(std::io::BufReader::new(file.reopen()?))),
        }
    }

    pub(crate) fn is_empty(&self) -> bool {
        self.len() == 0
    }

    #[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
    pub(crate) fn async_reader(
        &self,
    ) -> std::io::Result<Box<dyn tokio::io::AsyncRead + Unpin + Send + '_>> {
        match self {
            Self::Memory(bytes) => Ok(Box::new(std::io::Cursor::new(bytes.as_slice()))),
            Self::File(file, _) => Ok(Box::new(tokio::fs::File::from_std(file.reopen()?))),
        }
    }

    pub(crate) fn copy_to(
        &self,
        output: &mut impl Write,
        cancel: &std::sync::atomic::AtomicBool,
    ) -> Result<(), FetchError> {
        let mut reader = self.reader()?;
        let mut buffer = [0; 64 * 1024];
        loop {
            check_cancelled(cancel)?;
            let count = reader.read(&mut buffer)?;
            if count == 0 {
                return Ok(());
            }
            output.write_all(&buffer[..count])?;
        }
    }

    pub(crate) fn append(&mut self, bytes: &[u8]) -> Result<(), FetchError> {
        match self {
            Self::Memory(data) => data.extend_from_slice(bytes),
            Self::File(file, len) => {
                file.as_file_mut().seek(SeekFrom::Start(*len as u64))?;
                file.write_all(bytes)?;
                *len += bytes.len();
            }
        }
        Ok(())
    }

    #[cfg(test)]
    pub(crate) fn to_vec(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        self.reader().unwrap().read_to_end(&mut bytes).unwrap();
        bytes
    }
}

impl From<Vec<u8>> for Bytes {
    fn from(bytes: Vec<u8>) -> Self {
        Self::Memory(bytes)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::AtomicBool;

    use super::*;

    #[test]
    fn readers_have_independent_cursors_and_drop_removes_the_file() {
        let mut bytes = Bytes::temporary().unwrap();
        bytes.append(b"payload").unwrap();
        let path = bytes.path().unwrap().to_owned();
        let mut first = bytes.reader().unwrap();
        let mut prefix = [0; 3];
        first.read_exact(&mut prefix).unwrap();
        assert_eq!(&prefix, b"pay");
        assert_eq!(bytes.to_vec(), b"payload");
        drop(first);
        drop(bytes);
        assert!(!path.exists());
    }

    #[test]
    fn cancelled_installation_copy_writes_nothing() {
        let mut bytes = Bytes::temporary().unwrap();
        bytes.append(b"payload").unwrap();
        let mut output = Vec::new();
        assert!(matches!(
            bytes.copy_to(&mut output, &AtomicBool::new(true)),
            Err(FetchError::Cancelled)
        ));
        assert!(output.is_empty());
    }
}