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) => HeddleError::MissingObject {
58 object_type: "tree".to_string(),
59 id: hash.to_hex(),
60 },
61 }
62 }
63}
64
65impl std::fmt::Display for TreePathResolveError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 TreePathResolveError::Store { hash, source } => {
69 write!(f, "failed to load object {}: {source}", hash.short())
70 }
71 TreePathResolveError::SubtreeMissing(hash) => {
72 write!(f, "missing tree object: {hash} is not available locally")
73 }
74 }
75 }
76}
77
78pub fn split_path(path: &Path) -> Option<(&str, &Path)> {
80 let mut components = path.components();
81 let first = components.next()?;
82 let Component::Normal(name) = first else {
83 return None;
84 };
85 Some((name.to_str()?, components.as_path()))
86}
87
88pub fn resolve_tree_path<S: ObjectSource>(
95 store: &S,
96 root: &ContentHash,
97 path: &Path,
98 policy: LeafPolicy,
99) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
100 let Some(segments) = segments_for_policy(path, policy) else {
101 return Ok(None);
102 };
103 if segments.is_empty() {
104 return Ok(None);
105 }
106
107 let tree = load_subtree(store, root)?;
108 resolve_from_tree(store, &tree, &segments, policy)
109}
110
111#[cfg(feature = "async-source")]
112pub async fn resolve_tree_path_async<S: AsyncObjectSource + ?Sized>(
113 store: &S,
114 root: &ContentHash,
115 path: &Path,
116 policy: LeafPolicy,
117) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
118 let Some(segments) = segments_for_policy(path, policy) else {
119 return Ok(None);
120 };
121 if segments.is_empty() {
122 return Ok(None);
123 }
124
125 let tree = load_subtree_async(store, root).await?;
126 resolve_from_tree_async(store, &tree, &segments, policy).await
127}
128
129fn segments_for_policy(path: &Path, policy: LeafPolicy) -> Option<Vec<String>> {
130 match policy {
131 LeafPolicy::Entry => path_segments(path),
132 LeafPolicy::BlobOnly => {
133 let path_str = path.to_str()?;
134 Some(
135 path_str
136 .split('/')
137 .filter(|part| !part.is_empty())
138 .map(str::to_string)
139 .collect(),
140 )
141 }
142 LeafPolicy::LeafContentBlob => Some(
143 path.to_string_lossy()
144 .split('/')
145 .map(str::to_string)
146 .collect(),
147 ),
148 }
149}
150
151fn path_segments(path: &Path) -> Option<Vec<String>> {
152 if path.as_os_str().is_empty() {
153 return None;
154 }
155 let mut segments = Vec::new();
156 for component in path.components() {
157 match component {
158 Component::Normal(name) => segments.push(name.to_str()?.to_string()),
159 _ => return None,
160 }
161 }
162 if segments.is_empty() {
163 return None;
164 }
165 Some(segments)
166}
167
168fn resolve_from_tree<S: ObjectSource>(
169 store: &S,
170 tree: &Tree,
171 segments: &[String],
172 policy: LeafPolicy,
173) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
174 let mut current_tree = Cow::Borrowed(tree);
175
176 for (index, segment) in segments.iter().enumerate() {
177 let Some(entry) = current_tree.get(segment) else {
178 return Ok(None);
179 };
180
181 if index + 1 == segments.len() {
182 return resolve_leaf(store, entry.clone(), policy);
183 }
184
185 let Some(tree_hash) = entry.tree_hash() else {
186 return Ok(None);
187 };
188 let subtree = load_subtree(store, &tree_hash)?;
189 current_tree = Cow::Owned(subtree);
190 }
191
192 Ok(None)
193}
194
195#[cfg(feature = "async-source")]
196async fn resolve_from_tree_async<S: AsyncObjectSource + ?Sized>(
197 store: &S,
198 tree: &Tree,
199 segments: &[String],
200 policy: LeafPolicy,
201) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
202 let mut current_tree = Cow::Borrowed(tree);
203
204 for (index, segment) in segments.iter().enumerate() {
205 let Some(entry) = current_tree.get(segment) else {
206 return Ok(None);
207 };
208
209 if index + 1 == segments.len() {
210 return resolve_leaf_async(store, entry.clone(), policy).await;
211 }
212
213 let Some(tree_hash) = entry.tree_hash() else {
214 return Ok(None);
215 };
216 let subtree = load_subtree_async(store, &tree_hash).await?;
217 current_tree = Cow::Owned(subtree);
218 }
219
220 Ok(None)
221}
222
223fn resolve_leaf<S: ObjectSource>(
224 store: &S,
225 entry: TreeEntry,
226 policy: LeafPolicy,
227) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
228 match policy {
229 LeafPolicy::Entry => {
230 let content_hash = entry_content_hash(&entry);
231 Ok(Some(ResolvedTreeTarget {
232 entry,
233 content_hash,
234 blob: None,
235 }))
236 }
237 LeafPolicy::BlobOnly => {
238 let Some(content_hash) = entry.blob_hash() else {
239 return Ok(None);
240 };
241 Ok(Some(ResolvedTreeTarget {
242 entry,
243 content_hash: Some(content_hash),
244 blob: None,
245 }))
246 }
247 LeafPolicy::LeafContentBlob => {
248 let Some(content_hash) = entry.leaf_content_hash() else {
249 return Ok(None);
250 };
251 let blob = store.require_blob(&content_hash).map_err(|source| {
252 TreePathResolveError::Store {
253 hash: content_hash,
254 source: Box::new(source),
255 }
256 })?;
257 Ok(Some(ResolvedTreeTarget {
258 entry,
259 content_hash: Some(content_hash),
260 blob: Some(blob),
261 }))
262 }
263 }
264}
265
266#[cfg(feature = "async-source")]
267async fn resolve_leaf_async<S: AsyncObjectSource + ?Sized>(
268 store: &S,
269 entry: TreeEntry,
270 policy: LeafPolicy,
271) -> std::result::Result<Option<ResolvedTreeTarget>, TreePathResolveError> {
272 match policy {
273 LeafPolicy::Entry => {
274 let content_hash = entry_content_hash(&entry);
275 Ok(Some(ResolvedTreeTarget {
276 entry,
277 content_hash,
278 blob: None,
279 }))
280 }
281 LeafPolicy::BlobOnly => {
282 let Some(content_hash) = entry.blob_hash() else {
283 return Ok(None);
284 };
285 Ok(Some(ResolvedTreeTarget {
286 entry,
287 content_hash: Some(content_hash),
288 blob: None,
289 }))
290 }
291 LeafPolicy::LeafContentBlob => {
292 let Some(content_hash) = entry.leaf_content_hash() else {
293 return Ok(None);
294 };
295 let blob = store.require_blob(&content_hash).await.map_err(|source| {
296 TreePathResolveError::Store {
297 hash: content_hash,
298 source: Box::new(source),
299 }
300 })?;
301 Ok(Some(ResolvedTreeTarget {
302 entry,
303 content_hash: Some(content_hash),
304 blob: Some(blob),
305 }))
306 }
307 }
308}
309
310fn entry_content_hash(entry: &TreeEntry) -> Option<ContentHash> {
311 entry
312 .content_hash()
313 .or_else(|| entry.tree_hash())
314 .or_else(|| entry.leaf_content_hash())
315}
316
317fn load_subtree<S: ObjectSource>(
318 store: &S,
319 hash: &ContentHash,
320) -> std::result::Result<Tree, TreePathResolveError> {
321 match store.get_tree(hash) {
322 Ok(Some(tree)) => Ok(tree),
323 Ok(None) => Err(TreePathResolveError::SubtreeMissing(*hash)),
324 Err(source) => Err(TreePathResolveError::Store {
325 hash: *hash,
326 source: Box::new(source),
327 }),
328 }
329}
330
331#[cfg(feature = "async-source")]
332async fn load_subtree_async<S: AsyncObjectSource + ?Sized>(
333 store: &S,
334 hash: &ContentHash,
335) -> std::result::Result<Tree, TreePathResolveError> {
336 match store.get_tree(hash).await {
337 Ok(Some(tree)) => Ok(tree),
338 Ok(None) => Err(TreePathResolveError::SubtreeMissing(*hash)),
339 Err(source) => Err(TreePathResolveError::Store {
340 hash: *hash,
341 source: Box::new(source),
342 }),
343 }
344}