Skip to main content

git_zero_core/pack/
index.rs

1//! Version-2 pack indexes built from caller-owned entry metadata.
2use super::{IndexEntry, LooseEncoder, crc::Crc32, encode};
3use crate::{Error, ObjectId, Repository, Result, io::Write, object::Hash};
4
5/// Stream a stored-block pack and a version-2 index without buffering either.
6/// `order` selects unique object indices or `None` selects the whole repository.
7/// Metadata needs one slot per selected object; extra slots remain untouched.
8/// Scratch must be nonempty for nonempty selections. The resulting pack matches
9/// `write_selected_with_scratch`/`write_with_scratch` for every scratch size.
10///
11/// IDs, selection and metadata capacity are checked before output. The repository
12/// must satisfy its stable, accurate-ID contract (or be validated beforehand).
13/// Preflight IDs are retained, avoiding repeated provider hashing. Entries are
14/// collected while encoding each body once, then sorted in place
15/// without allocation in O(n log n) time. Index bytes follow after the pack's
16/// trailer is written. On any error discard both outputs and used metadata;
17/// publish them together only after success. Returns the pack checksum.
18pub fn write_indexed_with_scratch(
19    repository: &impl Repository,
20    order: Option<&[usize]>,
21    entries: &mut [IndexEntry],
22    scratch: &mut [u8],
23    pack_output: &mut impl Write,
24    index_output: &mut impl Write,
25) -> Result<ObjectId> {
26    let entries = prepare_entries(repository, order, entries, scratch)?;
27    let mut output = PackWriter {
28        output: pack_output,
29        hash: repository.object_format().hash(),
30        offset: 0,
31        crc: Crc32::new(),
32    };
33    output.write_all(b"PACK")?;
34    output.write_all(&2_u32.to_be_bytes())?;
35    output.write_all(&(entries.len() as u32).to_be_bytes())?;
36    for (position, entry) in entries.iter_mut().enumerate() {
37        let index = order.map_or(position, |order| order[position]);
38        *entry = output.write_entry(repository, index, entry.id, scratch)?;
39    }
40    let checksum = output.hash.finish();
41    output.output.write_all(checksum.as_bytes())?;
42    entries.sort_unstable_by_key(|entry| entry.id);
43    write_index(entries, checksum, index_output)?;
44    Ok(checksum)
45}
46
47fn prepare_entries<'entries>(
48    repository: &impl Repository,
49    order: Option<&[usize]>,
50    entries: &'entries mut [IndexEntry],
51    scratch: &[u8],
52) -> Result<&'entries mut [IndexEntry]> {
53    if let Some(order) = order {
54        encode::validate_order(repository, order)?;
55    }
56    let count = order.map_or(repository.object_count(), <[usize]>::len);
57    u32::try_from(count).map_err(|_| Error::Unsupported)?;
58    let entries = entries.get_mut(..count).ok_or(Error::BufferTooSmall)?;
59    if count != 0 && scratch.is_empty() {
60        return Err(Error::BufferTooSmall);
61    }
62    for (position, entry) in entries.iter_mut().enumerate() {
63        let index = order.map_or(position, |order| order[position]);
64        let id = repository.object_id(index).ok_or(Error::ObjectNotFound)?;
65        if id.format() != repository.object_format() {
66            return Err(Error::InvalidObjectId);
67        }
68        repository.object_info(index)?;
69        *entry = IndexEntry {
70            id,
71            // Retain caller order while sorting IDs for the uniqueness check.
72            // The selected count above fits u32, so each position fits u64.
73            offset: position as u64,
74            crc32: 0,
75        };
76    }
77    entries.sort_unstable_by_key(|entry| entry.id);
78    if entries.windows(2).any(|pair| pair[0].id == pair[1].id) {
79        return Err(Error::InvalidObject);
80    }
81    entries.sort_unstable_by_key(|entry| entry.offset);
82
83    Ok(entries)
84}
85
86/// Stream a Git version-2 index for already verified pack metadata. Supports
87/// SHA-1/SHA-256, empty packs and 64-bit offsets. The caller supplies entries in
88/// strictly increasing ID order and the corresponding complete pack checksum.
89/// Malformed metadata fails before output; sink failures invalidate partial
90/// output. This validates metadata shape, not pack bytes or object bodies.
91/// The returned index checksum is also written as its trailer.
92pub fn write_index(
93    entries: &[IndexEntry],
94    pack_checksum: ObjectId,
95    output: &mut impl Write,
96) -> Result<ObjectId> {
97    validate_entries(entries, pack_checksum)?;
98    let format = pack_checksum.format();
99    let mut output = IndexWriter {
100        output,
101        hash: format.hash(),
102    };
103    output.write_all(b"\xfftOc")?;
104    output.write_all(&2_u32.to_be_bytes())?;
105    output.write_fanout(entries)?;
106    for entry in entries {
107        output.write_all(entry.id.as_bytes())?;
108    }
109    for entry in entries {
110        output.write_all(&entry.crc32.to_be_bytes())?;
111    }
112    output.write_offsets(entries)?;
113    output.write_all(pack_checksum.as_bytes())?;
114    let checksum = output.hash.finish();
115    output.output.write_all(checksum.as_bytes())?;
116    Ok(checksum)
117}
118
119pub(super) fn validate_entries(entries: &[IndexEntry], pack_checksum: ObjectId) -> Result<()> {
120    u32::try_from(entries.len()).map_err(|_| Error::Unsupported)?;
121    let format = pack_checksum.format();
122    if entries
123        .iter()
124        .any(|entry| entry.id.format() != format || entry.offset < 12)
125        || entries.windows(2).any(|pair| pair[0].id >= pair[1].id)
126    {
127        return Err(Error::InvalidObject);
128    }
129    // The high bit of each 32-bit large-offset reference is its tag.
130    if entries
131        .iter()
132        .filter(|entry| entry.offset >= 0x8000_0000)
133        .count() as u64
134        > 0x8000_0000
135    {
136        return Err(Error::Unsupported);
137    }
138    Ok(())
139}
140
141struct PackWriter<'a, W> {
142    output: &'a mut W,
143    hash: Hash,
144    offset: u64,
145    crc: Crc32,
146}
147
148impl<W: Write> PackWriter<'_, W> {
149    fn write_entry(
150        &mut self,
151        repository: &impl Repository,
152        index: usize,
153        id: ObjectId,
154        scratch: &mut [u8],
155    ) -> Result<IndexEntry> {
156        let info = repository.object_info(index)?;
157        let offset = self.offset;
158        self.crc = Crc32::new();
159        encode::write_entry_header(info, self)?;
160        let mut encoder = LooseEncoder::from_info(index, info, false)?;
161        loop {
162            let count = encoder.read(repository, scratch)?;
163            if count == 0 {
164                break;
165            }
166            self.write_all(&scratch[..count])?;
167        }
168        Ok(IndexEntry {
169            id,
170            offset,
171            crc32: self.crc.value(),
172        })
173    }
174}
175
176impl<W: Write> Write for PackWriter<'_, W> {
177    fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
178        let end = self
179            .offset
180            .checked_add(bytes.len() as u64)
181            .ok_or(Error::LimitExceeded)?;
182        self.output.write_all(bytes)?;
183        self.hash.update(bytes);
184        self.crc.update(bytes);
185        self.offset = end;
186        Ok(())
187    }
188}
189
190struct IndexWriter<'a, W> {
191    output: &'a mut W,
192    hash: Hash,
193}
194
195impl<W: Write> IndexWriter<'_, W> {
196    fn write_fanout(&mut self, entries: &[IndexEntry]) -> Result<()> {
197        let mut position = 0;
198        for first in 0..=255_u8 {
199            while position < entries.len() && entries[position].id.as_bytes()[0] <= first {
200                position += 1;
201            }
202            self.write_all(&(position as u32).to_be_bytes())?;
203        }
204        Ok(())
205    }
206
207    fn write_offsets(&mut self, entries: &[IndexEntry]) -> Result<()> {
208        let mut large = 0_u32;
209        for entry in entries {
210            let offset = if entry.offset < 0x8000_0000 {
211                entry.offset as u32
212            } else {
213                let offset = 0x8000_0000 | large;
214                large += 1;
215                offset
216            };
217            self.write_all(&offset.to_be_bytes())?;
218        }
219        for entry in entries.iter().filter(|entry| entry.offset >= 0x8000_0000) {
220            self.write_all(&entry.offset.to_be_bytes())?;
221        }
222        Ok(())
223    }
224}
225
226impl<W: Write> Write for IndexWriter<'_, W> {
227    fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
228        self.output.write_all(bytes)?;
229        self.hash.update(bytes);
230        Ok(())
231    }
232}