heddle_object_model/compact/
tree.rs1use sley::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
4
5use super::{
6 Result, invalid,
7 io::{Reader, Writer, varint_len},
8};
9use crate::object::{ContentHash, EntryType, FileMode, SpoolId, StateId, Tree, TreeEntry};
10
11const TREE_MAGIC: &[u8; 4] = b"HCT1";
12
13pub fn is_tree_frame(bytes: &[u8]) -> bool {
15 bytes.starts_with(TREE_MAGIC)
16}
17
18pub fn encoded_tree_size(tree: &Tree) -> usize {
20 varint_len(tree.len())
21 + tree.len() * 2
22 + tree
23 .entries()
24 .iter()
25 .map(|entry| varint_len(entry.name().len()) + entry.name().len() + target_len(entry))
26 .sum::<usize>()
27}
28
29pub fn encode_tree_frame(trees: &[Tree]) -> Result<Vec<u8>> {
31 let mut output = Writer::new(TREE_MAGIC);
32 output.put_u64(trees.len() as u64);
33 for tree in trees {
34 tree.validate()?;
35 output.put_u64(tree.len() as u64);
36 for entry in tree.entries() {
37 output.put_u8(entry.mode().to_byte());
38 }
39 for entry in tree.entries() {
40 output.put_u8(entry.entry_type().to_byte());
41 }
42 for entry in tree.entries() {
43 output.put_bytes(entry.name().as_bytes());
44 }
45 for entry in tree.entries() {
46 encode_target(&mut output, entry);
47 }
48 }
49 Ok(output.finish())
50}
51
52pub fn decode_tree_frame(bytes: &[u8]) -> Result<Vec<Tree>> {
54 let mut input = Reader::verified(bytes, TREE_MAGIC)?;
55 let tree_count = input.get_count("tree frame")?;
56 let mut trees = Vec::with_capacity(tree_count);
57 for _ in 0..tree_count {
58 trees.push(decode_tree(&mut input)?);
59 }
60 input.finish()?;
61 Ok(trees)
62}
63
64fn decode_tree(input: &mut Reader<'_>) -> Result<Tree> {
65 let count = input.get_count("tree entry")?;
66 let modes = (0..count)
67 .map(|_| decode_mode(input.get_u8()?))
68 .collect::<Result<Vec<_>>>()?;
69 let kinds = (0..count)
70 .map(|_| decode_kind(input.get_u8()?))
71 .collect::<Result<Vec<_>>>()?;
72 let names = (0..count)
73 .map(|_| {
74 String::from_utf8(input.get_bytes()?).map_err(|_| invalid("tree name is not UTF-8"))
75 })
76 .collect::<Result<Vec<_>>>()?;
77 let mut entries = Vec::with_capacity(count);
78 for index in 0..count {
79 entries.push(decode_entry(
80 input,
81 names[index].clone(),
82 kinds[index],
83 modes[index],
84 )?);
85 }
86 let tree = Tree::from_entries(entries);
87 tree.validate()?;
88 Ok(tree)
89}
90
91fn encode_target(output: &mut Writer, entry: &TreeEntry) {
92 match entry.entry_type() {
93 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
94 output.put_fixed(entry.require_content_hash().as_bytes());
95 }
96 EntryType::Gitlink => {
97 let target = entry.gitlink_target().expect("gitlink target");
98 output.put_u8(git_format_tag(target.format()));
99 output.put_fixed(target.as_bytes());
100 }
101 EntryType::Spoollink => {
102 let (spool, state) = entry.spoollink_target().expect("spoollink target");
103 output.put_bytes(spool.as_str().as_bytes());
104 output.put_fixed(state.as_bytes());
105 }
106 }
107}
108
109fn decode_entry(
110 input: &mut Reader<'_>,
111 name: String,
112 kind: EntryType,
113 mode: FileMode,
114) -> Result<TreeEntry> {
115 let entry = match kind {
116 EntryType::Blob => TreeEntry::file(
117 name,
118 ContentHash::from_bytes(input.get_fixed()?),
119 mode == FileMode::Executable,
120 )?,
121 EntryType::Tree => TreeEntry::directory(name, ContentHash::from_bytes(input.get_fixed()?))?,
122 EntryType::Symlink => {
123 TreeEntry::symlink(name, ContentHash::from_bytes(input.get_fixed()?))?
124 }
125 EntryType::Gitlink => {
126 let format = decode_git_format(input.get_u8()?)?;
127 let oid_len = match format {
128 GitObjectFormat::Sha1 => 20,
129 GitObjectFormat::Sha256 => 32,
130 };
131 let target = GitObjectId::from_raw(format, input.take(oid_len)?)
132 .map_err(|error| super::CompactError::GitObjectId(error.to_string()))?;
133 TreeEntry::gitlink(name, target)?
134 }
135 EntryType::Spoollink => {
136 let spool = String::from_utf8(input.get_bytes()?)
137 .map_err(|_| invalid("spool id is not UTF-8"))?;
138 TreeEntry::spoollink(
139 name,
140 SpoolId::parse(spool)
141 .map_err(|error| super::CompactError::SpoolId(error.to_string()))?,
142 StateId::from_bytes(input.get_fixed()?),
143 )?
144 }
145 };
146 if entry.mode() != mode {
147 return Err(invalid(format!(
148 "tree kind/mode mismatch for {}: {kind:?}/{mode:?}",
149 entry.name()
150 )));
151 }
152 Ok(entry)
153}
154
155fn target_len(entry: &TreeEntry) -> usize {
156 match entry.entry_type() {
157 EntryType::Blob | EntryType::Tree | EntryType::Symlink => 32,
158 EntryType::Gitlink => {
159 1 + entry
160 .gitlink_target()
161 .expect("gitlink target")
162 .as_bytes()
163 .len()
164 }
165 EntryType::Spoollink => {
166 let (spool, _) = entry.spoollink_target().expect("spoollink target");
167 varint_len(spool.as_str().len()) + spool.as_str().len() + 32
168 }
169 }
170}
171
172fn decode_mode(value: u8) -> Result<FileMode> {
173 FileMode::from_byte(value).ok_or_else(|| invalid(format!("invalid tree mode {value}")))
174}
175
176fn decode_kind(value: u8) -> Result<EntryType> {
177 EntryType::from_byte(value).ok_or_else(|| invalid(format!("invalid tree kind {value}")))
178}
179
180fn git_format_tag(format: GitObjectFormat) -> u8 {
181 match format {
182 GitObjectFormat::Sha1 => 1,
183 GitObjectFormat::Sha256 => 2,
184 }
185}
186
187fn decode_git_format(value: u8) -> Result<GitObjectFormat> {
188 match value {
189 1 => Ok(GitObjectFormat::Sha1),
190 2 => Ok(GitObjectFormat::Sha256),
191 _ => Err(invalid(format!("invalid git object format {value}"))),
192 }
193}