Skip to main content

sley_pack/
fix_thin.rs

1//! Thin-pack completion.
2
3use super::*;
4
5/// A self-contained pack and the v2 index built for its final bytes.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct FixThinPackBuild {
8    /// The completed pack. This is byte-for-byte identical to the input when
9    /// the input was already self-contained.
10    pub pack: Vec<u8>,
11    /// The v2 index build corresponding to `pack`.
12    pub index: PackIndexBuild,
13    /// External base object ids appended to the pack, in append order.
14    pub appended_bases: Vec<ObjectId>,
15}
16
17/// Complete a thin pack by appending every external ref-delta base it needs.
18///
19/// Required bases are written once each as full, non-delta entries, the object
20/// count is patched, and the pack trailer and v2 index are rebuilt. If the
21/// input already resolves without `external_base`, its bytes are returned
22/// unchanged and the resolver is not called.
23///
24/// An object id already present in the pack body is never appended again. The
25/// completed bytes are indexed again without an external resolver, so unusual
26/// forward or cyclic ref-delta arrangements cannot use that de-duplication to
27/// produce a pack that only sley's permissive resolver accepts.
28pub fn fix_thin_pack<F>(
29    pack_bytes: &[u8],
30    format: ObjectFormat,
31    external_base: F,
32) -> Result<FixThinPackBuild>
33where
34    F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
35{
36    fix_thin_pack_with_limits(pack_bytes, format, external_base, PackReadLimits::default())
37}
38
39/// [`fix_thin_pack`] with explicit pack parsing and delta-depth limits.
40pub fn fix_thin_pack_with_limits<F>(
41    pack_bytes: &[u8],
42    format: ObjectFormat,
43    mut external_base: F,
44    limits: PackReadLimits,
45) -> Result<FixThinPackBuild>
46where
47    F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
48{
49    if let Ok(index) = PackIndex::write_v2_for_pack_with_limits(pack_bytes, format, limits) {
50        return Ok(FixThinPackBuild {
51            pack: pack_bytes.to_vec(),
52            index,
53            appended_bases: Vec::new(),
54        });
55    }
56
57    let mut resolved_external = HashMap::<ObjectId, Option<EncodedObject>>::new();
58    let mut external_order = Vec::new();
59    let thin_index = PackIndex::write_v2_for_pack_with_base_and_limits(
60        pack_bytes,
61        format,
62        |oid| {
63            if let Some(object) = resolved_external.get(oid) {
64                return Ok(object.clone());
65            }
66            let object = external_base(oid)?;
67            if let Some(object) = &object {
68                let actual = object.object_id(format)?;
69                if actual != *oid {
70                    return Err(GitError::InvalidObject(format!(
71                        "external base {oid} resolved to object {actual}"
72                    )));
73                }
74                external_order.push(*oid);
75            }
76            resolved_external.insert(*oid, object.clone());
77            Ok(object)
78        },
79        limits,
80    )?;
81
82    let body_oids = thin_index
83        .entries
84        .iter()
85        .map(|entry| entry.oid)
86        .collect::<HashSet<_>>();
87    let appended_bases = external_order
88        .into_iter()
89        .filter(|oid| !body_oids.contains(oid))
90        .collect::<Vec<_>>();
91
92    let trailer_len = format.raw_len();
93    let trailer_offset = pack_bytes
94        .len()
95        .checked_sub(trailer_len)
96        .ok_or_else(|| GitError::InvalidFormat("pack file too short".into()))?;
97    let old_count = u32_be(&pack_bytes[8..12]);
98    let append_count = u32::try_from(appended_bases.len())
99        .map_err(|_| GitError::InvalidFormat("too many external pack bases".into()))?;
100    let new_count = old_count
101        .checked_add(append_count)
102        .ok_or_else(|| GitError::InvalidFormat("pack object count overflow".into()))?;
103
104    let mut pack = Vec::with_capacity(pack_bytes.len());
105    pack.extend_from_slice(&pack_bytes[..trailer_offset]);
106    pack[8..12].copy_from_slice(&new_count.to_be_bytes());
107    for oid in &appended_bases {
108        let object = resolved_external
109            .get(oid)
110            .and_then(Option::as_ref)
111            .ok_or_else(|| GitError::not_found(format!("external pack base {oid}")))?;
112        write_entry_header(&mut pack, object.object_type, object.body.len() as u64);
113        write_compressed_payload(&mut pack, &object.body, 6)?;
114    }
115    let checksum = sley_core::digest_bytes(format, &pack)?;
116    pack.extend_from_slice(checksum.as_bytes());
117
118    // This is the interoperability gate: the result must resolve with no
119    // cross-pack base lookup, regardless of what the first indexing pass used.
120    let index = PackIndex::write_v2_for_pack_with_limits(&pack, format, limits)?;
121    Ok(FixThinPackBuild {
122        pack,
123        index,
124        appended_bases,
125    })
126}