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 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 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}