1use std::collections::HashSet;
8
9use crate::error::{Error, Result};
10use crate::objects::{tag_object_line_oid, CommitRef, ObjectId, ObjectKind};
11
12use super::Odb;
13
14impl Odb {
15 pub fn peel_tags(&self, oid: ObjectId) -> Result<(ObjectId, ObjectKind)> {
38 let mut current = oid;
39 let mut visited: Option<HashSet<ObjectId>> = None;
40 loop {
41 let kind = self.read_info(¤t)?.kind;
42 if kind != ObjectKind::Tag {
43 return Ok((current, kind));
44 }
45 if !visited.get_or_insert_with(HashSet::new).insert(current) {
46 return Err(Error::CorruptObject(format!(
47 "tag chain from {oid} loops back to {current}"
48 )));
49 }
50 let tag = self.read(¤t)?;
51 current = tag_object_line_oid(&tag.data).ok_or_else(|| {
52 Error::CorruptObject(format!("tag {current} has no valid object header"))
53 })?;
54 }
55 }
56
57 pub fn peel_to_commit(&self, oid: ObjectId) -> Result<Option<ObjectId>> {
72 let (peeled, kind) = self.peel_tags(oid)?;
73 Ok((kind == ObjectKind::Commit).then_some(peeled))
74 }
75
76 pub(crate) fn peel_tag_target(&self, oid: ObjectId) -> Result<Option<ObjectId>> {
90 self.peel_tag_target_from(oid, false)
91 }
92
93 pub(crate) fn peel_stored_tag_target(&self, oid: ObjectId) -> Result<Option<ObjectId>> {
105 self.peel_tag_target_from(oid, true)
106 }
107
108 fn peel_tag_target_from(&self, oid: ObjectId, stored_only: bool) -> Result<Option<ObjectId>> {
109 if stored_only && !self.contains(&oid)? {
110 return Ok(None);
111 }
112 if self.read_info(&oid)?.kind != ObjectKind::Tag {
113 return Ok(None);
114 }
115 let mut current = oid;
116 let mut visited: Option<HashSet<ObjectId>> = None;
117 loop {
118 if stored_only && !self.contains(¤t)? {
119 return Ok(None);
120 }
121 let tag = self.read(¤t)?;
122 if tag.kind != ObjectKind::Tag {
123 return Err(Error::CorruptObject(format!(
124 "{current} is named as a tag but is a {}",
125 tag.kind
126 )));
127 }
128 let (target, names_tag) = tag_target_header(&tag.data);
129 let target = target.ok_or_else(|| {
130 Error::CorruptObject(format!("tag {current} has no valid object header"))
131 })?;
132 if !names_tag {
133 return Ok(Some(target));
134 }
135 if !visited.get_or_insert_with(HashSet::new).insert(current) {
136 return Err(Error::CorruptObject(format!(
137 "tag chain from {oid} loops back to {current}"
138 )));
139 }
140 current = target;
141 }
142 }
143
144 pub(crate) fn peel_to_tree(&self, oid: ObjectId) -> Result<Option<ObjectId>> {
149 match self.peel_tags(oid)? {
150 (tree, ObjectKind::Tree) => Ok(Some(tree)),
151 (commit, ObjectKind::Commit) => {
152 let obj = self.read(&commit)?;
153 Ok(Some(CommitRef::parse(&obj.data)?.tree()))
154 }
155 _ => Ok(None),
156 }
157 }
158}
159
160fn tag_target_header(data: &[u8]) -> (Option<ObjectId>, bool) {
163 let mut target: Option<Option<ObjectId>> = None;
164 let mut names_tag = None;
165 for line in data.split(|&b| b == b'\n') {
166 if line.is_empty() || (target.is_some() && names_tag.is_some()) {
167 break;
168 }
169 if let Some(hex) = line.strip_prefix(b"object ") {
170 target.get_or_insert_with(|| {
171 std::str::from_utf8(hex)
172 .ok()
173 .and_then(|s| s.trim().parse().ok())
174 });
175 } else if let Some(kind) = line.strip_prefix(b"type ") {
176 names_tag.get_or_insert(kind.trim_ascii() == b"tag");
177 }
178 }
179 (target.flatten(), names_tag.unwrap_or(false))
180}
181
182#[cfg(test)]
183mod tests {
184 #![allow(clippy::expect_used, clippy::unwrap_used)]
185
186 use super::*;
187 use crate::objects::HashAlgo;
188 use crate::odb::{MemoryStore, OdbBuilder};
189 use std::sync::Arc;
190 use tempfile::TempDir;
191
192 fn tag(odb: &Odb, target: ObjectId, kind: &str, name: &str) -> ObjectId {
193 let body = format!(
194 "object {target}\ntype {kind}\ntag {name}\ntagger T <t@example.com> 0 +0000\n\nmsg\n"
195 );
196 odb.write(ObjectKind::Tag, body.as_bytes()).unwrap()
197 }
198
199 fn commit(odb: &Odb) -> ObjectId {
200 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
201 let body = format!(
202 "tree {tree}\nauthor A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nc\n"
203 );
204 odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
205 }
206
207 #[test]
208 fn non_tag_is_returned_unchanged() {
209 let dir = TempDir::new().unwrap();
210 let odb = Odb::new(dir.path());
211 let c = commit(&odb);
212 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
213 assert_eq!(odb.peel_tags(c).unwrap(), (c, ObjectKind::Commit));
214 assert_eq!(odb.peel_tags(blob).unwrap(), (blob, ObjectKind::Blob));
215 assert_eq!(odb.peel_to_commit(c).unwrap(), Some(c));
216 assert_eq!(odb.peel_to_commit(blob).unwrap(), None);
217 }
218
219 #[test]
220 fn nested_tags_peel_to_the_innermost_object() {
221 let dir = TempDir::new().unwrap();
222 let odb = Odb::new(dir.path());
223 let c = commit(&odb);
224 let inner = tag(&odb, c, "commit", "inner");
225 let outer = tag(&odb, inner, "tag", "outer");
226 assert_eq!(odb.peel_tags(outer).unwrap(), (c, ObjectKind::Commit));
227 assert_eq!(odb.peel_to_commit(outer).unwrap(), Some(c));
228
229 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
230 let tag_of_blob = tag(&odb, blob, "blob", "b");
231 assert_eq!(
232 odb.peel_tags(tag_of_blob).unwrap(),
233 (blob, ObjectKind::Blob)
234 );
235 assert_eq!(odb.peel_to_commit(tag_of_blob).unwrap(), None);
236 }
237
238 #[test]
239 fn missing_objects_are_errors() {
240 let dir = TempDir::new().unwrap();
241 let odb = Odb::new(dir.path());
242 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
243 assert!(matches!(
244 odb.peel_tags(missing),
245 Err(Error::ObjectNotFound(_))
246 ));
247 let dangling = tag(&odb, missing, "commit", "dangling");
248 assert!(matches!(
249 odb.peel_to_commit(dangling),
250 Err(Error::ObjectNotFound(_))
251 ));
252 }
253
254 #[test]
255 fn only_the_object_header_is_required() {
256 let dir = TempDir::new().unwrap();
257 let odb = Odb::new(dir.path());
258 let c = commit(&odb);
259 let mut body = format!("object {c}\ntype commit\n\n").into_bytes();
260 body.extend_from_slice(b"latin-1 message \xe9\n");
261 let bare = odb.write(ObjectKind::Tag, &body).unwrap();
262 assert_eq!(odb.peel_to_commit(bare).unwrap(), Some(c));
263 }
264
265 #[test]
266 fn unparsable_tag_is_corrupt() {
267 let dir = TempDir::new().unwrap();
268 let odb = Odb::new(dir.path());
269 let bad = odb.write(ObjectKind::Tag, b"not a tag\n").unwrap();
270 assert!(matches!(odb.peel_tags(bad), Err(Error::CorruptObject(_))));
271 }
272
273 #[test]
274 fn long_chains_peel_without_a_depth_limit() {
275 let dir = TempDir::new().unwrap();
276 let odb = Odb::new(dir.path());
277 let c = commit(&odb);
278 let mut tip = tag(&odb, c, "commit", "t0");
279 for i in 1..200 {
280 tip = tag(&odb, tip, "tag", &format!("t{i}"));
281 }
282 assert_eq!(odb.peel_to_commit(tip).unwrap(), Some(c));
283 }
284
285 #[test]
286 fn peel_tag_target_names_the_innermost_object_of_tags_only() {
287 let dir = TempDir::new().unwrap();
288 let odb = Odb::new(dir.path());
289 let c = commit(&odb);
290 let inner = tag(&odb, c, "commit", "inner");
291 let outer = tag(&odb, inner, "tag", "outer");
292 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
293 let tag_of_blob = tag(&odb, blob, "blob", "b");
294 assert_eq!(odb.peel_tag_target(c).unwrap(), None);
295 assert_eq!(odb.peel_tag_target(blob).unwrap(), None);
296 assert_eq!(odb.peel_tag_target(inner).unwrap(), Some(c));
297 assert_eq!(odb.peel_tag_target(outer).unwrap(), Some(c));
298 assert_eq!(odb.peel_tag_target(tag_of_blob).unwrap(), Some(blob));
299
300 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
301 let dangling = tag(&odb, missing, "commit", "dangling");
302 assert_eq!(odb.peel_tag_target(dangling).unwrap(), Some(missing));
303 let dangling_outer = tag(&odb, missing, "tag", "dangling-outer");
304 assert!(matches!(
305 odb.peel_tag_target(dangling_outer),
306 Err(Error::ObjectNotFound(_))
307 ));
308 assert!(matches!(
309 odb.peel_tag_target(missing),
310 Err(Error::ObjectNotFound(_))
311 ));
312 }
313
314 #[test]
315 fn peel_stored_tag_target_reads_only_stored_tags() {
316 let dir = TempDir::new().unwrap();
317 let odb = Odb::new(dir.path());
318 let c = commit(&odb);
319 let inner = tag(&odb, c, "commit", "inner");
320 let outer = tag(&odb, inner, "tag", "outer");
321 assert_eq!(odb.peel_stored_tag_target(outer).unwrap(), Some(c));
322 assert_eq!(odb.peel_stored_tag_target(c).unwrap(), None);
323
324 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
325 let on_missing_blob = tag(&odb, missing, "blob", "promised");
326 assert_eq!(
327 odb.peel_stored_tag_target(on_missing_blob).unwrap(),
328 Some(missing),
329 "the final target comes from the header and may be missing"
330 );
331 let on_missing_tag = tag(&odb, missing, "tag", "broken-chain");
332 assert_eq!(odb.peel_stored_tag_target(on_missing_tag).unwrap(), None);
333 assert_eq!(odb.peel_stored_tag_target(missing).unwrap(), None);
334 }
335
336 #[test]
337 fn peel_tag_target_rejects_a_type_tag_header_naming_a_non_tag() {
338 let dir = TempDir::new().unwrap();
339 let odb = Odb::new(dir.path());
340 let c = commit(&odb);
341 let lying = tag(&odb, c, "tag", "lying");
342 assert!(matches!(
343 odb.peel_tag_target(lying),
344 Err(Error::CorruptObject(_))
345 ));
346 }
347
348 #[test]
349 fn peel_tag_target_detects_a_tag_cycle() {
350 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
351 let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
352 let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
353 for (from, to) in [(a, b), (b, a)] {
354 let body = format!("object {to}\ntype tag\ntag cycle\n\n");
355 store.insert_object(from, ObjectKind::Tag, Arc::from(body.into_bytes()));
356 }
357 let dir = TempDir::new().unwrap();
358 let odb = OdbBuilder::files(dir.path())
359 .primary(store)
360 .alternates(false)
361 .build();
362 assert!(matches!(
363 odb.peel_tag_target(a),
364 Err(Error::CorruptObject(_))
365 ));
366 }
367
368 #[test]
369 fn peel_to_tree_follows_tags_and_commits() {
370 let dir = TempDir::new().unwrap();
371 let odb = Odb::new(dir.path());
372 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
373 let c = commit(&odb);
374 let outer = tag(&odb, tag(&odb, c, "commit", "inner"), "tag", "outer");
375 let tag_of_tree = tag(&odb, tree, "tree", "t");
376 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
377 let tag_of_blob = tag(&odb, blob, "blob", "b");
378 assert_eq!(odb.peel_to_tree(tree).unwrap(), Some(tree));
379 assert_eq!(odb.peel_to_tree(c).unwrap(), Some(tree));
380 assert_eq!(odb.peel_to_tree(outer).unwrap(), Some(tree));
381 assert_eq!(odb.peel_to_tree(tag_of_tree).unwrap(), Some(tree));
382 assert_eq!(odb.peel_to_tree(blob).unwrap(), None);
383 assert_eq!(odb.peel_to_tree(tag_of_blob).unwrap(), None);
384 }
385
386 #[test]
387 fn peel_to_tree_rejects_a_commit_without_a_tree_header() {
388 let dir = TempDir::new().unwrap();
389 let odb = Odb::new(dir.path());
390 let bad = odb
391 .write(
392 ObjectKind::Commit,
393 b"author A <a@example.com> 0 +0000\n\nno tree\n",
394 )
395 .unwrap();
396 assert!(matches!(
397 odb.peel_to_tree(bad),
398 Err(Error::CorruptObject(_))
399 ));
400 }
401
402 #[test]
403 fn a_store_serving_a_tag_cycle_is_corrupt() {
404 let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
405 let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
406 let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
407 for (from, to) in [(a, b), (b, a)] {
408 let body = format!("object {to}\ntype tag\ntag cycle\n\n");
409 store.insert_object(from, ObjectKind::Tag, Arc::from(body.into_bytes()));
410 }
411 let dir = TempDir::new().unwrap();
412 let odb = OdbBuilder::files(dir.path())
413 .primary(store)
414 .alternates(false)
415 .build();
416 assert!(matches!(odb.peel_tags(a), Err(Error::CorruptObject(_))));
417 }
418}