thin_fetch/object/
tree.rs1use 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 #[must_use]
18 pub fn is_dir(&self) -> bool {
19 self.mode == "40000"
20 }
21}
22
23impl TreeEntry {
24 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 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 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}