Skip to main content

gix_pack/data/entry/
header.rs

1use std::io;
2
3use super::{BLOB, COMMIT, OFS_DELTA, REF_DELTA, TAG, TREE};
4use crate::data;
5
6/// The header portion of a pack data entry, identifying the kind of stored object.
7#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[expect(missing_docs)]
10pub enum Header {
11    /// The object is a commit
12    Commit,
13    /// The object is a tree
14    Tree,
15    /// The object is a blob
16    Blob,
17    /// The object is a tag
18    Tag,
19    /// Describes a delta-object which needs to be applied to a base identified by `base_id`.
20    /// The base may occur anywhere in the same pack or in the parent repository, as it does in a **thin-pack**.
21    ///
22    /// **The naming** is exactly the same as the canonical implementation uses, namely **REF_DELTA**.
23    RefDelta { base_id: gix_hash::ObjectId },
24    /// Describes a delta-object present in this pack which acts as base for this object.
25    /// The base object is measured as a distance from this objects
26    /// pack offset, so that `base_pack_offset = this_objects_pack_offset - base_distance`
27    ///
28    /// # Note
29    ///
30    /// **The naming** is exactly the same as the canonical implementation uses, namely **OFS_DELTA**.
31    OfsDelta { base_distance: u64 },
32}
33
34impl Header {
35    /// Subtract `distance` from `pack_offset` safely without the chance for overflow or no-ops if `distance` is 0.
36    pub fn verified_base_pack_offset(pack_offset: data::Offset, distance: u64) -> Option<data::Offset> {
37        if distance == 0 {
38            return None;
39        }
40        pack_offset.checked_sub(distance)
41    }
42    /// Convert the header's object kind into [`gix_object::Kind`] if possible
43    pub fn as_kind(&self) -> Option<gix_object::Kind> {
44        use gix_object::Kind::*;
45        Some(match self {
46            Header::Tree => Tree,
47            Header::Blob => Blob,
48            Header::Commit => Commit,
49            Header::Tag => Tag,
50            Header::RefDelta { .. } | Header::OfsDelta { .. } => return None,
51        })
52    }
53    /// Convert this header's object kind into the packs internal representation
54    pub fn as_type_id(&self) -> u8 {
55        use Header::*;
56        match self {
57            Blob => BLOB,
58            Tree => TREE,
59            Commit => COMMIT,
60            Tag => TAG,
61            OfsDelta { .. } => OFS_DELTA,
62            RefDelta { .. } => REF_DELTA,
63        }
64    }
65    /// Return's true if this is a delta object, i.e. not a full object.
66    pub fn is_delta(&self) -> bool {
67        matches!(self, Header::OfsDelta { .. } | Header::RefDelta { .. })
68    }
69    /// Return's true if this is a base object, i.e. not a delta object.
70    pub fn is_base(&self) -> bool {
71        !self.is_delta()
72    }
73}
74
75impl Header {
76    /// Encode this header along the given `decompressed_size_in_bytes` into the `out` write stream for use within a data pack.
77    ///
78    /// Returns the amount of bytes written to `out`.
79    /// `decompressed_size_in_bytes` is the full size in bytes of the object that this header represents
80    pub fn write_to(&self, decompressed_size_in_bytes: u64, out: &mut dyn io::Write) -> io::Result<usize> {
81        let mut size = decompressed_size_in_bytes;
82        let mut written = 1;
83        let mut c: u8 = (self.as_type_id() << 4) | (size as u8 & 0b0000_1111);
84        size >>= 4;
85        while size != 0 {
86            out.write_all(&[c | 0b1000_0000])?;
87            written += 1;
88            c = size as u8 & 0b0111_1111;
89            size >>= 7;
90        }
91        out.write_all(&[c])?;
92
93        use Header::*;
94        match self {
95            RefDelta { base_id: oid } => {
96                out.write_all(oid.as_slice())?;
97                written += oid.as_slice().len();
98            }
99            OfsDelta { base_distance } => {
100                let mut buf = [0u8; 10];
101                let buf = leb64_encode(*base_distance, &mut buf);
102                out.write_all(buf)?;
103                written += buf.len();
104            }
105            Blob | Tree | Commit | Tag => {}
106        }
107        Ok(written)
108    }
109
110    /// The size of the header in bytes when written in canonical form.
111    ///
112    /// This is the number of bytes [`Self::write_to()`] would emit for `decompressed_size`.
113    /// It does not inspect existing pack bytes and therefore does not preserve non-canonical
114    /// overlong size encodings. Use [`data::Entry::header_size()`] for decoded entries when the
115    /// result has to match the header length present in the pack.
116    pub fn size(&self, decompressed_size: u64) -> usize {
117        self.write_to(decompressed_size, &mut io::sink())
118            .expect("io::sink() to never fail")
119    }
120}
121
122#[inline]
123fn leb64_encode(mut n: u64, buf: &mut [u8; 10]) -> &[u8] {
124    let mut bytes_written = 1;
125    buf[buf.len() - 1] = n as u8 & 0b0111_1111;
126    for out in buf.iter_mut().rev().skip(1) {
127        n >>= 7;
128        if n == 0 {
129            break;
130        }
131        n -= 1;
132        *out = 0b1000_0000 | (n as u8 & 0b0111_1111);
133        bytes_written += 1;
134    }
135    debug_assert_eq!(n, 0, "BUG: buffer must be large enough to hold a 64 bit integer");
136    &buf[buf.len() - bytes_written..]
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn leb64_encode_max_int() {
145        let mut buf = [0u8; 10];
146        let buf = leb64_encode(u64::MAX, &mut buf);
147        assert_eq!(buf.len(), 10, "10 bytes should be used when 64bits are encoded");
148    }
149}