Skip to main content

git_zero_core/cache/
selected.rs

1//! Cache raw packs by their exact selected object set.
2//!
3//! Before every call, including hits, hosts must complete authorization,
4//! visible-ref/reachability checks, selection and traversal budgets. ACK-only
5//! rounds and shallow-list probes do not use this cache. Generate their control
6//! frames separately: cached bytes contain only PACK data.
7
8use crate::{
9    Error, ObjectFormat, ObjectId, Repository, Result,
10    io::{SliceWriter, Write},
11    pack,
12};
13
14mod fixed;
15mod stored;
16pub use fixed::{FixedCache, Statistics};
17
18/// Explicit pack selection. A server's absent selection can mean an ACK-only
19/// round or probe, so callers must decide that a pack is required before using
20/// this type. `Indices(&[])` deliberately encodes an empty pack.
21#[derive(Clone, Copy, Debug)]
22pub enum Selection<'a> {
23    All,
24    Indices(&'a [usize]),
25}
26
27impl<'a> Selection<'a> {
28    fn indices(self) -> Option<&'a [usize]> {
29        match self {
30            Self::All => None,
31            Self::Indices(indices) => Some(indices),
32        }
33    }
34}
35
36/// Stable, owned identity for a stored, non-delta selected pack. Serialization
37/// uses explicit numeric codes and big-endian integers, independent of Rust ABI.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct Key([u8; 48]);
40
41impl Key {
42    /// Require strictly ascending selected indices, as returned by repository
43    /// selection. `All` selects every object. IDs are canonicalized in caller
44    /// storage, so equivalent sets have the same key across enumeration orders.
45    /// Metadata, format, count, indices and duplicate IDs fail before cache I/O.
46    pub fn derive(
47        repository: &impl Repository,
48        selection: Selection<'_>,
49        ids: &mut [ObjectId],
50    ) -> Result<Self> {
51        let indices = selection.indices();
52        let count = indices.map_or(repository.object_count(), <[usize]>::len);
53        let count32 = u32::try_from(count).map_err(|_| Error::LimitExceeded)?;
54        let ids = ids.get_mut(..count).ok_or(Error::BufferTooSmall)?;
55        if indices.is_some_and(|indices| indices.windows(2).any(|pair| pair[0] >= pair[1])) {
56            return Err(Error::InvalidObject);
57        }
58        let format = repository.object_format();
59        for (position, slot) in ids.iter_mut().enumerate() {
60            let index = indices.map_or(position, |indices| indices[position]);
61            if index >= repository.object_count() {
62                return Err(Error::ObjectNotFound);
63            }
64            repository.object_info(index)?;
65            *slot = repository.object_id(index).ok_or(Error::ObjectNotFound)?;
66            if slot.format() != format {
67                return Err(Error::InvalidObjectId);
68            }
69        }
70        Self::from_ids(format, count32, ids)
71    }
72
73    /// Parse exactly 48 bytes. Unknown schemas, codecs, revisions, formats and
74    /// reserved bits are rejected, rather than sharing a future encoder's keys.
75    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
76        let bytes: [u8; 48] = bytes.try_into().map_err(|_| Error::InvalidRequest)?;
77        if &bytes[..5] != b"GZPC\x01"
78            || !matches!(bytes[5], 1 | 2)
79            || bytes[6..12] != [0, 0, 0, 0, 0, 1]
80        {
81            return Err(Error::Unsupported);
82        }
83        Ok(Self(bytes))
84    }
85
86    pub fn as_bytes(&self) -> &[u8; 48] {
87        &self.0
88    }
89    pub fn object_format(self) -> ObjectFormat {
90        if self.0[5] == 1 {
91            ObjectFormat::Sha1
92        } else {
93            ObjectFormat::Sha256
94        }
95    }
96    pub fn object_count(self) -> u32 {
97        u32::from_be_bytes(self.0[12..16].try_into().unwrap())
98    }
99
100    /// Verify framing, checksums, canonical object IDs and exact unique set
101    /// before any bytes reach an output sink. Requires one caller ID per object;
102    /// no inflated-body buffer or DEFLATE window is needed for this codec.
103    pub fn verify_pack(self, bytes: &[u8], ids: &mut [ObjectId], byte_limit: usize) -> Result<()> {
104        if bytes.len() > byte_limit {
105            return Err(Error::LimitExceeded);
106        }
107        let count = usize::try_from(self.object_count()).map_err(|_| Error::LimitExceeded)?;
108        let ids = ids.get_mut(..count).ok_or(Error::BufferTooSmall)?;
109        stored::read(bytes, self.object_format(), ids)?;
110        if Self::from_ids(self.object_format(), self.object_count(), ids)? != self {
111            return Err(Error::CorruptPack);
112        }
113        Ok(())
114    }
115
116    fn from_ids(format: ObjectFormat, count: u32, ids: &mut [ObjectId]) -> Result<Self> {
117        ids.sort_unstable();
118        if ids.windows(2).any(|pair| pair[0] == pair[1]) {
119            return Err(Error::InvalidObject);
120        }
121        let mut bytes = [0; 48];
122        bytes[..5].copy_from_slice(b"GZPC\x01");
123        bytes[5] = match format {
124            ObjectFormat::Sha1 => 1,
125            ObjectFormat::Sha256 => 2,
126        };
127        bytes[8..12].copy_from_slice(&1u32.to_be_bytes());
128        bytes[12..16].copy_from_slice(&count.to_be_bytes());
129        let mut hash = ObjectFormat::Sha256.hash();
130        hash.update(b"git-zero/selected-pack/v1\0");
131        hash.update(&bytes[..16]);
132        for id in ids {
133            hash.update(id.as_bytes());
134        }
135        bytes[16..].copy_from_slice(hash.finish().as_bytes());
136        Ok(Self(bytes))
137    }
138}
139
140/// Hosts own persistence, eviction and concurrency. A failed insertion must
141/// preserve the old entry. Successful insertion publishes complete immutable
142/// bytes atomically. Returned bytes are always verified by `write` before use.
143pub trait PackCache {
144    fn get(&mut self, key: Key) -> Result<Option<&[u8]>>;
145    fn put(&mut self, key: Key, pack: &[u8]) -> Result<()>;
146}
147
148impl PackCache for super::NoCache {
149    fn get(&mut self, _: Key) -> Result<Option<&[u8]>> {
150        Ok(None)
151    }
152    fn put(&mut self, _: Key, _: &[u8]) -> Result<()> {
153        Ok(())
154    }
155}
156
157/// Caller-owned storage. Hits use IDs and `byte_limit` but do not need pack or
158/// read scratch. Misses are bounded by both pack storage and `byte_limit`.
159pub struct Buffers<'a> {
160    pub ids: &'a mut [ObjectId],
161    pub pack: &'a mut [u8],
162    pub read: &'a mut [u8],
163    pub byte_limit: usize,
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub enum Outcome {
168    Hit,
169    Miss,
170}
171
172/// Key/preflight errors, backend errors and corruption propagate with no
173/// output. A miss is encoded and verified before insertion. Sink errors may
174/// leave partial output and a valid inserted entry; discard that output and
175/// retry. Key/integrity work is O(pack bytes + n log n), with n caller ID slots;
176/// graph selection and authorization happen separately before this function.
177pub fn write(
178    repository: &impl Repository,
179    selection: Selection<'_>,
180    cache: &mut impl PackCache,
181    buffers: Buffers<'_>,
182    output: &mut impl Write,
183) -> Result<Outcome> {
184    let key = Key::derive(repository, selection, buffers.ids)?;
185    if let Some(bytes) = cache.get(key)? {
186        key.verify_pack(bytes, buffers.ids, buffers.byte_limit)?;
187        output.write_all(bytes)?;
188        return Ok(Outcome::Hit);
189    }
190    let capacity = buffers.pack.len().min(buffers.byte_limit);
191    let mut staging = SliceWriter::new(&mut buffers.pack[..capacity]);
192    match selection {
193        Selection::Indices(indices) => {
194            pack::write_selected_with_scratch(repository, indices, buffers.read, &mut staging)?
195        }
196        Selection::All => pack::write_with_scratch(repository, buffers.read, &mut staging)?,
197    };
198    let bytes = staging.finish();
199    key.verify_pack(bytes, buffers.ids, buffers.byte_limit)?;
200    cache.put(key, bytes)?;
201    output.write_all(bytes)?;
202    Ok(Outcome::Miss)
203}