grit_lib/
tree_path_follow.rs1use 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#[derive(Debug, Clone)]
15pub enum FollowPathResult {
16 Found { oid: ObjectId, mode: u32 },
18 OutOfRepo { path: Vec<u8> },
20}
21
22#[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
53pub 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 path_buf = rest.unwrap_or_default();
97 continue;
98 }
99
100 if first.is_empty() {
101 let Some(&oid) = stack.last() else {
102 return Ok(Err(FollowPathFailure::Missing));
103 };
104 return Ok(Ok(FollowPathResult::Found {
105 oid,
106 mode: 0o040000,
107 }));
108 }
109
110 let tree_obj = match odb.read(&tree_oid) {
111 Ok(o) => o,
112 Err(_) => return Ok(Err(FollowPathFailure::Missing)),
113 };
114 if tree_obj.kind != ObjectKind::Tree {
115 return Ok(Err(FollowPathFailure::Missing));
116 }
117
118 let found = match find_one_entry(&tree_obj.data, &first) {
119 Ok(x) => x,
120 Err(_) => return Ok(Err(FollowPathFailure::Missing)),
121 };
122
123 let Some((entry_oid, mode)) = found else {
124 if symlink_just_followed {
125 return Ok(Err(FollowPathFailure::DanglingSymlink));
126 }
127 return Ok(Err(FollowPathFailure::Missing));
128 };
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 symlink_just_followed = true;
163
164 let obj = match odb.read(&entry_oid) {
165 Ok(o) => o,
166 Err(_) => return Ok(Err(FollowPathFailure::DanglingSymlink)),
167 };
168 if obj.kind != ObjectKind::Blob {
169 return Ok(Err(FollowPathFailure::DanglingSymlink));
170 }
171
172 if obj.data.first() == Some(&b'/') {
173 return Ok(Ok(FollowPathResult::OutOfRepo {
174 path: obj.data.clone(),
175 }));
176 }
177
178 let mut new_path = String::from_utf8_lossy(&obj.data)
179 .trim_end_matches(['\n', '\r'])
180 .to_string();
181 if let Some(r) = rest {
182 new_path.push('/');
183 new_path.push_str(&r);
184 }
185 path_buf = new_path;
186 continue;
187 }
188
189 if rest.is_none() {
191 return Ok(Ok(FollowPathResult::Found {
192 oid: entry_oid,
193 mode,
194 }));
195 }
196 return Ok(Err(FollowPathFailure::NotDir));
197 }
198}