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 {
#[must_use]
pub fn is_dir(&self) -> bool {
self.mode == "40000"
}
}
impl TreeEntry {
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 {
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)
}
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()
}
}