Skip to main content

grit_lib/
tree_path_follow.rs

1//! Resolve tree paths with symlink following (`get_tree_entry_follow_symlinks`).
2//!
3//! Behaviour matches upstream Git (`tree-walk.c`).
4
5use std::collections::HashSet;
6
7use crate::error::Result;
8use crate::objects::{parse_tree, ObjectId, ObjectKind};
9use crate::odb::Odb;
10
11const MAX_SYMLINK_FOLLOWS: usize = 40;
12
13/// Result of resolving `tree_oid:path` with symlink following.
14#[derive(Debug, Clone)]
15pub enum FollowPathResult {
16    /// Found object inside the repository.
17    Found { oid: ObjectId, mode: u32 },
18    /// Symlink target is absolute (`/`); caller prints `symlink <len> <target>`.
19    OutOfRepo { path: Vec<u8> },
20}
21
22/// Failure modes reported as special `git cat-file --batch-check` lines.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum FollowPathFailure {
25    Missing,
26    DanglingSymlink,
27    SymlinkLoop,
28    NotDir,
29}
30
31fn git_mode_is_dir(mode: u32) -> bool {
32    mode == 0o040000
33}
34
35fn git_mode_is_symlink(mode: u32) -> bool {
36    mode == 0o120000
37}
38
39fn git_mode_is_blob(mode: u32) -> bool {
40    (mode & 0o170000) == 0o100000
41}
42
43fn find_one_entry(tree_data: &[u8], name: &str) -> Result<Option<(ObjectId, u32)>> {
44    let entries = parse_tree(tree_data)?;
45    for e in entries {
46        if e.name == name.as_bytes() {
47            return Ok(Some((e.oid, e.mode)));
48        }
49    }
50    Ok(None)
51}
52
53/// Walk `tree_oid` following symlinks like `get_tree_entry_follow_symlinks`.
54pub fn get_tree_entry_follow_symlinks(
55    odb: &Odb,
56    tree_oid: &ObjectId,
57    path: &str,
58) -> Result<std::result::Result<FollowPathResult, FollowPathFailure>> {
59    let mut stack: Vec<ObjectId> = vec![*tree_oid];
60    let mut path_buf = path.to_string();
61    let mut follows = 0usize;
62    let mut followed_symlink_blobs: HashSet<ObjectId> = HashSet::new();
63    let mut symlink_just_followed = false;
64
65    loop {
66        let Some(&tree_oid) = stack.last() else {
67            return Ok(Err(FollowPathFailure::Missing));
68        };
69
70        while path_buf.starts_with('/') {
71            path_buf.remove(0);
72        }
73
74        if path_buf.is_empty() {
75            return Ok(Ok(FollowPathResult::Found {
76                oid: tree_oid,
77                mode: 0o040000,
78            }));
79        }
80
81        let (first, rest) = match path_buf.split_once('/') {
82            Some((a, b)) => (a.to_string(), Some(b.to_string())),
83            None => (path_buf.clone(), None),
84        };
85
86        if first == ".." {
87            if stack.len() <= 1 {
88                return Ok(Ok(FollowPathResult::OutOfRepo {
89                    path: path_buf.into_bytes(),
90                }));
91            }
92            stack.pop();
93            followed_symlink_blobs.clear();
94            symlink_just_followed = false;
95            path_buf = rest.unwrap_or_default();
96            continue;
97        }
98
99        if first.is_empty() {
100            let Some(&oid) = stack.last() else {
101                return Ok(Err(FollowPathFailure::Missing));
102            };
103            return Ok(Ok(FollowPathResult::Found {
104                oid,
105                mode: 0o040000,
106            }));
107        }
108
109        let tree_obj = match odb.read(&tree_oid) {
110            Ok(o) => o,
111            Err(_) => return Ok(Err(FollowPathFailure::Missing)),
112        };
113        if tree_obj.kind != ObjectKind::Tree {
114            return Ok(Err(FollowPathFailure::Missing));
115        }
116
117        let found = match find_one_entry(&tree_obj.data, &first) {
118            Ok(x) => x,
119            Err(_) => return Ok(Err(FollowPathFailure::Missing)),
120        };
121
122        let Some((entry_oid, mode)) = found else {
123            if symlink_just_followed {
124                return Ok(Err(FollowPathFailure::DanglingSymlink));
125            }
126            return Ok(Err(FollowPathFailure::Missing));
127        };
128        symlink_just_followed = false;
129
130        if git_mode_is_dir(mode) {
131            if rest.is_none() {
132                return Ok(Ok(FollowPathResult::Found {
133                    oid: entry_oid,
134                    mode,
135                }));
136            }
137            stack.push(entry_oid);
138            path_buf = rest.unwrap();
139            continue;
140        }
141
142        if git_mode_is_blob(mode) {
143            if rest.is_none() {
144                return Ok(Ok(FollowPathResult::Found {
145                    oid: entry_oid,
146                    mode,
147                }));
148            }
149            return Ok(Err(FollowPathFailure::NotDir));
150        }
151
152        if git_mode_is_symlink(mode) {
153            if follows >= MAX_SYMLINK_FOLLOWS {
154                return Ok(Err(FollowPathFailure::SymlinkLoop));
155            }
156            if !followed_symlink_blobs.insert(entry_oid) {
157                return Ok(Err(FollowPathFailure::SymlinkLoop));
158            }
159            follows += 1;
160
161            let obj = match odb.read(&entry_oid) {
162                Ok(o) => o,
163                Err(_) => return Ok(Err(FollowPathFailure::DanglingSymlink)),
164            };
165            if obj.kind != ObjectKind::Blob {
166                return Ok(Err(FollowPathFailure::DanglingSymlink));
167            }
168
169            if obj.data.first() == Some(&b'/') {
170                return Ok(Ok(FollowPathResult::OutOfRepo {
171                    path: obj.data.clone(),
172                }));
173            }
174
175            let mut new_path = String::from_utf8_lossy(&obj.data)
176                .trim_end_matches(|c| c == '\n' || c == '\r')
177                .to_string();
178            if let Some(r) = rest {
179                new_path.push('/');
180                new_path.push_str(&r);
181            }
182            path_buf = new_path;
183            symlink_just_followed = true;
184            continue;
185        }
186
187        // Submodule (gitlink) or other.
188        if rest.is_none() {
189            return Ok(Ok(FollowPathResult::Found {
190                oid: entry_oid,
191                mode,
192            }));
193        }
194        return Ok(Err(FollowPathFailure::NotDir));
195    }
196}