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::metadata::{EntryMeta, MetaAttached},
71        internal::{object::types::ObjectType, pack::entry::Entry},
72    };
73
74    /// Helper to create a test Entry with given content.
75    fn create_test_entry(content: &[u8]) -> Entry {
76        Entry {
77            obj_type: ObjectType::Blob,
78            data: content.to_vec(),
79            hash: ObjectHash::new(content),
80            chain_len: 0,
81        }
82    }
83
84    #[test]
85    fn test_index_entry_new() {
86        let _guard = set_hash_kind_for_test(HashKind::Sha1);
87        let entry = create_test_entry(b"test data");
88        let offset = 123;
89
90        let index_entry = IndexEntry::new(&entry, offset);
91
92        assert_eq!(index_entry.hash, entry.hash);
93        assert_eq!(index_entry.offset, offset as u64);
94
95        let mut hasher = Hasher::new();
96        hasher.update(b"test data");
97        assert_eq!(index_entry.crc32, hasher.finalize());
98    }
99
100    #[test]
101    fn test_try_from_meta_attached_with_crc() {
102        let _guard = set_hash_kind_for_test(HashKind::Sha1);
103        let entry = create_test_entry(b"test data");
104        let meta = EntryMeta {
105            pack_offset: Some(456),
106            crc32: Some(0x12345678),
107            ..Default::default()
108        };
109        let meta_attached = MetaAttached { inner: entry, meta };
110
111        let index_entry = IndexEntry::try_from(&meta_attached).unwrap();
112
113        assert_eq!(index_entry.hash, meta_attached.inner.hash);
114        assert_eq!(index_entry.offset, 456);
115        assert_eq!(index_entry.crc32, 0x12345678);
116    }
117
118    #[test]
119    fn test_try_from_meta_attached_crc_fallback() {
120        let _guard = set_hash_kind_for_test(HashKind::Sha1);
121        let entry_data = b"fallback crc";
122        let entry = create_test_entry(entry_data);
123        let meta = EntryMeta {
124            pack_offset: Some(789),
125            crc32: None, // CRC is not provided in meta
126            ..Default::default()
127        };
128        let meta_attached = MetaAttached { inner: entry, meta };
129
130        let index_entry = IndexEntry::try_from(&meta_attached).unwrap();
131
132        assert_eq!(index_entry.hash, meta_attached.inner.hash);
133        assert_eq!(index_entry.offset, 789);
134
135        let mut hasher = Hasher::new();
136        hasher.update(entry_data);
137        assert_eq!(index_entry.crc32, hasher.finalize());
138    }
139
140    #[test]
141    fn test_try_from_meta_attached_no_offset() {
142        let _guard = set_hash_kind_for_test(HashKind::Sha1);
143        let entry = create_test_entry(b"no offset");
144        let meta = EntryMeta {
145            pack_offset: None, // Offset is not provided
146            ..Default::default()
147        };
148        let meta_attached = MetaAttached { inner: entry, meta };
149
150        let result = IndexEntry::try_from(&meta_attached);
151        assert!(result.is_err());
152        match result.unwrap_err() {
153            GitError::ConversionError(msg) => {
154                assert_eq!(msg, "empty offset in pack entry");
155            }
156            _ => panic!("Expected ConversionError"),
157        }
158    }
159}