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