heddle_object_model/object/
tree_path.rs1use std::{
5 borrow::Cow,
6 path::{Component, Path},
7};
8
9#[cfg(feature = "async-source")]
10use super::AsyncObjectSource;
11use super::{Blob, ContentHash, ObjectSource, Tree, TreeEntry};
12use crate::error::HeddleError;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum LeafPolicy {
17 Entry,
19 BlobOnly,
21 LeafContentBlob,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ResolvedTreeTarget {
29 pub entry: TreeEntry,
30 pub content_hash: Option<ContentHash>,
31 pub blob: Option<Blob>,
32}
33
34#[derive(Debug)]
36pub enum TreePathResolveError {
37 Store {
38 hash: ContentHash,
39 source: Box<HeddleError>,
40 },
41 SubtreeMissing(ContentHash),
42}
43
44impl std::error::Error for TreePathResolveError {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 TreePathResolveError::Store { source, .. } => Some(source.as_ref()),
48 TreePathResolveError::SubtreeMissing(_) => None,
49 }
50 }
51}
52
53impl From<TreePathResolveError> for HeddleError {
54 fn from(value: TreePathResolveError) -> Self {
55 match value {
56 TreePathResolveError::Store { source, .. } => *source,
57 TreePathResolveError::SubtreeMissing(hash) => {
58 HeddleError::InvalidObject(format!("subtree {} missing from store", hash.short()))
59 }
60 }
61 }
62}
63
64impl std::fmt::Display for TreePathResolveError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 TreePathResolveError::Store { hash, .. } => {
68 write!(f, "failed to load tree {}", hash.short())
69 }
70 TreePathResolveError::SubtreeMissing(hash) => {
71 write!(f, "subtree {} missing from store", hash.short())
72 }
73 }
74 }
75}
76
77pub fn split_path(path: &Path) -> Option<(&str, &Path)> {
79 let mut components = path.components();
80 let first = components.next()?;
81 let Component::Normal(name) = first else {
82 return None;
83 };
84 Some((name.to_str()?, components.as_path()))
85}
86
87pub fn resolve_tree_path<S: ObjectSource>(
94 store: &S,
95 root: &ContentHash,
96 path: &Path,
97 policy: LeafPolicy,
98) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
99 let Some(segments) = segments_for_policy(path, policy) else {
100 return Ok(None);
101 };
102 if segments.is_empty() {
103 return Ok(None);
104 }
105
106 let Some(tree) = load_subtree(store, root, policy)? else {
107 return Ok(None);
108 };
109 resolve_from_tree(store, &tree, &segments, policy)
110}
111
112#[cfg(feature = "async-source")]
113pub async fn resolve_tree_path_async<S: AsyncObjectSource + ?Sized>(
114 store: &S,
115 root: &ContentHash,
116 path: &Path,
117 policy: LeafPolicy,
118) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
119 let Some(segments) = segments_for_policy(path, policy) else {
120 return Ok(None);
121 };
122 if segments.is_empty() {
123 return Ok(None);
124 }
125
126 let Some(tree) = load_subtree_async(store, root, policy).await? else {
127 return Ok(None);
128 };
129 resolve_from_tree_async(store, &tree, &segments, policy).await
130}
131
132fn segments_for_policy(path: &Path, policy: LeafPolicy) -> Option<Vec<String>> {
133 match policy {
134 LeafPolicy::Entry => path_segments(path),
135 LeafPolicy::BlobOnly => {
136 let path_str = path.to_str()?;
137 Some(
138 path_str
139 .split('/')
140 .filter(|part| !part.is_empty())
141 .map(str::to_string)
142 .collect(),
143 )
144 }
145 LeafPolicy::LeafContentBlob => Some(
146 path.to_string_lossy()
147 .split('/')
148 .map(str::to_string)
149 .collect(),
150 ),
151 }
152}
153
154fn path_segments(path: &Path) -> Option<Vec<String>> {
155 if path.as_os_str().is_empty() {
156 return None;
157 }
158 let mut segments = Vec::new();
159 for component in path.components() {
160 match component {
161 Component::Normal(name) => segments.push(name.to_str()?.to_string()),
162 _ => return None,
163 }
164 }
165 if segments.is_empty() {
166 return None;
167 }
168 Some(segments)
169}
170
171fn resolve_from_tree<S: ObjectSource>(
172 store: &S,
173 tree: &Tree,
174 segments: &[String],
175 policy: LeafPolicy,
176) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
177 let mut current_tree = Cow::Borrowed(tree);
178
179 for (index, segment) in segments.iter().enumerate() {
180 let Some(entry) = current_tree.get(segment) else {
181 return Ok(None);
182 };
183
184 if index + 1 == segments.len() {
185 return resolve_leaf(store, entry.clone(), policy);
186 }
187
188 let Some(tree_hash) = entry.tree_hash() else {
189 return Ok(None);
190 };
191 let Some(subtree) = load_subtree(store, &tree_hash, policy)? else {
192 return Ok(None);
193 };
194 current_tree = Cow::Owned(subtree);
195 }
196
197 Ok(None)
198}
199
200#[cfg(feature = "async-source")]
201async fn resolve_from_tree_async<S: AsyncObjectSource + ?Sized>(
202 store: &S,
203 tree: &Tree,
204 segments: &[String],
205 policy: LeafPolicy,
206) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
207 let mut current_tree = Cow::Borrowed(tree);
208
209 for (index, segment) in segments.iter().enumerate() {
210 let Some(entry) = current_tree.get(segment) else {
211 return Ok(None);
212 };
213
214 if index + 1 == segments.len() {
215 return resolve_leaf_async(store, entry.clone(), policy).await;
216 }
217
218 let Some(tree_hash) = entry.tree_hash() else {
219 return Ok(None);
220 };
221 let Some(subtree) = load_subtree_async(store, &tree_hash, policy).await? else {
222 return Ok(None);
223 };
224 current_tree = Cow::Owned(subtree);
225 }
226
227 Ok(None)
228}
229
230fn resolve_leaf<S: ObjectSource>(
231 store: &S,
232 entry: TreeEntry,
233 policy: LeafPolicy,
234) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
235 match policy {
236 LeafPolicy::Entry => {
237 let content_hash = entry_content_hash(&entry);
238 Ok(Some(ResolvedTreeTarget {
239 entry,
240 content_hash,
241 blob: None,
242 }))
243 }
244 LeafPolicy::BlobOnly => {
245 let Some(content_hash) = entry.blob_hash() else {
246 return Ok(None);
247 };
248 Ok(Some(ResolvedTreeTarget {
249 entry,
250 content_hash: Some(content_hash),
251 blob: None,
252 }))
253 }
254 LeafPolicy::LeafContentBlob => {
255 let Some(content_hash) = entry.leaf_content_hash() else {
256 return Ok(None);
257 };
258 let blob = match store.get_blob(&content_hash) {
259 Ok(Some(blob)) => Some(blob),
260 Ok(None) => None,
261 Err(source) => {
262 return Err(TreePathResolveError::Store {
263 hash: content_hash,
264 source: Box::new(source),
265 });
266 }
267 };
268 Ok(blob.map(|blob| ResolvedTreeTarget {
269 entry,
270 content_hash: Some(content_hash),
271 blob: Some(blob),
272 }))
273 }
274 }
275}
276
277#[cfg(feature = "async-source")]
278async fn resolve_leaf_async<S: AsyncObjectSource + ?Sized>(
279 store: &S,
280 entry: TreeEntry,
281 policy: LeafPolicy,
282) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
283 match policy {
284 LeafPolicy::Entry => {
285 let content_hash = entry_content_hash(&entry);
286 Ok(Some(ResolvedTreeTarget {
287 entry,
288 content_hash,
289 blob: None,
290 }))
291 }
292 LeafPolicy::BlobOnly => {
293 let Some(content_hash) = entry.blob_hash() else {
294 return Ok(None);
295 };
296 Ok(Some(ResolvedTreeTarget {
297 entry,
298 content_hash: Some(content_hash),
299 blob: None,
300 }))
301 }
302 LeafPolicy::LeafContentBlob => {
303 let Some(content_hash) = entry.leaf_content_hash() else {
304 return Ok(None);
305 };
306 let blob = match store.get_blob(&content_hash).await {
307 Ok(Some(blob)) => Some(blob),
308 Ok(None) => None,
309 Err(source) => {
310 return Err(TreePathResolveError::Store {
311 hash: content_hash,
312 source: Box::new(source),
313 });
314 }
315 };
316 Ok(blob.map(|blob| ResolvedTreeTarget {
317 entry,
318 content_hash: Some(content_hash),
319 blob: Some(blob),
320 }))
321 }
322 }
323}
324
325fn entry_content_hash(entry: &TreeEntry) -> Option<ContentHash> {
326 entry
327 .content_hash()
328 .or_else(|| entry.tree_hash())
329 .or_else(|| entry.leaf_content_hash())
330}
331
332fn load_subtree<S: ObjectSource>(
333 store: &S,
334 hash: &ContentHash,
335 policy: LeafPolicy,
336) -> std::result::Result<Option<Tree>, TreePathResolveError> {
337 match policy {
338 LeafPolicy::Entry => Ok(store.get_tree(hash).ok().flatten()),
339 LeafPolicy::LeafContentBlob => match store.get_tree(hash) {
340 Ok(tree) => Ok(tree),
341 Err(source) => Err(TreePathResolveError::Store {
342 hash: *hash,
343 source: Box::new(source),
344 }),
345 },
346 LeafPolicy::BlobOnly => match store.get_tree(hash) {
347 Ok(Some(tree)) => Ok(Some(tree)),
348 Ok(None) => Err(TreePathResolveError::SubtreeMissing(*hash)),
349 Err(source) => Err(TreePathResolveError::Store {
350 hash: *hash,
351 source: Box::new(source),
352 }),
353 },
354 }
355}
356
357#[cfg(feature = "async-source")]
358async fn load_subtree_async<S: AsyncObjectSource + ?Sized>(
359 store: &S,
360 hash: &ContentHash,
361 policy: LeafPolicy,
362) -> std::result::Result<Option<Tree>, TreePathResolveError> {
363 match policy {
364 LeafPolicy::Entry => Ok(store.get_tree(hash).await.ok().flatten()),
365 LeafPolicy::LeafContentBlob => match store.get_tree(hash).await {
366 Ok(tree) => Ok(tree),
367 Err(source) => Err(TreePathResolveError::Store {
368 hash: *hash,
369 source: Box::new(source),
370 }),
371 },
372 LeafPolicy::BlobOnly => match store.get_tree(hash).await {
373 Ok(Some(tree)) => Ok(Some(tree)),
374 Ok(None) => Err(TreePathResolveError::SubtreeMissing(*hash)),
375 Err(source) => Err(TreePathResolveError::Store {
376 hash: *hash,
377 source: Box::new(source),
378 }),
379 },
380 }
381}