thin-fetch 0.1.0

Low-level Git crate to fetch objects with high granularity
Documentation
use crate::error::{Error, WithError as _, error};

use super::{MARKER_END, MARKER_SEP};

#[derive(Debug, PartialEq, Eq)]
pub enum ParcelItem {
    Data(Vec<u8>),
    Separator,
    End,
}

pub struct ParcelDecoder<R: std::io::Read> {
    pub(crate) reader: R,
    finished: bool,
    pub(crate) ignore_end: bool,
}

impl<R: std::io::Read> ParcelDecoder<R> {
    pub fn new(reader: R) -> Self {
        Self {
            reader,
            finished: false,
            ignore_end: false,
        }
    }

    /// Get single raw parcel item from the response, including [`ParcelItem::End`].
    ///
    /// # Errors
    ///
    /// Returns an error when the length cannot be read/parsed, or when data cannot be read.
    pub fn get_one(&mut self) -> Result<ParcelItem, Error> {
        let mut len_buf: [u8; 4] = [0; 4];

        self.reader
            .read_exact(&mut len_buf)
            .with_err_msg("could not read length")?;

        if len_buf == MARKER_END {
            return Ok(ParcelItem::End);
        }

        if len_buf == MARKER_SEP {
            return Ok(ParcelItem::Separator);
        }

        let length_raw_str = str::from_utf8(&len_buf).with_err_msg("length is not valid utf-8")?;

        let length = usize::from_str_radix(length_raw_str, 16)
            .with_err(|| error!("length is not a valid number: {length_raw_str}"))?;

        let mut data = vec![0; length - 4];
        self.reader
            .read_exact(&mut data)
            .with_err_msg("could not read data")?;

        Ok(ParcelItem::Data(data))
    }
}

impl<R: std::io::Read> Iterator for ParcelDecoder<R> {
    type Item = Result<ParcelItem, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.finished {
            return None;
        }

        let item = self.get_one().with_err_msg("get next item");

        match item {
            Ok(ParcelItem::End) if !self.ignore_end => {
                self.finished = true;
                None
            }
            Err(err) if self.ignore_end && err.message.contains("failed to fill whole buffer") => {
                self.finished = true;
                None
            }
            res => Some(res),
        }
    }
}