Skip to main content

thin_fetch/object/
tree.rs

1use std::io::Write;
2
3use ring::digest::Digest;
4
5use crate::utils::{as_hex, get_hash};
6use crate::{Error, HashKind, WithError};
7
8#[derive(PartialEq, Eq, Clone)]
9pub struct TreeEntry {
10    pub mode: String,
11    pub name: String,
12    pub hash: String,
13}
14
15impl TreeEntry {
16    /// Whether the entry represents a directory.
17    #[must_use]
18    pub fn is_dir(&self) -> bool {
19        self.mode == "40000"
20    }
21}
22
23impl TreeEntry {
24    /// # Errors
25    pub fn write_to<W: Write>(&self, mut writer: W) -> Result<(), Error> {
26        writer.write_all(self.mode.as_bytes())?;
27        writer.write_all(&[0x20])?;
28        writer.write_all(self.name.as_bytes())?;
29        writer.write_all(&[0x00])?;
30
31        let byte_size = self.hash.len() / 2;
32        let mut raw_hash: Vec<u8> = vec![0_u8; byte_size];
33
34        for (i, value) in raw_hash.iter_mut().enumerate() {
35            let segment = &self.hash[i * 2..(i + 1) * 2];
36            let byte = u8::from_str_radix(segment, 16).with_err_msg("non-hex hash")?;
37            *value = byte;
38        }
39
40        writer.write_all(&raw_hash)?;
41
42        Ok(())
43    }
44
45    #[must_use]
46    pub fn size(&self) -> u64 {
47        let size = self.mode.len() + self.name.len() + (self.hash.len() / 2) + 2;
48        size as u64
49    }
50}
51
52impl std::fmt::Debug for TreeEntry {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(
55            f,
56            "Entry(mode={}, name=\"{}\", hash={})",
57            self.mode, self.name, self.hash,
58        )
59    }
60}
61
62#[derive(Debug, Clone)]
63pub struct Tree {
64    pub hash: Digest,
65    pub entries: Vec<TreeEntry>,
66}
67
68impl Tree {
69    /// Parse tree object from raw data.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error when the data is misformatted or invalid.
74    pub fn parse(mut data: &[u8], hash_kind: HashKind) -> Result<Self, Error> {
75        let mut entries = Vec::new();
76
77        let hash = get_hash("tree", data, hash_kind);
78
79        loop {
80            if data.is_empty() {
81                break;
82            }
83
84            let spc = data
85                .iter()
86                .position(|b| *b == 0x20)
87                .with_err_msg("could not find space")?;
88            let nul = data
89                .iter()
90                .position(|b| *b == 0x00)
91                .with_err_msg("could not find nullbyte")?;
92
93            let mode = data.get(0..spc).with_err_msg("could not read entry mode")?;
94            let filename = data
95                .get(spc + 1..nul)
96                .with_err_msg("could not read entry name")?;
97
98            let hash_len = if hash_kind == HashKind::Sha256 {
99                32
100            } else {
101                20
102            };
103
104            let hash_start = nul + 1;
105            let hash_end = hash_start + hash_len;
106            let hash = data
107                .get(hash_start..hash_end)
108                .with_err_msg("could not read hash")?;
109
110            let mode = str::from_utf8(mode).with_err_msg("entry mode is not utf-8")?;
111            let filename = str::from_utf8(filename).with_err_msg("entry name is not utf-8")?;
112
113            entries.push(TreeEntry {
114                mode: mode.to_string(),
115                name: filename.to_string(),
116                hash: as_hex(hash),
117            });
118
119            data = &data[hash_end..];
120        }
121
122        Ok(Self { hash, entries })
123    }
124
125    #[must_use]
126    pub fn diff(&self, other: &Tree) -> Vec<&TreeEntry> {
127        self.entries
128            .iter()
129            .filter(|entry| !other.entries.contains(entry))
130            .collect()
131    }
132
133    #[must_use]
134    pub fn get_by_name(&self, needle: &str) -> Option<&TreeEntry> {
135        self.entries.iter().find(|entry| entry.name == needle)
136    }
137
138    /// # Errors
139    pub fn write_to<W: Write>(&self, mut writer: W) -> Result<(), Error> {
140        for entry in &self.entries {
141            entry
142                .write_to(&mut writer)
143                .with_err_msg("could not write entry")?;
144        }
145
146        Ok(())
147    }
148
149    pub fn size(&self) -> u64 {
150        self.entries.iter().map(TreeEntry::size).sum()
151    }
152}