Skip to main content

git_zero_core/pack/
index_lookup.rs

1//! Bounded random access to authenticated, immutable Git version-2 indexes.
2use super::IndexEntry;
3use crate::{Error, ObjectFormat, ObjectId, Result};
4
5/// An exact range to read from the same immutable index used for the header.
6/// No read exceeds 32 bytes; hosts may cache/coalesce larger storage pages.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct IndexRange {
9    pub offset: u64,
10    pub length: usize,
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum LookupStatus {
15    Read(IndexRange),
16    Found { row: u32, entry: IndexEntry },
17    Missing,
18}
19
20/// The 1032-byte fanout/header of a previously authenticated v2 index.
21/// Parsing checks layout and fanout shape. It does NOT verify the full index
22/// checksum, ID ordering, or its relationship to pack contents. Establish those
23/// when importing/creating the index, before trusting lookup results or absence.
24/// The header and every range reply must refer to that same immutable snapshot.
25pub struct IndexHeader {
26    fanout: [u32; 256],
27    format: ObjectFormat,
28    crc_start: u64,
29    offset_start: u64,
30    large_start: u64,
31    large_count: u32,
32    pack_end: u64,
33}
34
35impl IndexHeader {
36    pub const BYTES: usize = 1032;
37
38    pub fn parse(
39        format: ObjectFormat,
40        header: &[u8],
41        index_bytes: u64,
42        pack_bytes: u64,
43    ) -> Result<Self> {
44        if header.len() != Self::BYTES || &header[..8] != b"\xfftOc\0\0\0\x02" {
45            return Err(Error::CorruptPack);
46        }
47        let mut fanout = [0; 256];
48        let mut previous = 0;
49        for (slot, bytes) in fanout.iter_mut().zip(header[8..].chunks_exact(4)) {
50            *slot = number(bytes);
51            if *slot < previous {
52                return Err(Error::CorruptPack);
53            }
54            previous = *slot;
55        }
56        let count = u64::from(previous);
57        let crc_start = Self::BYTES as u64 + count * format.id_length() as u64;
58        let offset_start = crc_start + count * 4;
59        let large_start = offset_start + count * 4;
60        let table_bytes = index_bytes
61            .checked_sub(large_start + 2 * format.id_length() as u64)
62            .ok_or(Error::CorruptPack)?;
63        if table_bytes % 8 != 0 || table_bytes / 8 > count || table_bytes / 8 > 0x8000_0000 {
64            return Err(Error::CorruptPack);
65        }
66        let pack_end = pack_bytes
67            .checked_sub(format.id_length() as u64)
68            .filter(|end| *end >= 12)
69            .ok_or(Error::CorruptPack)?;
70        Ok(Self {
71            fanout,
72            format,
73            crc_start,
74            offset_start,
75            large_start,
76            large_count: (table_bytes / 8) as u32,
77            pack_end,
78        })
79    }
80
81    pub fn object_count(&self) -> u32 {
82        self.fanout[255]
83    }
84
85    /// Start a fanout-limited binary search. The lookup owns only fixed state,
86    /// so unrelated lookups can interleave storage operations without a lock.
87    pub fn lookup(&self, id: ObjectId) -> Result<IndexLookup> {
88        if id.format() != self.format {
89            return Err(Error::InvalidObjectId);
90        }
91        let first = usize::from(id.as_bytes()[0]);
92        let lower = if first == 0 {
93            0
94        } else {
95            self.fanout[first - 1]
96        };
97        let upper = self.fanout[first];
98        let mut lookup = IndexLookup {
99            id,
100            lower,
101            upper,
102            row: 0,
103            crc32: 0,
104            crc_start: self.crc_start,
105            offset_start: self.offset_start,
106            large_start: self.large_start,
107            large_count: self.large_count,
108            pack_end: self.pack_end,
109            state: State::Missing,
110        };
111        lookup.search();
112        Ok(lookup)
113    }
114}
115
116#[derive(Clone, Copy)]
117enum State {
118    Id(IndexRange),
119    Crc(IndexRange),
120    Offset(IndexRange),
121    Large(IndexRange),
122    Found(IndexEntry),
123    Missing,
124    Failed(Error),
125}
126
127/// A resumable index lookup. Fetch `status()`'s range, then supply its exact
128/// bytes to `update`. A storage error can be retried without advancing state.
129/// Malformed replies are terminal. There is no allocation or provider callback.
130pub struct IndexLookup {
131    id: ObjectId,
132    lower: u32,
133    upper: u32,
134    row: u32,
135    crc32: u32,
136    crc_start: u64,
137    offset_start: u64,
138    large_start: u64,
139    large_count: u32,
140    pack_end: u64,
141    state: State,
142}
143
144impl IndexLookup {
145    pub fn status(&self) -> Result<LookupStatus> {
146        Ok(match self.state {
147            State::Id(range) | State::Crc(range) | State::Offset(range) | State::Large(range) => {
148                LookupStatus::Read(range)
149            }
150            State::Found(entry) => LookupStatus::Found {
151                row: self.row,
152                entry,
153            },
154            State::Missing => LookupStatus::Missing,
155            State::Failed(error) => return Err(error),
156        })
157    }
158
159    pub fn update(&mut self, bytes: &[u8]) -> Result<LookupStatus> {
160        let result = self.reply(bytes).and_then(|()| self.status());
161        if let Err(error) = result {
162            self.state = State::Failed(error);
163        }
164        result
165    }
166
167    fn search(&mut self) {
168        if self.lower == self.upper {
169            self.state = State::Missing;
170            return;
171        }
172        self.row = self.lower + (self.upper - self.lower) / 2;
173        self.state = State::Id(IndexRange {
174            offset: IndexHeader::BYTES as u64
175                + u64::from(self.row) * self.id.format().id_length() as u64,
176            length: self.id.format().id_length(),
177        });
178    }
179
180    fn reply(&mut self, bytes: &[u8]) -> Result<()> {
181        let LookupStatus::Read(range) = self.status()? else {
182            return if bytes.is_empty() {
183                Ok(())
184            } else {
185                Err(Error::CorruptPack)
186            };
187        };
188        if bytes.len() != range.length {
189            return Err(Error::CorruptPack);
190        }
191        match self.state {
192            State::Id(_) => self.candidate(bytes)?,
193            State::Crc(_) => {
194                self.crc32 = number(bytes);
195                self.state = State::Offset(IndexRange {
196                    offset: self.offset_start + u64::from(self.row) * 4,
197                    length: 4,
198                });
199            }
200            State::Offset(_) => {
201                let offset = number(bytes);
202                if offset & 0x8000_0000 == 0 {
203                    self.found(u64::from(offset))?;
204                } else {
205                    let index = offset & 0x7fff_ffff;
206                    if index >= self.large_count {
207                        return Err(Error::CorruptPack);
208                    }
209                    self.state = State::Large(IndexRange {
210                        offset: self.large_start + u64::from(index) * 8,
211                        length: 8,
212                    });
213                }
214            }
215            State::Large(_) => self.found(u64::from_be_bytes(
216                bytes.try_into().map_err(|_| Error::CorruptPack)?,
217            ))?,
218            _ => unreachable!(),
219        }
220        Ok(())
221    }
222
223    fn candidate(&mut self, bytes: &[u8]) -> Result<()> {
224        if bytes[0] != self.id.as_bytes()[0] {
225            return Err(Error::CorruptPack);
226        }
227        match bytes.cmp(self.id.as_bytes()) {
228            core::cmp::Ordering::Less => {
229                self.lower = self.row + 1;
230                self.search();
231            }
232            core::cmp::Ordering::Greater => {
233                self.upper = self.row;
234                self.search();
235            }
236            core::cmp::Ordering::Equal => {
237                self.state = State::Crc(IndexRange {
238                    offset: self.crc_start + u64::from(self.row) * 4,
239                    length: 4,
240                });
241            }
242        }
243        Ok(())
244    }
245
246    fn found(&mut self, offset: u64) -> Result<()> {
247        if offset < 12 || offset >= self.pack_end {
248            return Err(Error::CorruptPack);
249        }
250        self.state = State::Found(IndexEntry {
251            id: self.id,
252            offset,
253            crc32: self.crc32,
254        });
255        Ok(())
256    }
257}
258
259fn number(bytes: &[u8]) -> u32 {
260    // Internal callers validate width before reaching this helper.
261    u32::from_be_bytes(bytes.try_into().expect("four-byte index field"))
262}