thin-fetch 0.1.0

Low-level Git crate to fetch objects with high granularity
Documentation
use std::io::Write;

use ring::digest::Digest;

use crate::utils::{as_hex, get_hash};
use crate::{Error, HashKind, WithError};

#[derive(PartialEq, Eq, Clone)]
pub struct TreeEntry {
    pub mode: String,
    pub name: String,
    pub hash: String,
}

impl TreeEntry {
    /// Whether the entry represents a directory.
    #[must_use]
    pub fn is_dir(&self) -> bool {
        self.mode == "40000"
    }
}

impl TreeEntry {
    /// # Errors
    pub fn write_to<W: Write>(&self, mut writer: W) -> Result<(), Error> {
        writer.write_all(self.mode.as_bytes())?;
        writer.write_all(&[0x20])?;
        writer.write_all(self.name.as_bytes())?;
        writer.write_all(&[0x00])?;

        let byte_size = self.hash.len() / 2;
        let mut raw_hash: Vec<u8> = vec![0_u8; byte_size];

        for (i, value) in raw_hash.iter_mut().enumerate() {
            let segment = &self.hash[i * 2..(i + 1) * 2];
            let byte = u8::from_str_radix(segment, 16).with_err_msg("non-hex hash")?;
            *value = byte;
        }

        writer.write_all(&raw_hash)?;

        Ok(())
    }

    #[must_use]
    pub fn size(&self) -> u64 {
        let size = self.mode.len() + self.name.len() + (self.hash.len() / 2) + 2;
        size as u64
    }
}

impl std::fmt::Debug for TreeEntry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Entry(mode={}, name=\"{}\", hash={})",
            self.mode, self.name, self.hash,
        )
    }
}

#[derive(Debug, Clone)]
pub struct Tree {
    pub hash: Digest,
    pub entries: Vec<TreeEntry>,
}

impl Tree {
    /// Parse tree object from raw data.
    ///
    /// # Errors
    ///
    /// Returns an error when the data is misformatted or invalid.
    pub fn parse(mut data: &[u8], hash_kind: HashKind) -> Result<Self, Error> {
        let mut entries = Vec::new();

        let hash = get_hash("tree", data, hash_kind);

        loop {
            if data.is_empty() {
                break;
            }

            let spc = data
                .iter()
                .position(|b| *b == 0x20)
                .with_err_msg("could not find space")?;
            let nul = data
                .iter()
                .position(|b| *b == 0x00)
                .with_err_msg("could not find nullbyte")?;

            let mode = data.get(0..spc).with_err_msg("could not read entry mode")?;
            let filename = data
                .get(spc + 1..nul)
                .with_err_msg("could not read entry name")?;

            let hash_len = if hash_kind == HashKind::Sha256 {
                32
            } else {
                20
            };

            let hash_start = nul + 1;
            let hash_end = hash_start + hash_len;
            let hash = data
                .get(hash_start..hash_end)
                .with_err_msg("could not read hash")?;

            let mode = str::from_utf8(mode).with_err_msg("entry mode is not utf-8")?;
            let filename = str::from_utf8(filename).with_err_msg("entry name is not utf-8")?;

            entries.push(TreeEntry {
                mode: mode.to_string(),
                name: filename.to_string(),
                hash: as_hex(hash),
            });

            data = &data[hash_end..];
        }

        Ok(Self { hash, entries })
    }

    #[must_use]
    pub fn diff(&self, other: &Tree) -> Vec<&TreeEntry> {
        self.entries
            .iter()
            .filter(|entry| !other.entries.contains(entry))
            .collect()
    }

    #[must_use]
    pub fn get_by_name(&self, needle: &str) -> Option<&TreeEntry> {
        self.entries.iter().find(|entry| entry.name == needle)
    }

    /// # Errors
    pub fn write_to<W: Write>(&self, mut writer: W) -> Result<(), Error> {
        for entry in &self.entries {
            entry
                .write_to(&mut writer)
                .with_err_msg("could not write entry")?;
        }

        Ok(())
    }

    pub fn size(&self) -> u64 {
        self.entries.iter().map(TreeEntry::size).sum()
    }
}