Skip to main content

git_internal/internal/pack/
index_entry.rs

1//! Representation of a single `.idx` entry including precomputed CRC32 and offset extraction from
2//! decoded pack metadata.
3
4use crc32fast::Hasher;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    errors::GitError,
9    hash::ObjectHash,
10    internal::{
11        metadata::{EntryMeta, MetaAttached},
12        pack::entry::Entry,
13    },
14};
15
16/// Git index entry corresponding to a pack entry
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct IndexEntry {
19    pub hash: ObjectHash,
20    pub crc32: u32,
21    pub offset: u64, // 64-bit because offsets may exceed 32-bit
22}
23
24impl TryFrom<&MetaAttached<Entry, EntryMeta>> for IndexEntry {
25    type Error = GitError;
26
27    fn try_from(pack_entry: &MetaAttached<Entry, EntryMeta>) -> Result<Self, GitError> {
28        let offset = pack_entry
29            .meta
30            .pack_offset
31            .ok_or(GitError::ConversionError(String::from(
32                "empty offset in pack entry",
33            )))?;
34        // Use the CRC32 from metadata if available (calculated from compressed data),
35        // otherwise fallback to calculating it from decompressed data (which is technically wrong for .idx but handles legacy cases)
36        let crc32 = pack_entry
37            .meta
38            .crc32
39            .unwrap_or_else(|| calculate_crc32(&pack_entry.inner.data));
40        Ok(IndexEntry {
41            hash: pack_entry.inner.hash,
42            crc32,
43            offset: offset as u64,
44        })
45    }
46}
47
48impl IndexEntry {
49    /// Create a new IndexEntry from a pack Entry and its offset in the pack file.
50    pub fn new(entry: &Entry, offset: usize) -> Self {
51        IndexEntry {
52            hash: entry.hash,
53            crc32: calculate_crc32(&entry.data),
54            offset: offset as u64,
55        }
56    }
57}
58
59fn calculate_crc32(bytes: &[u8]) -> u32 {
60    let mut hasher = Hasher::new();
61    hasher.update(bytes);
62    hasher.finalize()
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::{
69        hash::{HashKind, ObjectHash, set_hash_kind_for_test},
70        internal::{
71            metadata::{EntryMeta, MetaAttached},
72            object::types::ObjectType,
73            pack::entry::Entry,
74        },
75    };
76
77    /// Helper to create a test Entry with given content.
78    fn create_test_entry(content: &[u8]) -> Entry {
79        Entry {
80            obj_type: ObjectType::Blob,
81            data: content.to_vec(),
82            hash: ObjectHash::new(content),
83            chain_len: 0,
84        }
85    }
86
87    #[test]
88    fn test_index_entry_new() {
89        let _guard = set_hash_kind_for_test(HashKind::Sha1);
90        let entry = create_test_entry(b"test data");
91        let offset = 123;
92
93        let index_entry = IndexEntry::new(&entry, offset);
94
95        assert_eq!(index_entry.hash, entry.hash);
96        assert_eq!(index_entry.offset, offset as u64);
97
98        let mut hasher = Hasher::new();
99        hasher.update(b"test data");
100        assert_eq!(index_entry.crc32, hasher.finalize());
101    }
102
103    #[test]
104    fn test_try_from_meta_attached_with_crc() {
105        let _guard = set_hash_kind_for_test(HashKind::Sha1);
106        let entry = create_test_entry(b"test data");
107        let meta = EntryMeta {
108            pack_offset: Some(456),
109            crc32: Some(0x12345678),
110            ..Default::default()
111        };
112        let meta_attached = MetaAttached { inner: entry, meta };
113
114        let index_entry = IndexEntry::try_from(&meta_attached).unwrap();
115
116        assert_eq!(index_entry.hash, meta_attached.inner.hash);
117        assert_eq!(index_entry.offset, 456);
118        assert_eq!(index_entry.crc32, 0x12345678);
119    }
120
121    #[test]
122    fn test_try_from_meta_attached_crc_fallback() {
123        let _guard = set_hash_kind_for_test(HashKind::Sha1);
124        let entry_data = b"fallback crc";
125        let entry = create_test_entry(entry_data);
126        let meta = EntryMeta {
127            pack_offset: Some(789),
128            crc32: None, // CRC is not provided in meta
129            ..Default::default()
130        };
131        let meta_attached = MetaAttached { inner: entry, meta };
132
133        let index_entry = IndexEntry::try_from(&meta_attached).unwrap();
134
135        assert_eq!(index_entry.hash, meta_attached.inner.hash);
136        assert_eq!(index_entry.offset, 789);
137
138        let mut hasher = Hasher::new();
139        hasher.update(entry_data);
140        assert_eq!(index_entry.crc32, hasher.finalize());
141    }
142
143    #[test]
144    fn test_try_from_meta_attached_no_offset() {
145        let _guard = set_hash_kind_for_test(HashKind::Sha1);
146        let entry = create_test_entry(b"no offset");
147        let meta = EntryMeta {
148            pack_offset: None, // Offset is not provided
149            ..Default::default()
150        };
151        let meta_attached = MetaAttached { inner: entry, meta };
152
153        let result = IndexEntry::try_from(&meta_attached);
154        assert!(result.is_err());
155        match result.unwrap_err() {
156            GitError::ConversionError(msg) => {
157                assert_eq!(msg, "empty offset in pack entry");
158            }
159            _ => panic!("Expected ConversionError"),
160        }
161    }
162}