Skip to main content

git_zero_core/pack/
reuse.rs

1//! Pack assembly using previously authenticated compressed object entries.
2use super::{IndexEntry, crc::Crc32, encode::write_typed_header};
3use crate::{
4    Error, ObjectFormat, ObjectId, ObjectKind, Result,
5    io::{SliceWriter, Write},
6    object::Hash,
7};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CompressedKind {
11    Object(ObjectKind),
12    /// Reference deltas permit arbitrary output ordering and pack offsets.
13    /// Convert OFS bases to their verified object IDs before starting an entry.
14    Delta {
15        base_id: ObjectId,
16    },
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct CompressedEntry {
21    pub id: ObjectId,
22    pub kind: CompressedKind,
23    /// Inflated object size, or inflated instruction size for a delta.
24    pub inflated_size: u64,
25    /// Exactly one complete zlib stream, excluding the original entry header.
26    pub compressed_size: u64,
27}
28
29struct Active {
30    id: ObjectId,
31    offset: u64,
32    remaining: u64,
33    crc: Crc32,
34}
35struct Segment {
36    objects: u32,
37    remaining: u64,
38}
39
40/// Assemble a version-2 pack without inflating/recompressing stored entries.
41/// All state is inline; each sink is supplied per call so a host can await
42/// storage/network operations between bounded writes. Preserve produced bytes.
43/// Any error is terminal and invalidates the entire partial output.
44///
45/// This is an encoding API, not a validator. The caller MUST authenticate the
46/// source streams and metadata, select unique IDs and an authorized object
47/// closure, and provide every delta base either in the pack or in the receiver
48/// (only when thin packs are allowed). Zlib bytes are copied unchanged. New
49/// headers, entry CRCs and the pack checksum reflect the output representation.
50pub struct PackComposer {
51    format: ObjectFormat,
52    remaining: u32,
53    limit: u64,
54    position: u64,
55    hash: Hash,
56    active: Option<Active>,
57    segment: Option<Segment>,
58    checksum: Option<ObjectId>,
59    failure: Option<Error>,
60}
61
62impl PackComposer {
63    pub fn new(
64        format: ObjectFormat,
65        count: u32,
66        max_pack_bytes: u64,
67        output: &mut impl Write,
68    ) -> Result<Self> {
69        if max_pack_bytes < 12 + format.id_length() as u64 {
70            return Err(Error::LimitExceeded);
71        }
72        let mut header = [0; 12];
73        header[..8].copy_from_slice(b"PACK\0\0\0\x02");
74        header[8..].copy_from_slice(&count.to_be_bytes());
75        output.write_all(&header)?;
76        // Source authentication belongs to intake. This encoding-only API
77        // retains a compact checksum state while copying verified entries.
78        let mut hash = format.hash();
79        hash.update(&header);
80        Ok(Self {
81            format,
82            remaining: count,
83            limit: max_pack_bytes,
84            position: 12,
85            hash,
86            active: None,
87            segment: None,
88            checksum: None,
89            failure: None,
90        })
91    }
92
93    pub fn position(&self) -> u64 {
94        self.position
95    }
96    pub fn checksum(&self) -> Option<ObjectId> {
97        self.checksum
98    }
99
100    pub fn start_entry(&mut self, entry: CompressedEntry, output: &mut impl Write) -> Result<()> {
101        let result = self.begin(entry, output);
102        self.record(result)
103    }
104
105    pub fn write_compressed(&mut self, bytes: &[u8], output: &mut impl Write) -> Result<()> {
106        let result = if self.segment.is_some() {
107            self.copy_segment(bytes, output)
108        } else {
109            self.copy(bytes, output)
110        };
111        self.record(result)
112    }
113
114    /// Copy one complete authenticated source pack's entry region, excluding
115    /// its 12-byte header and object-format trailer. Source entries stay
116    /// contiguous: every OFS distance is invariant under the segment's shift.
117    /// The host MUST prove unique IDs across all segments and include every
118    /// REF base (or explicitly permit a thin output). This never parses bytes.
119    pub fn start_segment(&mut self, objects: u32, bytes: u64) -> Result<()> {
120        let result = self.begin_segment(objects, bytes);
121        self.record(result)
122    }
123
124    pub fn finish_segment(&mut self) -> Result<()> {
125        let result = (|| {
126            self.ready()?;
127            let segment = self.segment.as_ref().ok_or(Error::InvalidRequest)?;
128            if segment.remaining != 0 {
129                return Err(Error::InvalidObject);
130            }
131            self.remaining -= segment.objects;
132            self.segment = None;
133            Ok(())
134        })();
135        self.record(result)
136    }
137
138    /// Close the current entry after its exact compressed length was supplied.
139    /// The returned metadata describes the newly encoded pack, not its source.
140    pub fn finish_entry(&mut self) -> Result<IndexEntry> {
141        let result = self.close_entry();
142        self.record(result)
143    }
144
145    /// Write the checksum only after all declared entries are complete.
146    /// Successful repeated calls return the checksum without writing again.
147    pub fn finish(&mut self, output: &mut impl Write) -> Result<ObjectId> {
148        let result = self.close(output);
149        self.record(result)
150    }
151
152    fn ready(&self) -> Result<()> {
153        self.failure.map_or(Ok(()), Err)
154    }
155
156    fn record<T>(&mut self, result: Result<T>) -> Result<T> {
157        if let Err(error) = result {
158            self.failure = Some(error);
159            self.checksum = None;
160        }
161        result
162    }
163
164    fn begin(&mut self, entry: CompressedEntry, output: &mut impl Write) -> Result<()> {
165        self.ready()?;
166        if self.active.is_some()
167            || self.segment.is_some()
168            || self.remaining == 0
169            || self.checksum.is_some()
170        {
171            return Err(Error::InvalidRequest);
172        }
173        if entry.id.format() != self.format {
174            return Err(Error::InvalidObjectId);
175        }
176        if entry.compressed_size < 8 {
177            return Err(Error::InvalidObject);
178        }
179        let mut storage = [0; 64];
180        let mut header = SliceWriter::new(&mut storage);
181        match entry.kind {
182            CompressedKind::Object(kind) => {
183                write_typed_header(kind as u8, entry.inflated_size, &mut header)?
184            }
185            CompressedKind::Delta { base_id } => {
186                if base_id.format() != self.format {
187                    return Err(Error::InvalidObjectId);
188                }
189                write_typed_header(7, entry.inflated_size, &mut header)?;
190                header.write_all(base_id.as_bytes())?;
191            }
192        }
193        let bytes = header.finish();
194        let end = self
195            .position
196            .checked_add(bytes.len() as u64)
197            .and_then(|end| end.checked_add(entry.compressed_size))
198            .and_then(|end| end.checked_add(self.format.id_length() as u64))
199            .ok_or(Error::LimitExceeded)?;
200        if end > self.limit {
201            return Err(Error::LimitExceeded);
202        }
203        output.write_all(bytes)?;
204        self.hash.update(bytes);
205        let mut crc = Crc32::new();
206        crc.update(bytes);
207        self.active = Some(Active {
208            id: entry.id,
209            offset: self.position,
210            remaining: entry.compressed_size,
211            crc,
212        });
213        self.position += bytes.len() as u64;
214        Ok(())
215    }
216
217    fn copy(&mut self, bytes: &[u8], output: &mut impl Write) -> Result<()> {
218        self.ready()?;
219        let active = self.active.as_mut().ok_or(Error::InvalidRequest)?;
220        if bytes.len() as u64 > active.remaining {
221            return Err(Error::InvalidObject);
222        }
223        output.write_all(bytes)?;
224        self.hash.update(bytes);
225        active.crc.update(bytes);
226        active.remaining -= bytes.len() as u64;
227        self.position += bytes.len() as u64;
228        Ok(())
229    }
230
231    fn close_entry(&mut self) -> Result<IndexEntry> {
232        self.ready()?;
233        let active = self.active.as_ref().ok_or(Error::InvalidRequest)?;
234        if active.remaining != 0 {
235            return Err(Error::InvalidObject);
236        }
237        let entry = IndexEntry {
238            id: active.id,
239            offset: active.offset,
240            crc32: active.crc.value(),
241        };
242        self.remaining -= 1;
243        self.active = None;
244        Ok(entry)
245    }
246
247    fn begin_segment(&mut self, objects: u32, bytes: u64) -> Result<()> {
248        self.ready()?;
249        if self.active.is_some()
250            || self.segment.is_some()
251            || self.checksum.is_some()
252            || objects > self.remaining
253            || (objects == 0) != (bytes == 0)
254        {
255            return Err(Error::InvalidRequest);
256        }
257        let end = self
258            .position
259            .checked_add(bytes)
260            .and_then(|end| end.checked_add(self.format.id_length() as u64))
261            .ok_or(Error::LimitExceeded)?;
262        if end > self.limit {
263            return Err(Error::LimitExceeded);
264        }
265        self.segment = Some(Segment {
266            objects,
267            remaining: bytes,
268        });
269        Ok(())
270    }
271
272    fn copy_segment(&mut self, bytes: &[u8], output: &mut impl Write) -> Result<()> {
273        self.ready()?;
274        let segment = self.segment.as_mut().ok_or(Error::InvalidRequest)?;
275        if bytes.len() as u64 > segment.remaining {
276            return Err(Error::InvalidObject);
277        }
278        output.write_all(bytes)?;
279        self.hash.update(bytes);
280        self.position += bytes.len() as u64;
281        segment.remaining -= bytes.len() as u64;
282        Ok(())
283    }
284
285    fn close(&mut self, output: &mut impl Write) -> Result<ObjectId> {
286        self.ready()?;
287        if let Some(id) = self.checksum {
288            return Ok(id);
289        }
290        if self.remaining != 0 || self.active.is_some() || self.segment.is_some() {
291            return Err(Error::InvalidObject);
292        }
293        let id = self.hash.clone().finish();
294        output.write_all(id.as_bytes())?;
295        self.position += id.as_bytes().len() as u64;
296        self.checksum = Some(id);
297        Ok(id)
298    }
299}