1#![cfg(unix)]
20
21use super::repo_read_odb::alternates_of;
22use std::fs::File;
23use std::os::unix::fs::FileExt;
24use std::path::{Path, PathBuf};
25
26pub const FALLBACK_ABBREVIATION: usize = 7;
28
29pub const MINIMUM_ABBREVIATION: i64 = 4;
31
32const LONGEST_NAME: usize = 64;
34
35const FANOUT_END: u64 = 8 + 256 * 4;
37
38const FANOUT_END_V1: u64 = 256 * 4;
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum Setting {
44 Auto,
46 Length(usize),
48}
49
50pub fn setting_of(value: Option<&[u8]>) -> Setting {
61 let Some(bytes) = value else { return Setting::Auto };
62 let Ok(text) = std::str::from_utf8(bytes) else { return Setting::Auto };
63 let lowered: String = text.trim().to_ascii_lowercase();
64 if lowered == "auto" {
65 return Setting::Auto;
66 }
67 if lowered.is_empty() || ["no", "false", "off"].contains(&lowered.as_str()) {
68 return Setting::Length(LONGEST_NAME);
69 }
70 return match lowered.parse::<i64>() {
71 Ok(number) if number >= MINIMUM_ABBREVIATION => Setting::Length(usize::try_from(number).unwrap_or(LONGEST_NAME)),
72 _ => Setting::Auto,
73 };
74}
75
76fn highest_bit(count: u64) -> usize {
78 return if count == 0 { 0 } else { 63 - count.leading_zeros() as usize };
79}
80
81pub fn automatic_length(count: u64) -> usize {
92 return (highest_bit(count) + 1).div_ceil(2).max(FALLBACK_ABBREVIATION);
93}
94
95fn shared_prefix(first: &[u8], second: &[u8]) -> usize {
97 let mut shared: usize = 0;
98 for (left, right) in first.iter().zip(second) {
99 if left == right {
100 shared += 2;
101 continue;
102 }
103 if left >> 4 == right >> 4 {
104 shared += 1;
105 }
106 break;
107 }
108 return shared;
109}
110
111fn be32(file: &File, position: u64) -> Option<u32> {
113 let mut bytes: [u8; 4] = [0; 4];
114 file.read_exact_at(&mut bytes, position).ok()?;
115 return Some(u32::from_be_bytes(bytes));
116}
117
118fn be64(file: &File, position: u64) -> Option<u64> {
120 let mut bytes: [u8; 8] = [0; 8];
121 file.read_exact_at(&mut bytes, position).ok()?;
122 return Some(u64::from_be_bytes(bytes));
123}
124
125struct SortedNames {
127 file: File,
128 first: u64,
129 stride: u64,
130 count: u64,
131 length: usize,
132}
133
134impl SortedNames {
135 fn name(&self, position: u64) -> Option<Vec<u8>> {
137 let mut bytes: Vec<u8> = vec![0; self.length];
138 self.file.read_exact_at(bytes.as_mut_slice(), self.first + position * self.stride).ok()?;
139 return Some(bytes);
140 }
141
142 fn longest_shared(&self, target: &[u8]) -> Option<usize> {
153 let mut low: u64 = 0;
154 let mut high: u64 = self.count;
155 let mut found: bool = false;
156 while low < high {
157 let middle: u64 = low + (high - low) / 2;
158 match self.name(middle)?.as_slice().cmp(target) {
159 std::cmp::Ordering::Equal => {
160 found = true;
161 low = middle;
162 break;
163 }
164 std::cmp::Ordering::Less => low = middle + 1,
165 std::cmp::Ordering::Greater => high = middle,
166 }
167 }
168 let mut longest: usize = 0;
169 if low > 0 {
170 longest = longest.max(shared_prefix(self.name(low - 1)?.as_slice(), target));
171 }
172 let after: u64 = if found { low + 1 } else { low };
173 if after < self.count {
174 longest = longest.max(shared_prefix(self.name(after)?.as_slice(), target));
175 }
176 return Some(longest);
177 }
178}
179
180fn open_index(path: &Path, length: usize) -> Option<SortedNames> {
182 let file: File = File::open(path).ok()?;
183 let mut magic: [u8; 8] = [0; 8];
184 file.read_exact_at(&mut magic, 0).ok()?;
185 if magic == *b"\xfftOc\0\0\0\x02" {
186 let count: u64 = u64::from(be32(&file, 8 + 255 * 4)?);
187 return Some(SortedNames { file, first: FANOUT_END, stride: length as u64, count, length });
188 }
189 let count: u64 = u64::from(be32(&file, 255 * 4)?);
191 return Some(SortedNames { file, first: FANOUT_END_V1 + 4, stride: 4 + length as u64, count, length });
192}
193
194const CHUNK_FANOUT: u32 = 0x4f49_4446;
196const CHUNK_NAMES: u32 = 0x4f49_444c;
197const CHUNK_PACKS: u32 = 0x504e_414d;
198
199struct Midx {
201 names: SortedNames,
202 covered: Vec<String>,
203}
204
205fn read_midx(pack_directory: &Path, length: usize) -> Result<Option<Midx>, ()> {
216 if pack_directory.join("multi-pack-index.d").exists() {
217 return Err(());
218 }
219 let path: PathBuf = pack_directory.join("multi-pack-index");
220 let Ok(file) = File::open(&path) else { return Ok(None) };
221 let mut header: [u8; 12] = [0; 12];
222 file.read_exact_at(&mut header, 0).map_err(|_| {})?;
223 if &header[..4] != b"MIDX" || header[4] != 1 || header[7] != 0 {
225 return Err(());
226 }
227 let chunks: u64 = u64::from(header[6]);
228 let packs: u64 = u64::from(u32::from_be_bytes([header[8], header[9], header[10], header[11]]));
229 let mut fanout_chunk: Option<u64> = None;
230 let mut names_chunk: Option<u64> = None;
231 let mut packs_chunk: Option<(u64, u64)> = None;
232 let mut previous: Option<(u32, u64)> = None;
233 for index in 0..=chunks {
234 let at: u64 = 12 + index * 12;
235 let id: u32 = be32(&file, at).ok_or(())?;
236 let offset: u64 = be64(&file, at + 4).ok_or(())?;
237 if let Some((earlier, start)) = previous {
238 match earlier {
239 CHUNK_FANOUT => fanout_chunk = Some(start),
240 CHUNK_NAMES => names_chunk = Some(start),
241 CHUNK_PACKS => packs_chunk = Some((start, offset)),
242 _ => {}
243 }
244 }
245 previous = Some((id, offset));
246 }
247 let (fanout_at, names_at, (packs_start, packs_end)) = (fanout_chunk.ok_or(())?, names_chunk.ok_or(())?, packs_chunk.ok_or(())?);
248 let count: u64 = u64::from(be32(&file, fanout_at + 255 * 4).ok_or(())?);
249 let mut listed: Vec<u8> = vec![0; usize::try_from(packs_end.saturating_sub(packs_start)).map_err(|_| {})?];
250 file.read_exact_at(listed.as_mut_slice(), packs_start).map_err(|_| {})?;
251 let covered: Vec<String> = listed
252 .split(|byte| { return *byte == 0; })
253 .filter(|name| { return !name.is_empty(); })
254 .map(|name| { return String::from_utf8_lossy(name).into_owned(); })
255 .take(usize::try_from(packs).map_err(|_| {})?)
256 .collect();
257 return Ok(Some(Midx { names: SortedNames { file, first: names_at, stride: length as u64, count, length }, covered }));
258}
259
260struct Packed {
262 midx: Option<Midx>,
263 indexes: Vec<SortedNames>,
264}
265
266fn open_packed(root: &Path, length: usize) -> Result<Packed, ()> {
268 let directory: PathBuf = root.join("pack");
269 let listing = match std::fs::read_dir(&directory) {
270 Ok(listing) => listing,
271 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Packed { midx: None, indexes: Vec::new() }),
272 Err(_) => return Err(()),
273 };
274 let midx: Option<Midx> = read_midx(directory.as_path(), length)?;
275 let mut indexes: Vec<SortedNames> = Vec::new();
276 for entry in listing {
277 let path: PathBuf = entry.map_err(|_| {})?.path();
278 let Some(name) = path.file_name().and_then(|name| { return name.to_str(); }).map(str::to_string) else { continue };
279 if !name.ends_with(".idx") || midx.as_ref().is_some_and(|covering| { return covering.covered.contains(&name); }) {
280 continue;
281 }
282 indexes.push(open_index(path.as_path(), length).ok_or(())?);
283 }
284 return Ok(Packed { midx, indexes });
285}
286
287fn loose_shared(root: &Path, hex: &str) -> Option<usize> {
289 let (directory, rest): (&str, &str) = hex.split_at(2);
290 let listing = match std::fs::read_dir(root.join(directory)) {
291 Ok(listing) => listing,
292 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(0),
293 Err(_) => return None,
294 };
295 let mut longest: usize = 0;
296 for entry in listing {
297 let file_name = entry.ok()?.file_name();
298 let Some(other) = file_name.to_str() else { continue };
299 if other.len() != rest.len() || !other.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) || other == rest {
300 continue;
301 }
302 let shared: usize = rest.bytes().zip(other.bytes()).take_while(|(left, right)| { return left == right; }).count();
303 longest = longest.max(2 + shared);
304 }
305 return Some(longest);
306}
307
308fn bytes_of(hex: &str) -> Option<Vec<u8>> {
310 if hex.len() % 2 != 0 || !hex.bytes().all(|byte| { return byte.is_ascii_hexdigit(); }) {
311 return None;
312 }
313 return (0..hex.len() / 2).map(|at| { return u8::from_str_radix(&hex[at * 2..at * 2 + 2], 16).ok(); }).collect();
314}
315
316pub fn abbreviate(object_directory: &Path, setting: Setting, hex: &str) -> Option<String> {
328 let target: Vec<u8> = bytes_of(hex)?;
329 if hex != hex.to_ascii_lowercase() {
330 return None;
331 }
332 let own: PathBuf = std::fs::canonicalize(object_directory).ok()?;
333 let mut roots: Vec<PathBuf> = vec![own.clone()];
334 alternates_of(own.as_path(), 0, &mut roots)?;
335 let name_length: usize = target.len();
336 let mut opened: Vec<(PathBuf, Packed)> = Vec::new();
337 for root in &roots {
338 opened.push((root.clone(), open_packed(root, name_length).ok()?));
339 }
340 let mut length: usize = match setting {
341 Setting::Length(asked) => asked,
342 Setting::Auto => {
343 let counted: u64 = opened
344 .iter()
345 .map(|(_, packed)| { return packed.midx.as_ref().map_or(0, |midx| { return midx.names.count; }) + packed.indexes.iter().map(|index| { return index.count; }).sum::<u64>(); })
346 .sum();
347 automatic_length(counted)
348 }
349 };
350 if length >= hex.len() || length == 0 {
351 return Some(hex.to_string());
352 }
353 for (root, packed) in &opened {
354 let mut longest: usize = loose_shared(root.as_path(), hex)?;
355 for names in packed.midx.iter().map(|midx| { return &midx.names; }).chain(packed.indexes.iter()) {
356 longest = longest.max(names.longest_shared(&target)?);
357 }
358 if longest >= length {
359 length = longest + 1;
360 }
361 }
362 return Some(hex.get(..length.min(hex.len()))?.to_string());
363}
364
365#[cfg(test)]
367#[path = "object_abbreviation_tests.rs"]
368mod tests;