Skip to main content

git_zero_core/
repository.rs

1//! Immutable object/ref providers, HEAD states, ref validation and symbolic resolution.
2//! Fetch graph validation and selection live in `validation`.
3use crate::{Error, Object, ObjectFormat, ObjectId, ObjectInfo, Result};
4
5/// V0 upload capabilities, including their leading separator. Shared with the
6/// server so repository ref-name limits include the complete advertisement.
7pub const UPLOAD_CAPABILITIES: &[u8] =
8    b" multi_ack multi_ack_detailed side-band side-band-64k no-progress shallow deepen-relative filter ofs-delta include-tag";
9
10/// Maximum SHA-1 ref name size that fits a smart HTTP advertisement.
11pub const MAX_REF_NAME_LENGTH: usize = crate::pktline::MAX_LENGTH - 4 - 40 - b" \n".len();
12
13/// Maximum SHA-1 default branch size, including its HEAD capability packet.
14pub const MAX_HEAD_REF_LENGTH: usize = head_ref_limit(ObjectFormat::Sha1);
15
16/// Maximum SHA-1 unborn branch size, including the empty capabilities marker.
17pub const MAX_UNBORN_REF_LENGTH: usize = unborn_ref_limit(ObjectFormat::Sha1);
18
19/// A peeled advertisement appends `^{}` to the stored ref name.
20pub const MAX_PEELED_REF_LENGTH: usize = MAX_REF_NAME_LENGTH - 3;
21
22pub const fn ref_name_limit(format: ObjectFormat) -> usize {
23    crate::pktline::MAX_LENGTH - 4 - format.hex_length() - b" \n".len()
24}
25pub const fn head_ref_limit(format: ObjectFormat) -> usize {
26    ref_name_limit(format)
27        - b"HEAD\0symref=HEAD:".len()
28        - 1
29        - format.capability().len()
30        - UPLOAD_CAPABILITIES.len()
31}
32pub const fn unborn_ref_limit(format: ObjectFormat) -> usize {
33    ref_name_limit(format)
34        - b"capabilities^{}\0symref=HEAD:".len()
35        - 1
36        - format.capability().len()
37        - UPLOAD_CAPABILITIES.len()
38}
39
40/// One resolved advertised ref, with optional symbolic and annotated-tag data.
41/// Symbolic targets must exist, resolve without cycles, and have the same ID.
42/// `peeled` is the final non-tag object ID, including through nested tags.
43#[derive(Clone, Copy, Debug)]
44pub struct Reference<'a> {
45    pub name: &'a str,
46    pub id: ObjectId,
47    pub symbolic_target: Option<&'a str>,
48    pub peeled: Option<ObjectId>,
49}
50
51impl<'a> Reference<'a> {
52    pub const fn direct(name: &'a str, id: ObjectId) -> Self {
53        Self {
54            name,
55            id,
56            symbolic_target: None,
57            peeled: None,
58        }
59    }
60}
61
62/// HEAD's resolved commit and optional symbolic branch target.
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum Head<'a> {
65    Symbolic {
66        target: &'a str,
67        id: ObjectId,
68    },
69    Detached(ObjectId),
70    /// A symbolic branch that has no commit or advertised ref yet.
71    Unborn(&'a str),
72}
73
74impl<'a> Head<'a> {
75    pub fn id(self) -> Option<ObjectId> {
76        match self {
77            Self::Symbolic { id, .. } | Self::Detached(id) => Some(id),
78            Self::Unborn(_) => None,
79        }
80    }
81
82    pub fn target(self) -> Option<&'a str> {
83        match self {
84            Self::Symbolic { target, .. } | Self::Unborn(target) => Some(target),
85            Self::Detached(_) => None,
86        }
87    }
88}
89
90/// An empty repository whose HEAD points at an unborn branch.
91#[derive(Clone, Copy, Debug)]
92pub struct EmptyRepository<'a> {
93    target: &'a str,
94    format: ObjectFormat,
95}
96
97impl<'a> EmptyRepository<'a> {
98    pub fn new(target: &'a str) -> Result<Self> {
99        Self::new_with_format(target, ObjectFormat::Sha1)
100    }
101    pub fn new_with_format(target: &'a str, format: ObjectFormat) -> Result<Self> {
102        let repository = Self { target, format };
103        validate_refs(&repository)?;
104        Ok(repository)
105    }
106}
107
108impl Repository for EmptyRepository<'_> {
109    fn head(&self) -> ObjectId {
110        self.format.zero_id()
111    }
112    fn object_format(&self) -> ObjectFormat {
113        self.format
114    }
115    fn head_state(&self) -> Head<'_> {
116        Head::Unborn(self.target)
117    }
118    fn head_ref(&self) -> &str {
119        self.target
120    }
121    fn object_count(&self) -> usize {
122        0
123    }
124    fn reference_count(&self) -> usize {
125        0
126    }
127    fn reference_name(&self, _index: usize) -> Option<&str> {
128        None
129    }
130}
131
132/// A stable snapshot containing the union reachable from HEAD and advertised refs.
133///
134/// A resolved HEAD and branch tips must refer to commits. An unborn HEAD has no
135/// commit, but other branches or tags may still exist.
136/// Objects must stay stable throughout a request
137/// and across discovery/fetch requests for the same repository URL. Enumeration
138/// must be complete and deterministic; each index below `object_count` must exist.
139/// Implementations may borrow memory or implement `object_info`/`read_object`
140/// for file-backed or generated bodies. Their lengths and IDs must be exact.
141/// Ref names must be
142/// valid, unique, byte-sorted, and include the symbolic HEAD branch, if resolved.
143/// Names must fit `ref_name_limit(object_format())`; symbolic and unborn targets
144/// must fit `head_ref_limit` and `unborn_ref_limit` for the repository format.
145pub trait Repository {
146    /// Resolved commit ID, or the all-zero sentinel for an unborn HEAD.
147    /// Use `head_state().id()` when absence needs to be distinguished.
148    fn head(&self) -> ObjectId;
149    /// Fixed for the entire repository, including unborn HEAD. Defaults to
150    /// SHA-1; SHA-256 providers must override this independently of `object_id`.
151    fn object_format(&self) -> ObjectFormat {
152        ObjectFormat::Sha1
153    }
154    fn head_state(&self) -> Head<'_> {
155        Head::Symbolic {
156            target: self.head_ref(),
157            id: self.head(),
158        }
159    }
160    fn object_count(&self) -> usize;
161    /// Optional borrowed-body fast path. Streaming providers can leave this as
162    /// `None` and implement `object_info`, `read_object`, and `object_id` instead.
163    fn object(&self, _index: usize) -> Option<Object<'_>> {
164        None
165    }
166
167    fn object_info(&self, index: usize) -> Result<ObjectInfo> {
168        self.object(index)
169            .map(|object| object.info())
170            .ok_or(Error::ObjectNotFound)
171    }
172
173    /// Read at most `storage.len()` body bytes starting at a byte offset.
174    /// Short reads are allowed. Return zero only at EOF or for empty storage.
175    /// Calls must be independent of earlier reads, enabling interleaved readers
176    /// and retries. Errors may modify storage but must not change the object.
177    fn read_object(&self, index: usize, offset: u64, storage: &mut [u8]) -> Result<usize> {
178        let object = self.object(index).ok_or(Error::ObjectNotFound)?;
179        let Some(bytes) = usize::try_from(offset)
180            .ok()
181            .and_then(|offset| object.data.get(offset..))
182        else {
183            return Ok(0);
184        };
185        let length = bytes.len().min(storage.len());
186        storage[..length].copy_from_slice(&bytes[..length]);
187        Ok(length)
188    }
189
190    /// Return an object's ID. Streaming providers must supply precomputed IDs;
191    /// `ObjectHasher` computes them incrementally with caller-owned scratch.
192    fn object_id(&self, index: usize) -> Option<ObjectId> {
193        self.object(index)
194            .map(|object| object.id_with_format(self.object_format()))
195    }
196
197    fn head_ref(&self) -> &str {
198        "refs/heads/main"
199    }
200
201    fn reference_count(&self) -> usize {
202        1
203    }
204
205    fn reference_name(&self, index: usize) -> Option<&str> {
206        (index == 0).then(|| self.head_ref())
207    }
208
209    /// Defaults preserve snapshot commit aliases. Override for independent tips.
210    fn reference_id(&self, index: usize) -> Option<ObjectId> {
211        self.reference_name(index)?;
212        self.head_state().id()
213    }
214
215    fn reference_symbolic_target(&self, _index: usize) -> Option<&str> {
216        None
217    }
218
219    fn reference_peeled(&self, _index: usize) -> Option<ObjectId> {
220        None
221    }
222
223    fn find(&self, id: ObjectId) -> Option<Object<'_>> {
224        self.object(self.find_index(id)?)
225    }
226
227    /// Lookup without requiring a borrowed body. Metadata providers can index it.
228    fn find_index(&self, id: ObjectId) -> Option<usize> {
229        (0..self.object_count()).find(|&index| self.object_id(index) == Some(id))
230    }
231}
232
233/// Attach a caller-owned sorted ref manifest and HEAD to an object provider.
234/// Construction validates refs and symbolic links, without reading object bodies.
235/// Use `ValidatedRepository` to check IDs, target kinds, tags and the union graph.
236pub struct RefRepository<'refs, 'repo, R: Repository + ?Sized> {
237    source: &'repo R,
238    head: Head<'refs>,
239    references: &'refs [Reference<'refs>],
240}
241
242impl<'refs, 'repo, R: Repository + ?Sized> RefRepository<'refs, 'repo, R> {
243    pub fn new(
244        source: &'repo R,
245        head: Head<'refs>,
246        references: &'refs [Reference<'refs>],
247    ) -> Result<Self> {
248        let repository = Self {
249            source,
250            head,
251            references,
252        };
253        validate_refs(&repository)?;
254        Ok(repository)
255    }
256}
257
258impl<R: Repository + ?Sized> Repository for RefRepository<'_, '_, R> {
259    fn head(&self) -> ObjectId {
260        self.head
261            .id()
262            .unwrap_or(self.source.object_format().zero_id())
263    }
264    fn object_format(&self) -> ObjectFormat {
265        self.source.object_format()
266    }
267    fn head_state(&self) -> Head<'_> {
268        self.head
269    }
270    fn head_ref(&self) -> &str {
271        self.head.target().unwrap_or("refs/heads/main")
272    }
273    fn object_count(&self) -> usize {
274        self.source.object_count()
275    }
276    fn object(&self, index: usize) -> Option<Object<'_>> {
277        self.source.object(index)
278    }
279    fn object_info(&self, index: usize) -> Result<ObjectInfo> {
280        self.source.object_info(index)
281    }
282    fn object_id(&self, index: usize) -> Option<ObjectId> {
283        self.source.object_id(index)
284    }
285    fn read_object(&self, index: usize, offset: u64, storage: &mut [u8]) -> Result<usize> {
286        self.source.read_object(index, offset, storage)
287    }
288    fn find_index(&self, id: ObjectId) -> Option<usize> {
289        self.source.find_index(id)
290    }
291    fn reference_count(&self) -> usize {
292        self.references.len()
293    }
294    fn reference_name(&self, index: usize) -> Option<&str> {
295        self.references.get(index).map(|reference| reference.name)
296    }
297    fn reference_id(&self, index: usize) -> Option<ObjectId> {
298        self.references.get(index).map(|reference| reference.id)
299    }
300    fn reference_symbolic_target(&self, index: usize) -> Option<&str> {
301        self.references.get(index)?.symbolic_target
302    }
303    fn reference_peeled(&self, index: usize) -> Option<ObjectId> {
304        self.references.get(index)?.peeled
305    }
306}
307
308/// A borrowed object and its precomputed ID, stored in caller-owned memory.
309#[derive(Clone, Copy, Debug)]
310pub struct ObjectMetadata<'a> {
311    pub id: ObjectId,
312    pub object: Object<'a>,
313}
314
315/// Index a stable repository without allocating or copying object bodies.
316///
317/// Construction reads the source count once, reads and hashes each object once,
318/// and sorts an ID index in place. Subsequent object enumeration and ID access
319/// are O(1); lookup by ID is O(log n). Object order, HEAD, and refs are preserved.
320/// The source still owns the bodies and must satisfy [`Repository`]'s stability
321/// contract. It must expose borrowed bodies through `object`; this wrapper does
322/// not index streaming-only providers, validate object graphs, or deduplicate them.
323pub struct IndexedRepository<'index, 'objects, R: Repository + ?Sized> {
324    repository: &'objects R,
325    objects: &'index [Option<ObjectMetadata<'objects>>],
326    by_id: &'index [usize],
327}
328
329impl<'index, 'objects, R: Repository + ?Sized> IndexedRepository<'index, 'objects, R> {
330    /// Both arrays need at least `repository.object_count()` elements. Extra
331    /// slots are untouched. Initialize metadata with `[None; N]`.
332    /// Insufficient capacity fails before reading any object. A missing source
333    /// object returns `ObjectNotFound`; discard partial arrays after an error.
334    ///
335    /// ```
336    /// # #[cfg(feature = "write")]
337    /// # fn example() -> git_zero_core::Result<()> {
338    /// use git_zero_core::{File, IndexedRepository, Repository, Snapshot};
339    ///
340    /// let files = [File { name: "hello", contents: b"hello\n" }];
341    /// let mut tree = [0; 128];
342    /// let mut commit = [0; 256];
343    /// let snapshot = Snapshot::new(&files, &mut tree, &mut commit)?;
344    /// let mut objects = [None; 3]; // Commit, tree, and blob.
345    /// let mut by_id = [0; 3];
346    /// let indexed = IndexedRepository::new(&snapshot, &mut objects, &mut by_id)?;
347    /// assert_eq!(indexed.find(indexed.head()).unwrap().kind,
348    ///            git_zero_core::ObjectKind::Commit);
349    /// # Ok(())
350    /// # }
351    /// # #[cfg(feature = "write")]
352    /// # example().unwrap();
353    /// ```
354    pub fn new(
355        repository: &'objects R,
356        object_storage: &'index mut [Option<ObjectMetadata<'objects>>],
357        index_storage: &'index mut [usize],
358    ) -> Result<Self> {
359        let count = repository.object_count();
360        if object_storage.len() < count || index_storage.len() < count {
361            return Err(Error::BufferTooSmall);
362        }
363        let objects = &mut object_storage[..count];
364        let by_id = &mut index_storage[..count];
365        for (index, slot) in objects.iter_mut().enumerate() {
366            let object = repository.object(index).ok_or(Error::ObjectNotFound)?;
367            *slot = Some(ObjectMetadata {
368                id: object.id_with_format(repository.object_format()),
369                object,
370            });
371            by_id[index] = index;
372        }
373        // sort_unstable uses no heap storage. Every used metadata slot is Some.
374        by_id.sort_unstable_by_key(|&index| objects[index].expect("initialized metadata").id);
375        Ok(Self {
376            repository,
377            objects,
378            by_id,
379        })
380    }
381
382    /// Metadata in the source repository's enumeration order.
383    pub fn metadata(&self, index: usize) -> Option<&ObjectMetadata<'objects>> {
384        self.objects.get(index)?.as_ref()
385    }
386}
387
388impl<R: Repository + ?Sized> Repository for IndexedRepository<'_, '_, R> {
389    fn object_format(&self) -> ObjectFormat {
390        self.repository.object_format()
391    }
392    fn head(&self) -> ObjectId {
393        self.repository.head()
394    }
395
396    fn head_state(&self) -> Head<'_> {
397        self.repository.head_state()
398    }
399
400    fn object_count(&self) -> usize {
401        self.objects.len()
402    }
403
404    fn object(&self, index: usize) -> Option<Object<'_>> {
405        self.metadata(index).map(|metadata| metadata.object)
406    }
407
408    fn object_id(&self, index: usize) -> Option<ObjectId> {
409        self.metadata(index).map(|metadata| metadata.id)
410    }
411
412    fn head_ref(&self) -> &str {
413        self.repository.head_ref()
414    }
415
416    fn reference_count(&self) -> usize {
417        self.repository.reference_count()
418    }
419
420    fn reference_name(&self, index: usize) -> Option<&str> {
421        self.repository.reference_name(index)
422    }
423
424    fn reference_id(&self, index: usize) -> Option<ObjectId> {
425        self.repository.reference_id(index)
426    }
427    fn reference_symbolic_target(&self, index: usize) -> Option<&str> {
428        self.repository.reference_symbolic_target(index)
429    }
430    fn reference_peeled(&self, index: usize) -> Option<ObjectId> {
431        self.repository.reference_peeled(index)
432    }
433
434    fn find_index(&self, id: ObjectId) -> Option<usize> {
435        let position = self
436            .by_id
437            .binary_search_by_key(&id, |&index| {
438                self.objects[index].expect("initialized metadata").id
439            })
440            .ok()?;
441        Some(self.by_id[position])
442    }
443}
444
445/// Validate a fully qualified ref name that fits a smart advertisement.
446/// The default branch additionally needs to fit `MAX_HEAD_REF_LENGTH`.
447pub fn validate_ref_name(name: &str) -> crate::Result<()> {
448    validate_ref_name_bytes(name.as_bytes())
449}
450
451/// Validate a raw fully qualified ref name, including non-UTF-8 Git names.
452pub fn validate_ref_name_bytes(name: &[u8]) -> crate::Result<()> {
453    if name.len() > MAX_REF_NAME_LENGTH
454        || !name.starts_with(b"refs/")
455        || name.ends_with(b".")
456        || name.windows(2).any(|pair| matches!(pair, b".." | b"@{"))
457        || name.iter().any(|&byte| {
458            byte <= b' ' || matches!(byte, 127 | b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\')
459        })
460        || name
461            .split(|&byte| byte == b'/')
462            .any(|part| part.is_empty() || part.starts_with(b".") || part.ends_with(b".lock"))
463    {
464        return Err(crate::Error::InvalidReference);
465    }
466    Ok(())
467}
468
469/// Validate a repository's advertised refs, symbolic links and HEAD state.
470/// Names must be valid, sorted, unique, free of path conflicts, and include a
471/// resolved symbolic HEAD's branch. Unborn HEAD's target must be absent.
472/// Object identities and graph closure are checked separately by the validator.
473pub fn validate_refs(repository: &(impl Repository + ?Sized)) -> Result<()> {
474    let head = repository.head_state();
475    let format = repository.object_format();
476    validate_head(repository, head, format)?;
477    let mut found = false;
478    for index in 0..repository.reference_count() {
479        let name = repository
480            .reference_name(index)
481            .ok_or(Error::InvalidReference)?;
482        validate_reference(repository, index, name, head, format)?;
483        found |= Some(name) == head.target();
484        validate_reference_order(repository, index, name)?;
485    }
486    if matches!(head, Head::Symbolic { .. }) && !found {
487        return Err(Error::InvalidReference);
488    }
489    if let Head::Symbolic { target, .. } = head {
490        let (target, _) = resolve_reference(repository, target)?;
491        if target.len() > head_ref_limit(format) {
492            return Err(Error::InvalidReference);
493        }
494    }
495    Ok(())
496}
497
498// The manifest must be byte-sorted and free of duplicate or conflicting paths.
499fn validate_reference_order(
500    repository: &(impl Repository + ?Sized),
501    index: usize,
502    name: &str,
503) -> Result<()> {
504    for previous in 0..index {
505        let previous = repository
506            .reference_name(previous)
507            .ok_or(Error::InvalidReference)?;
508        if previous >= name
509            || name
510                .strip_prefix(previous)
511                .is_some_and(|suffix| suffix.starts_with('/'))
512        {
513            return Err(Error::InvalidReference);
514        }
515    }
516    Ok(())
517}
518
519// HEAD consistency is checked before reading the ref manifest.
520fn validate_head(
521    repository: &(impl Repository + ?Sized),
522    head: Head<'_>,
523    format: ObjectFormat,
524) -> Result<()> {
525    if let Some(target) = head.target() {
526        validate_ref_name(target)?;
527        let maximum = if head.id().is_some() {
528            head_ref_limit(format)
529        } else {
530            unborn_ref_limit(format)
531        };
532        if !target.starts_with("refs/heads/") || target.len() > maximum {
533            return Err(Error::InvalidReference);
534        }
535    }
536    match head.id() {
537        Some(id) if id != repository.head() || id.is_zero() || id.format() != format => {
538            return Err(Error::InvalidObject);
539        }
540        None if repository.head() != format.zero_id()
541            || (repository.reference_count() == 0 && repository.object_count() != 0) =>
542        {
543            return Err(Error::InvalidObject);
544        }
545        _ => {}
546    }
547    Ok(())
548}
549
550// Validate one advertisement before comparing its name with earlier refs.
551fn validate_reference(
552    repository: &(impl Repository + ?Sized),
553    index: usize,
554    name: &str,
555    head: Head<'_>,
556    format: ObjectFormat,
557) -> Result<()> {
558    let ref_limit = ref_name_limit(format);
559    validate_ref_name(name)?;
560    if name.len() > ref_limit {
561        return Err(Error::InvalidReference);
562    }
563    let id = repository.reference_id(index).ok_or(Error::InvalidObject)?;
564    if id.is_zero()
565        || id.format() != format
566        || repository
567            .reference_peeled(index)
568            .is_some_and(|peeled| peeled.is_zero() || peeled.format() != format)
569    {
570        return Err(Error::InvalidObject);
571    }
572    if repository.reference_peeled(index).is_some() && name.len() > ref_limit - 3 {
573        return Err(Error::InvalidReference);
574    }
575    if head.id().is_none()
576        && index == 0
577        && name.len() > ref_limit - 1 - format.capability().len() - UPLOAD_CAPABILITIES.len()
578    {
579        return Err(Error::InvalidReference);
580    }
581    resolve_reference(repository, name)?;
582    if Some(name) == head.target() && head.id() != Some(id) {
583        return Err(Error::InvalidReference);
584    }
585    Ok(())
586}
587
588/// Resolve a named symbolic ref using a bounded number of manifest probes.
589pub fn resolve_reference<'a>(
590    repository: &'a (impl Repository + ?Sized),
591    name: &str,
592) -> Result<(&'a str, ObjectId)> {
593    let mut index = reference_index(repository, name).ok_or(Error::InvalidReference)?;
594    let id = repository.reference_id(index).ok_or(Error::InvalidObject)?;
595    if id.is_zero() || id.format() != repository.object_format() {
596        return Err(Error::InvalidObject);
597    }
598    for _ in 0..repository.reference_count() {
599        let name = repository
600            .reference_name(index)
601            .ok_or(Error::InvalidReference)?;
602        let Some(target) = repository.reference_symbolic_target(index) else {
603            return Ok((name, id));
604        };
605        validate_ref_name(target)?;
606        index = reference_index(repository, target).ok_or(Error::InvalidReference)?;
607        if repository.reference_id(index) != Some(id) {
608            return Err(Error::InvalidReference);
609        }
610    }
611    Err(Error::InvalidReference)
612}
613
614fn reference_index(repository: &(impl Repository + ?Sized), name: &str) -> Option<usize> {
615    (0..repository.reference_count()).find(|&index| repository.reference_name(index) == Some(name))
616}
617
618/// Complete-pack HEAD keys are safe only when every ref aliases that HEAD.
619pub fn uses_only_head(repository: &(impl Repository + ?Sized)) -> bool {
620    (0..repository.reference_count())
621        .all(|index| repository.reference_id(index) == repository.head_state().id())
622}