1use std::fs;
15use std::io::Read;
16use std::path::{Path, PathBuf};
17
18use flate2::read::ZlibDecoder;
19
20use crate::error::{Error, Result};
21use crate::objects::{HashAlgo, ObjectId, ObjectKind};
22use crate::pack::{
23 clear_pack_cache, hash_object_bytes, parse_ofs_delta_base, parse_pack_object_header,
24 skip_one_pack_object, PackedType,
25};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct PackIndexWriteEntry {
30 pub oid: ObjectId,
32 pub offset: u64,
34}
35
36#[derive(Debug, Clone)]
38pub struct InstallPackOptions {
39 pub stem: Option<String>,
41 pub hash_algo: HashAlgo,
43}
44
45impl Default for InstallPackOptions {
46 fn default() -> Self {
47 Self {
48 stem: None,
49 hash_algo: HashAlgo::Sha1,
50 }
51 }
52}
53
54fn validated_pack_body(pack: &[u8], hash_algo: HashAlgo) -> Result<(usize, usize)> {
56 let hash_bytes = hash_algo.len();
57 if pack.len() < 12 + hash_bytes {
58 return Err(Error::CorruptObject("pack file too small".into()));
59 }
60 if &pack[0..4] != b"PACK" {
61 return Err(Error::CorruptObject("invalid PACK signature".into()));
62 }
63 let version = u32::from_be_bytes(pack[4..8].try_into().unwrap_or([0; 4]));
64 if version != 2 && version != 3 {
65 return Err(Error::CorruptObject(format!(
66 "unsupported pack version {version}"
67 )));
68 }
69 let count = u32::from_be_bytes(pack[8..12].try_into().unwrap_or([0; 4])) as usize;
70 let body_end = pack.len() - hash_bytes;
71 Ok((body_end, count))
72}
73
74pub fn enumerate_whole_pack_entries(
80 pack: &[u8],
81 hash_algo: HashAlgo,
82) -> Result<Vec<PackIndexWriteEntry>> {
83 let hash_bytes = hash_algo.len();
84 let (body_end, count) = validated_pack_body(pack, hash_algo)?;
85 let mut pos = 12usize;
86 let mut out = Vec::with_capacity(count);
87 while pos < body_end {
88 let offset =
89 u64::try_from(pos).map_err(|_| Error::CorruptObject("pack offset overflow".into()))?;
90 let (packed_type, size) = parse_pack_object_header(pack, &mut pos)?;
91 match packed_type {
92 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
93 let mut dec = ZlibDecoder::new(&pack[pos..]);
94 let mut data = Vec::with_capacity(size as usize);
95 dec.read_to_end(&mut data)
96 .map_err(|e| Error::Zlib(e.to_string()))?;
97 pos += dec.total_in() as usize;
98 let kind = packed_type_to_kind(packed_type)?;
99 let oid_bytes = hash_object_bytes(kind, &data, hash_bytes)?;
100 let oid = ObjectId::from_bytes(&oid_bytes)?;
101 out.push(PackIndexWriteEntry { oid, offset });
102 }
103 PackedType::RefDelta | PackedType::OfsDelta => {
104 return Err(Error::CorruptObject(
105 "enumerate_whole_pack_entries requires a non-delta pack".into(),
106 ));
107 }
108 }
109 }
110 if out.len() != count {
111 return Err(Error::CorruptObject(format!(
112 "pack header object count {count} != scanned {}",
113 out.len()
114 )));
115 }
116 out.sort_by(|a, b| a.oid.as_bytes().cmp(b.oid.as_bytes()));
117 Ok(out)
118}
119
120pub fn write_pack_index_v2(
128 idx_path: &Path,
129 pack_path: &Path,
130 entries: &[PackIndexWriteEntry],
131 hash_bytes: usize,
132) -> Result<()> {
133 let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
134 if pack_bytes.len() < hash_bytes {
135 return Err(Error::CorruptObject(
136 "pack too small for idx trailer".into(),
137 ));
138 }
139 let mut fanout = [0u32; 256];
140 for byte in 0u32..256 {
141 let count = entries
142 .iter()
143 .filter(|e| u32::from(e.oid.as_bytes()[0]) <= byte)
144 .count();
145 fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
146 }
147 let mut buf = Vec::new();
148 buf.extend_from_slice(b"\xfftOc");
149 buf.extend_from_slice(&2u32.to_be_bytes());
150 for f in fanout {
151 buf.extend_from_slice(&f.to_be_bytes());
152 }
153 for e in entries {
154 buf.extend_from_slice(e.oid.as_bytes());
155 }
156 for e in entries {
157 let start = e.offset as usize;
158 let mut end = start;
159 skip_one_pack_object(&pack_bytes, &mut end, e.offset, hash_bytes)
160 .map_err(|err| Error::CorruptObject(format!("idx crc walk: {err}")))?;
161 let crc = crc32fast::hash(&pack_bytes[start..end]);
162 buf.extend_from_slice(&crc.to_be_bytes());
163 }
164 let (offsets32, large_offsets) = pack_index_v2_offset_tables(entries);
165 for v in offsets32 {
166 buf.extend_from_slice(&v.to_be_bytes());
167 }
168 for lo in large_offsets {
169 buf.extend_from_slice(&lo.to_be_bytes());
170 }
171 buf.extend_from_slice(&pack_bytes[pack_bytes.len() - hash_bytes..]);
172 let idx_hash = match hash_bytes {
173 20 => HashAlgo::Sha1.checksum_digest(&buf),
174 32 => HashAlgo::Sha256.checksum_digest(&buf),
175 _ => {
176 return Err(Error::CorruptObject(format!(
177 "unsupported hash width {hash_bytes} for pack index"
178 )));
179 }
180 };
181 buf.extend_from_slice(&idx_hash);
182 fs::write(idx_path, buf).map_err(Error::Io)?;
183 Ok(())
184}
185
186pub fn install_pack_stream(
195 objects_dir: &Path,
196 pack: &[u8],
197 opts: InstallPackOptions,
198) -> Result<(PathBuf, PathBuf)> {
199 let hash_bytes = opts.hash_algo.len();
200 let entries = enumerate_whole_pack_entries(pack, opts.hash_algo)?;
201 let pack_dir = objects_dir.join("pack");
202 fs::create_dir_all(&pack_dir).map_err(Error::Io)?;
203 let stem = opts.stem.unwrap_or_else(|| {
204 let trailer = &pack[pack.len() - hash_bytes..];
205 hex::encode(&trailer[..hash_bytes.min(12)])
206 });
207 let pack_path = pack_dir.join(format!("pack-{stem}.pack"));
208 let idx_path = pack_dir.join(format!("pack-{stem}.idx"));
209 fs::write(&pack_path, pack).map_err(Error::Io)?;
210 write_pack_index_v2(&idx_path, &pack_path, &entries, hash_bytes)?;
211 clear_pack_cache();
212 Ok((pack_path, idx_path))
213}
214
215#[must_use = "callers use this for size reporting"]
221pub fn whole_pack_uncompressed_size(pack: &[u8], hash_algo: HashAlgo) -> Result<u64> {
222 let hash_bytes = hash_algo.len();
223 let (body_end, header_count) = validated_pack_body(pack, hash_algo)?;
224 let mut pos = 12usize;
225 let mut total = 0u64;
226 let mut seen = 0usize;
227 while pos < body_end {
228 let (packed_type, size) = parse_pack_object_header(pack, &mut pos)?;
229 match packed_type {
230 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
231 total = total.saturating_add(size);
232 let mut dec = ZlibDecoder::new(&pack[pos..]);
233 let mut tmp = Vec::with_capacity(size as usize);
234 dec.read_to_end(&mut tmp)
235 .map_err(|e| Error::Zlib(e.to_string()))?;
236 pos += dec.total_in() as usize;
237 seen += 1;
238 }
239 PackedType::RefDelta => {
240 pos += hash_bytes;
241 let mut dec = ZlibDecoder::new(&pack[pos..]);
242 let mut tmp = Vec::with_capacity(size as usize);
243 dec.read_to_end(&mut tmp)
244 .map_err(|e| Error::Zlib(e.to_string()))?;
245 pos += dec.total_in() as usize;
246 seen += 1;
247 }
248 PackedType::OfsDelta => {
249 let obj_start = pos;
250 let _base = parse_ofs_delta_base(pack, &mut pos, obj_start as u64)?;
251 let mut dec = ZlibDecoder::new(&pack[pos..]);
252 let mut tmp = Vec::with_capacity(size as usize);
253 dec.read_to_end(&mut tmp)
254 .map_err(|e| Error::Zlib(e.to_string()))?;
255 pos += dec.total_in() as usize;
256 seen += 1;
257 }
258 }
259 }
260 if seen != header_count {
261 return Err(Error::CorruptObject(format!(
262 "pack header object count {header_count} != scanned {seen}"
263 )));
264 }
265 Ok(total)
266}
267
268#[must_use]
274pub fn pack_index_v2_offset_tables(entries: &[PackIndexWriteEntry]) -> (Vec<u32>, Vec<u64>) {
275 let mut offsets32 = Vec::with_capacity(entries.len());
276 let mut large = Vec::new();
277 for e in entries {
278 if e.offset < 0x8000_0000 {
279 offsets32.push(e.offset as u32);
280 } else {
281 let idx = u32::try_from(large.len()).unwrap_or(u32::MAX);
282 offsets32.push(0x8000_0000 | idx);
283 large.push(e.offset);
284 }
285 }
286 (offsets32, large)
287}
288
289fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
290 match pt {
291 PackedType::Commit => Ok(ObjectKind::Commit),
292 PackedType::Tree => Ok(ObjectKind::Tree),
293 PackedType::Blob => Ok(ObjectKind::Blob),
294 PackedType::Tag => Ok(ObjectKind::Tag),
295 PackedType::RefDelta | PackedType::OfsDelta => Err(Error::CorruptObject(
296 "delta object in whole-object enumeration".into(),
297 )),
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::objects::ObjectKind;
305 use crate::odb::Odb;
306 use crate::pack::read_pack_index;
307 use crate::transfer::{build_pack, PackBuildOptions};
308
309 #[test]
310 fn install_pack_passes_git_verify_pack() {
311 let tmp = tempfile::tempdir().expect("tempdir");
312 let odb = Odb::new(tmp.path());
313 let blob = odb
314 .write(ObjectKind::Blob, b"synth pack-write test\n")
315 .expect("write");
316 let pack = build_pack(
317 &odb,
318 &[blob],
319 &[],
320 &PackBuildOptions {
321 delta: false,
322 ..PackBuildOptions::default()
323 },
324 )
325 .expect("build");
326 let (pack_path, idx_path) = install_pack_stream(
327 tmp.path(),
328 &pack,
329 InstallPackOptions {
330 hash_algo: HashAlgo::Sha1,
331 ..InstallPackOptions::default()
332 },
333 )
334 .expect("install");
335 let idx = read_pack_index(&idx_path).expect("read idx");
336 assert_eq!(idx.len(), 1);
337 let offset = idx
338 .find_offset(&blob)
339 .expect("blob in idx")
340 .expect("offset");
341 assert!(offset > 0, "pack index offset is positive");
342 let pack_str = pack_path.to_str().expect("pack path utf-8");
343 let out = grit_test_support::git_cmd(&["verify-pack", "-s", pack_str])
344 .in_dir(tmp.path())
345 .exec();
346 assert!(out.ok(), "{}", out.dump("git verify-pack"));
347 assert_eq!(
348 idx.find_offset(&blob).expect("lookup"),
349 Some(idx.offset_at(0).expect("off0")),
350 "idx offset matches blob"
351 );
352 assert_eq!(
353 idx.find_offset(&blob).expect("lookup"),
354 Some(offset),
355 "idx offset stable"
356 );
357 }
358
359 #[test]
360 fn pack_index_v2_large_offset_table_round_trips() {
361 let entries = vec![
362 PackIndexWriteEntry {
363 oid: ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
364 offset: 100,
365 },
366 PackIndexWriteEntry {
367 oid: ObjectId::from_hex("0000000000000000000000000000000000000002").unwrap(),
368 offset: 0x8000_0000 + 512,
369 },
370 PackIndexWriteEntry {
371 oid: ObjectId::from_hex("0000000000000000000000000000000000000003").unwrap(),
372 offset: (1u64 << 33) + 99,
373 },
374 ];
375 let (offsets32, large) = pack_index_v2_offset_tables(&entries);
376 assert_eq!(offsets32[0], 100);
377 assert_eq!(offsets32[1], 0x8000_0000);
378 assert_eq!(offsets32[2], 0x8000_0001);
379 assert_eq!(large, vec![0x8000_0000 + 512, (1u64 << 33) + 99]);
380 let mut decoded_large = 0usize;
381 for (i, raw) in offsets32.iter().enumerate() {
382 let off = if (*raw & 0x8000_0000) == 0 {
383 u64::from(*raw)
384 } else {
385 let slot = decoded_large;
386 decoded_large += 1;
387 large[slot]
388 };
389 assert_eq!(off, entries[i].offset, "entry {i}");
390 }
391 }
392
393 #[test]
394 fn whole_pack_uncompressed_size_rejects_malformed_input() {
395 use crate::error::Error;
396 let err = whole_pack_uncompressed_size(&[], HashAlgo::Sha1).unwrap_err();
397 assert!(matches!(err, Error::CorruptObject(_)));
398 let truncated = b"PACK";
399 let err = whole_pack_uncompressed_size(truncated, HashAlgo::Sha1).unwrap_err();
400 assert!(matches!(err, Error::CorruptObject(_)));
401 let bad_sig = b"NOPE\x00\x00\x00\x02\x00\x00\x00\x00";
402 let err = whole_pack_uncompressed_size(bad_sig, HashAlgo::Sha1).unwrap_err();
403 assert!(matches!(err, Error::CorruptObject(_)));
404 }
405
406 #[test]
407 fn whole_pack_uncompressed_size_empty_pack_is_zero() {
408 let mut pack = Vec::from(b"PACK" as &[u8]);
409 pack.extend_from_slice(&2u32.to_be_bytes());
410 pack.extend_from_slice(&0u32.to_be_bytes());
411 pack.extend_from_slice(&[0u8; 20]);
412 assert_eq!(
413 whole_pack_uncompressed_size(&pack, HashAlgo::Sha1).unwrap(),
414 0
415 );
416 }
417
418 #[test]
419 fn enumerate_whole_pack_matches_build_pack() {
420 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
421 let a = odb.write(ObjectKind::Blob, b"aaa").unwrap();
422 let b = odb.write(ObjectKind::Blob, b"bbb").unwrap();
423 let pack = build_pack(
424 &odb,
425 &[a, b],
426 &[],
427 &PackBuildOptions {
428 delta: false,
429 ..Default::default()
430 },
431 )
432 .unwrap();
433 let entries = enumerate_whole_pack_entries(&pack, HashAlgo::Sha1).unwrap();
434 assert_eq!(entries.len(), 2);
435 assert!(entries
436 .windows(2)
437 .all(|w| w[0].oid.as_bytes() <= w[1].oid.as_bytes()));
438 }
439}