git_policy_cli/lfs/
lfs_pointer_scan.rs1#![cfg(unix)]
18
19use crate::lfs_pointer::{Verdict, decode_pointer};
20use crate::repo_read_layout::ObjectFormat;
21use crate::repo_read_object::{ObjectKind, name_length};
22use crate::repo_read_odb::ObjectDatabase;
23use crate::repo_read_tree::{CommitFields, MODE_GITLINK, is_tree_mode, parse_commit, parse_tree};
24use std::collections::{BTreeSet, HashMap, HashSet};
25use std::time::{Duration, Instant};
26
27pub const MAX_COMMITS: usize = 100_000;
29
30pub const SCAN_BUDGET: Duration = Duration::from_secs(3);
32
33struct Scan<'a> {
35 database: &'a ObjectDatabase,
37 format: ObjectFormat,
39 commits: HashMap<Vec<u8>, CommitFields>,
41 times: HashMap<Vec<u8>, i64>,
43 seen_trees: HashSet<Vec<Vec<u8>>>,
46 candidates: BTreeSet<Vec<u8>>,
48 deadline: Instant,
50}
51
52impl Scan<'_> {
53 fn commit(&mut self, name: &[u8]) -> Option<CommitFields> {
55 if let Some(known) = self.commits.get(name) {
56 return Some(known.clone());
57 }
58 let object = self.database.read(name)?;
59 if object.kind != ObjectKind::Commit {
60 return None;
61 }
62 let parsed: CommitFields = parse_commit(object.bytes.as_slice(), self.format)?;
63 self.times.insert(name.to_vec(), committer_time(object.bytes.as_slice())?);
64 self.commits.insert(name.to_vec(), parsed.clone());
65 return Some(parsed);
66 }
67
68 fn time(&self, name: &[u8]) -> i64 {
70 return self.times.get(name).copied().unwrap_or_default();
71 }
72
73 fn tree(&self, name: &[u8]) -> Option<Vec<crate::repo_read_tree::TreeEntry>> {
75 let object = self.database.read(name)?;
76 if object.kind != ObjectKind::Tree {
77 return None;
78 }
79 return parse_tree(object.bytes.as_slice(), name_length(self.format));
80 }
81
82 fn compare(&mut self, tree: &[u8], parents: &[Vec<u8>]) -> Option<()> {
84 if Instant::now() >= self.deadline {
85 return None;
86 }
87 if parents.iter().any(|parent| {
88 return parent.as_slice() == tree;
89 }) || !self.seen_trees.insert([vec![tree.to_vec()], parents.to_vec()].concat())
90 {
91 return Some(());
92 }
93 let entries = self.tree(tree)?;
94 let mut parent_entries = Vec::with_capacity(parents.len());
95 for parent in parents {
96 parent_entries.push(self.tree(parent)?);
97 }
98 for entry in &entries {
99 if entry.mode == MODE_GITLINK {
100 continue;
101 }
102 let counterparts: Vec<&crate::repo_read_tree::TreeEntry> = parent_entries
103 .iter()
104 .filter_map(|list| {
105 return list.iter().find(|candidate| {
106 return candidate.name == entry.name;
107 });
108 })
109 .collect();
110 if is_tree_mode(entry.mode) {
111 let below: Vec<Vec<u8>> = counterparts
112 .iter()
113 .filter(|other| {
114 return is_tree_mode(other.mode);
115 })
116 .map(|other| {
117 return other.id.clone();
118 })
119 .collect();
120 self.compare(entry.id.as_slice(), below.as_slice())?;
121 } else if !counterparts.iter().any(|other| {
122 return other.id == entry.id && !is_tree_mode(other.mode);
123 }) {
124 self.candidates.insert(entry.id.clone());
125 }
126 }
127 return Some(());
128 }
129}
130
131const CLOCK_SKEW_SLOP: usize = 5;
133
134fn committer_time(bytes: &[u8]) -> Option<i64> {
136 let header_end: usize = bytes.windows(2).position(|pair| {
137 return pair == b"\n\n";
138 })?;
139 let line: &[u8] = bytes[..header_end].split(|byte| {
140 return *byte == b'\n';
141 }).find(|line| {
142 return line.starts_with(b"committer ");
143 })?;
144 let after_email: &[u8] = &line[line.iter().rposition(|byte| {
145 return *byte == b'>';
146 })? + 1..];
147 let text: &str = std::str::from_utf8(after_email).ok()?;
148 return text.split_whitespace().next()?.parse::<i64>().ok();
149}
150
151struct Walk {
153 queue: std::collections::BinaryHeap<(i64, Vec<u8>)>,
155 known: HashMap<Vec<u8>, bool>,
157 queued: HashSet<Vec<u8>>,
159 unknown_queued: usize,
161 expanded: HashSet<Vec<u8>>,
163}
164
165impl Walk {
166 fn mark(&mut self, scan: &mut Scan<'_>, name: &[u8], known: bool) -> Option<()> {
169 let mut pending: Vec<Vec<u8>> = vec![name.to_vec()];
170 while let Some(current) = pending.pop() {
171 match self.known.get(current.as_slice()).copied() {
172 None => {
173 scan.commit(current.as_slice())?;
174 self.known.insert(current.clone(), known);
175 self.queue.push((scan.time(current.as_slice()), current.clone()));
176 self.queued.insert(current);
177 if !known {
178 self.unknown_queued += 1;
179 }
180 }
181 Some(false) if known => {
182 self.known.insert(current.clone(), true);
183 if self.queued.contains(current.as_slice()) {
184 self.unknown_queued -= 1;
185 } else if self.expanded.contains(current.as_slice()) {
186 pending.extend(scan.commit(current.as_slice())?.parents);
187 }
188 }
189 Some(_) => {}
190 }
191 }
192 return Some(());
193 }
194}
195
196fn new_commits(scan: &mut Scan<'_>, include: &[Vec<u8>], stop: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
210 let mut walk: Walk = Walk {
211 queue: std::collections::BinaryHeap::new(),
212 known: HashMap::new(),
213 queued: HashSet::new(),
214 unknown_queued: 0,
215 expanded: HashSet::new(),
216 };
217 for name in stop {
218 walk.mark(scan, name.as_slice(), true)?;
219 }
220 for name in include {
221 walk.mark(scan, name.as_slice(), false)?;
222 }
223 let mut listed: Vec<Vec<u8>> = Vec::new();
224 let mut slop: usize = 0;
225 let mut popped: usize = 0;
226 while let Some((_, name)) = walk.queue.pop() {
227 walk.queued.remove(name.as_slice());
228 let known: bool = walk.known.get(name.as_slice()).copied().unwrap_or(true);
229 if !known {
230 walk.unknown_queued -= 1;
231 listed.push(name.clone());
232 }
233 popped += 1;
234 if popped > MAX_COMMITS || Instant::now() >= scan.deadline {
235 return None;
236 }
237 let parents: Vec<Vec<u8>> = scan.commit(name.as_slice())?.parents;
238 walk.expanded.insert(name);
239 for parent in &parents {
240 walk.mark(scan, parent.as_slice(), known)?;
241 }
242 if walk.unknown_queued == 0 {
243 slop += 1;
244 if slop > CLOCK_SKEW_SLOP {
245 break;
246 }
247 } else {
248 slop = 0;
249 }
250 }
251 return Some(
252 listed
253 .into_iter()
254 .filter(|name| {
255 return !walk.known.get(name.as_slice()).copied().unwrap_or(false);
256 })
257 .collect(),
258 );
259}
260
261pub fn new_pointer_objects(
276 database: &ObjectDatabase,
277 format: ObjectFormat,
278 include: &[Vec<u8>],
279 stop: &[Vec<u8>],
280 budget: Duration,
281) -> Option<Vec<String>> {
282 let mut scan: Scan<'_> = Scan {
283 database,
284 format,
285 commits: HashMap::new(),
286 times: HashMap::new(),
287 seen_trees: HashSet::new(),
288 candidates: BTreeSet::new(),
289 deadline: Instant::now() + budget,
290 };
291 for name in new_commits(&mut scan, include, stop)? {
292 let commit: CommitFields = scan.commit(name.as_slice())?;
293 let mut parent_trees: Vec<Vec<u8>> = Vec::with_capacity(commit.parents.len());
294 for parent in &commit.parents {
295 parent_trees.push(scan.commit(parent.as_slice())?.tree);
296 }
297 scan.compare(commit.tree.as_slice(), parent_trees.as_slice())?;
298 }
299 let mut objects: BTreeSet<String> = BTreeSet::new();
300 for candidate in &scan.candidates {
301 if Instant::now() >= scan.deadline {
302 return None;
303 }
304 let blob = database.read(candidate.as_slice())?;
305 if blob.kind != ObjectKind::Blob {
306 return None;
307 }
308 match decode_pointer(blob.bytes.as_slice()) {
309 Verdict::NotUploadable => {}
310 Verdict::Pointer(name) => {
311 objects.insert(name);
312 }
313 Verdict::Decline => return None,
314 }
315 }
316 return Some(objects.into_iter().collect());
317}
318
319#[cfg(test)]
321#[path = "lfs_pointer_scan_tests.rs"]
322mod tests;