Skip to main content

gix_pack/data/output/entry/
mod.rs

1use std::io::Write;
2
3use gix_hash::ObjectId;
4
5use crate::{data, data::output, find};
6
7///
8pub mod iter_from_counts;
9pub use iter_from_counts::function::iter_from_counts;
10
11/// The kind of pack entry to be written
12#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum Kind {
15    /// A complete base object, including its kind
16    Base(gix_object::Kind),
17    /// A delta against the object with the given index. It's always an index that was already encountered to refer only
18    /// to object we have written already.
19    DeltaRef {
20        /// The absolute index to the object to serve as base. It's up to the writer to maintain enough state to allow producing
21        /// a packed delta object from it.
22        object_index: usize,
23    },
24    /// A delta against the given object as identified by its `ObjectId`.
25    /// This is the case for thin packs only, i.e. those that are sent over the wire.
26    /// Note that there is the option of the `ObjectId` being used to refer to an object within
27    /// the same pack, but it's a discontinued practice which won't be encountered here.
28    DeltaOid {
29        /// The object serving as base for this delta
30        id: ObjectId,
31    },
32}
33
34/// The error returned by [`output::Entry::from_data()`].
35#[expect(missing_docs)]
36#[derive(Debug, thiserror::Error)]
37pub enum Error {
38    #[error("{0}")]
39    ZlibDeflate(#[from] std::io::Error),
40    #[error(transparent)]
41    EntryType(#[from] crate::data::entry::decode::Error),
42}
43
44impl output::Entry {
45    /// An object which can be identified as invalid easily which happens if objects didn't exist even if they were referred to.
46    pub fn invalid() -> output::Entry {
47        output::Entry {
48            id: gix_hash::Kind::shortest().null(), // NOTE: the actual object hash used in the repo doesn't matter here, this is a sentinel value.
49            kind: Kind::Base(gix_object::Kind::Blob),
50            decompressed_size: 0,
51            compressed_data: vec![],
52        }
53    }
54
55    /// Returns true if this object doesn't really exist but still has to be handled responsibly
56    ///
57    /// Note that this is true for tree entries that are commits/git submodules, or for objects which aren't present in our local clone
58    /// due to shallow clones.
59    pub fn is_invalid(&self) -> bool {
60        self.id.is_null()
61    }
62
63    /// Create an Entry from a previously counted object which is located in a pack. It's `entry` is provided here.
64    /// The `version` specifies what kind of target `Entry` version the caller desires.
65    pub fn from_pack_entry(
66        mut entry: find::Entry,
67        count: &output::Count,
68        potential_bases: &[output::Count],
69        bases_index_offset: usize,
70        pack_offset_to_oid: Option<impl FnMut(u32, u64) -> Option<ObjectId>>,
71        target_version: data::Version,
72    ) -> Option<Result<Self, Error>> {
73        if entry.version != target_version {
74            return None;
75        }
76
77        let pack_offset_must_be_zero = 0;
78        let pack_entry = match data::Entry::from_bytes(&entry.data, pack_offset_must_be_zero, count.id.kind()) {
79            Ok(e) => e,
80            Err(err) => return Some(Err(err.into())),
81        };
82
83        use crate::data::entry::Header::*;
84        match pack_entry.header {
85            Commit => Some(output::entry::Kind::Base(gix_object::Kind::Commit)),
86            Tree => Some(output::entry::Kind::Base(gix_object::Kind::Tree)),
87            Blob => Some(output::entry::Kind::Base(gix_object::Kind::Blob)),
88            Tag => Some(output::entry::Kind::Base(gix_object::Kind::Tag)),
89            OfsDelta { base_distance } => {
90                let pack_location = count.entry_pack_location.as_ref().expect("packed");
91                let base_offset = pack_location
92                    .pack_offset
93                    .checked_sub(base_distance)
94                    .expect("pack-offset - distance is firmly within the pack");
95                potential_bases
96                    .binary_search_by(|e| {
97                        e.entry_pack_location
98                            .as_ref()
99                            .expect("packed")
100                            .pack_offset
101                            .cmp(&base_offset)
102                    })
103                    .ok()
104                    .map(|idx| output::entry::Kind::DeltaRef {
105                        object_index: idx + bases_index_offset,
106                    })
107                    .or_else(|| {
108                        pack_offset_to_oid
109                            .and_then(|mut f| f(pack_location.pack_id, base_offset))
110                            .map(|id| output::entry::Kind::DeltaOid { id })
111                    })
112            }
113            RefDelta { base_id: _ } => None, // ref deltas are for thin packs or legacy, repack them as base objects
114        }
115        .map(|kind| {
116            Ok(output::Entry {
117                id: count.id.to_owned(),
118                kind,
119                decompressed_size: pack_entry.decompressed_size as usize,
120                compressed_data: {
121                    entry.data.copy_within(pack_entry.data_offset as usize.., 0);
122                    entry.data.resize(
123                        entry.data.len()
124                            - usize::try_from(pack_entry.data_offset).expect("offset representable as usize"),
125                        0,
126                    );
127                    entry.data
128                },
129            })
130        })
131    }
132
133    /// Create a new instance from the given `oid` and its corresponding git object data `obj`,
134    /// deflating it with `compression`.
135    ///
136    /// Note that `git` compresses pack entries with the level configured with `pack.compression`,
137    /// whose default is [`Compression::DEFAULT`](gix_zlib::Compression::DEFAULT).
138    pub fn from_data(
139        count: &output::Count,
140        obj: &gix_object::Data<'_>,
141        compression: gix_zlib::Compression,
142    ) -> Result<Self, Error> {
143        Ok(output::Entry {
144            id: count.id.to_owned(),
145            kind: Kind::Base(obj.kind),
146            decompressed_size: obj.data.len(),
147            compressed_data: {
148                let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), compression);
149                if let Err(err) = std::io::copy(&mut &*obj.data, &mut out) {
150                    match err.kind() {
151                        std::io::ErrorKind::Other => return Err(Error::ZlibDeflate(err)),
152                        err => unreachable!("Should never see other errors than zlib, but got {:?}", err),
153                    }
154                }
155                out.flush()?;
156                out.into_inner()
157            },
158        })
159    }
160
161    /// Transform ourselves into pack entry header of `version` which can be written into a pack.
162    ///
163    /// `index_to_pack(object_index) -> pack_offset` is a function to convert the base object's index into
164    /// the input object array (if each object is numbered) to an offset into the pack.
165    /// This information is known to the one calling the method.
166    pub fn to_entry_header(
167        &self,
168        version: data::Version,
169        index_to_base_distance: impl FnOnce(usize) -> u64,
170    ) -> data::entry::Header {
171        assert!(
172            matches!(version, data::Version::V2),
173            "we can only write V2 pack entries for now"
174        );
175
176        use Kind::*;
177        match self.kind {
178            Base(kind) => {
179                use gix_object::Kind::*;
180                match kind {
181                    Tree => data::entry::Header::Tree,
182                    Blob => data::entry::Header::Blob,
183                    Commit => data::entry::Header::Commit,
184                    Tag => data::entry::Header::Tag,
185                }
186            }
187            DeltaOid { id } => data::entry::Header::RefDelta { base_id: id.to_owned() },
188            DeltaRef { object_index } => data::entry::Header::OfsDelta {
189                base_distance: index_to_base_distance(object_index),
190            },
191        }
192    }
193}