Skip to main content

git_zero_core/
validation.rs

1//! Object/link validation and bounded reachable-object selection for upload-pack.
2//! `ValidatedRepository` shares its authenticated index with fetch history traversal.
3//! Opt-in validation of the repository graph contract, without allocation.
4use core::cmp::Ordering;
5
6use crate::{
7    BlobFilter, Error, FetchOptions, Head, HistoryPolicy as History, Object, ObjectFormat,
8    ObjectHasher, ObjectId, ObjectInfo, ObjectKind, ObjectReader, Repository, Result,
9    names::{cmp_tree_names, validate_name},
10    repository::validate_refs,
11};
12
13mod revision;
14pub use revision::{RevisionLimits, RevisionOptions, RevisionState, ShallowChange};
15
16/// Cached identity and size of a validated object, in enumeration order.
17#[derive(Clone, Copy, Debug)]
18pub struct ValidationMetadata {
19    pub id: ObjectId,
20    pub info: ObjectInfo,
21    reached: bool,
22    checked: bool,
23    excluded: bool,
24    forced: bool,
25    wanted: bool,
26    // Commit distance, or one plus tree distance during tree-filter selection.
27    depth: u32,
28    client_shallow: bool,
29    shallow: bool,
30}
31
32impl ValidationMetadata {
33    fn reset_walk(&mut self) {
34        self.reached = false;
35        self.checked = false;
36        self.depth = u32::MAX;
37        self.shallow = false;
38        self.forced = false;
39        self.wanted = false;
40    }
41
42    /// A reached commit at a requested history boundary (including root commits).
43    pub fn is_shallow(&self) -> bool {
44        self.reached && self.shallow
45    }
46
47    /// A client's former boundary reached inside the requested history.
48    pub fn is_unshallow(&self) -> bool {
49        self.reached && self.client_shallow && !self.shallow
50    }
51}
52
53/// Traversal budget and annotated-tag policy for a selected fetch pack.
54#[derive(Clone, Copy, Debug)]
55pub struct SelectionOptions {
56    /// Maximum structural bytes read across common history, wanted objects and
57    /// included tags. Buffered reads and rereads consume the same budget.
58    pub graph_read_limit: u64,
59    /// Include advertised annotated and nested tags whose final peeled target
60    /// is being sent. A target excluded as common history does not qualify.
61    /// Tags use the existing object/index arrays and traversal budget; no extra
62    /// collection or allocation is required. False selects only explicit wants.
63    pub include_tags: bool,
64}
65
66/// Validate and index a stable repository, including streaming-only providers.
67///
68/// Checks repository-format IDs, unique enumeration, links and target kinds,
69/// tree modes, names, Git ordering and duplicates, tag links/peeling, and exact
70/// reachability from the union of HEAD and refs. Commit parents are followed;
71/// gitlinks name external repositories
72/// and are not followed. Branches and resolved HEAD must name commits.
73///
74/// This is graph validation, not a replacement for all of Git's fsck policies:
75/// identity semantics, platform-specific filename aliases, and special files
76/// such as .gitmodules are outside its scope.
77///
78/// The provider must remain stable after validation. Bodies are never retained
79/// or copied in full. Enumeration/IDs/metadata use the index; body reads and refs
80/// continue to use the provider.
81pub struct ValidatedRepository<'index, 'repo, R: Repository + ?Sized> {
82    repository: &'repo R,
83    objects: &'index [Option<ValidationMetadata>],
84    by_id: &'index [usize],
85    head: ObjectId,
86}
87
88impl<'index, 'repo, R: Repository + ?Sized> ValidatedRepository<'index, 'repo, R> {
89    /// Each metadata/index array needs object_count slots; extra slots are
90    /// untouched. Initialize metadata with `[None; N]`. Read scratch needs at
91    /// least one byte for nonempty bodies. Empty repositories need no scratch.
92    /// Parse scratch must fit the longest commit/tag header line
93    /// (excluding LF), and twice the longest tree name (excluding NUL).
94    /// Neither scratch buffer is retained. Their sizes do not limit body sizes.
95    ///
96    /// Insufficient arrays/scratch return BufferTooSmall, absent objects/links
97    /// ObjectNotFound, invalid refs InvalidReference, invalid names
98    /// InvalidFileName, and ordering/duplicate names UnsortedFiles. Invalid IDs,
99    /// kinds, framing, modes, duplicate IDs or unreachable objects return
100    /// InvalidObject. Backend errors propagate. Discard partial arrays on error.
101    ///
102    /// Hashing is linear in body bytes. Lookup uses binary search. Traversal
103    /// scans pending metadata without recursion, using O(n^2) metadata probes
104    /// in the worst case. Directory/file duplicate detection can reread earlier
105    /// tree entries, trading worst-case quadratic tree reads for bounded memory.
106    pub fn new(
107        repository: &'repo R,
108        object_storage: &'index mut [Option<ValidationMetadata>],
109        index_storage: &'index mut [usize],
110        read_scratch: &mut [u8],
111        parse_scratch: &mut [u8],
112    ) -> Result<Self> {
113        let count = repository.object_count();
114        if object_storage.len() < count || index_storage.len() < count {
115            return Err(Error::BufferTooSmall);
116        }
117        validate_refs(repository)?;
118        let objects = &mut object_storage[..count];
119        let by_id = &mut index_storage[..count];
120        initialize_index(repository, objects, by_id, read_scratch)?;
121        let head = repository.head();
122        if let Some(id) = repository.head_state().id() {
123            reach(objects, by_id, id, ObjectKind::Commit)?;
124        }
125        for index in 0..repository.reference_count() {
126            validate_reference_target(
127                repository,
128                index,
129                objects,
130                by_id,
131                read_scratch,
132                parse_scratch,
133            )?;
134        }
135        walk(
136            repository,
137            objects,
138            by_id,
139            (read_scratch, parse_scratch),
140            None,
141            History::default(),
142            BlobFilter::All,
143        )?;
144        if objects
145            .iter()
146            .any(|slot| !slot.expect("initialized metadata").reached)
147        {
148            return Err(Error::InvalidObject);
149        }
150        Ok(Self {
151            repository,
152            objects,
153            by_id,
154            head,
155        })
156    }
157
158    pub fn metadata(&self, index: usize) -> Option<&ValidationMetadata> {
159        self.objects.get(index)?.as_ref()
160    }
161
162    /// Select the exact reachable union of explicit object IDs without allocation.
163    /// Each work/index array needs object_count slots. Read/parse budgets have the
164    /// same requirements as validation; graph bodies are read again, not hashed.
165    /// The result borrows only `selected`, in the provider's enumeration order.
166    /// Overlapping roots are deduplicated. Gitlinks remain external. A failed
167    /// selection invalidates the work arrays; every retry resets all traversal
168    /// state. `graph_read_limit` caps actual structural reads, including prefetch
169    /// and rereads for tree duplicate checks; exhaustion returns LimitExceeded.
170    /// Blob bodies and opaque commit/tag messages are not traversed. Metadata
171    /// probes are bounded by the object-array capacity (quadratic worst case).
172    /// Independent requests use independent caller storage. This primitive
173    /// accepts any present root; HTTP's advertised-want policy is separate.
174    pub fn select_objects<'selected>(
175        &self,
176        roots: &[ObjectId],
177        work: &mut [Option<ValidationMetadata>],
178        selected: &'selected mut [usize],
179        read_scratch: &mut [u8],
180        parse_scratch: &mut [u8],
181        graph_read_limit: u64,
182    ) -> Result<&'selected [usize]> {
183        self.select_missing_objects(
184            roots,
185            core::iter::empty(),
186            work,
187            selected,
188            (read_scratch, parse_scratch),
189            graph_read_limit,
190        )
191    }
192
193    /// Select the wanted closure minus the complete closure of `common` roots.
194    /// Common roots must be present, and are trusted to describe complete client
195    /// history (shallow clients must be rejected by the protocol layer). Both
196    /// traversals share one graph-read budget and the same caller-owned arrays.
197    /// Common-root iteration may fail (for example, a host metadata read); errors
198    /// propagate. Unknown roots fail; retries reset exclusions and traversal state.
199    pub fn select_missing_objects<'selected>(
200        &self,
201        roots: &[ObjectId],
202        common: impl IntoIterator<Item = Result<ObjectId>>,
203        work: &mut [Option<ValidationMetadata>],
204        selected: &'selected mut [usize],
205        scratch: (&mut [u8], &mut [u8]),
206        graph_read_limit: u64,
207    ) -> Result<&'selected [usize]> {
208        self.select_fetch_objects(
209            roots,
210            common,
211            work,
212            selected,
213            scratch,
214            SelectionOptions {
215                graph_read_limit,
216                include_tags: false,
217            },
218        )
219    }
220
221    /// Select missing objects and optionally include advertised annotated tags
222    /// whose final peeled targets are being sent. Tag traversal shares the
223    /// graph-read budget with wanted and common history; retries reset all state.
224    pub fn select_fetch_objects<'selected>(
225        &self,
226        roots: &[ObjectId],
227        common: impl IntoIterator<Item = Result<ObjectId>>,
228        work: &mut [Option<ValidationMetadata>],
229        selected: &'selected mut [usize],
230        scratch: (&mut [u8], &mut [u8]),
231        options: SelectionOptions,
232    ) -> Result<&'selected [usize]> {
233        self.select_bounded_fetch(
234            roots,
235            (common, core::iter::empty()),
236            work,
237            selected,
238            scratch,
239            (options, FetchOptions::default()),
240        )
241    }
242
243    /// Select shallow/filtered history with caller-owned arrays. Client shallow
244    /// IDs must name present commits. Common traversal stops at those commits;
245    /// depth requests instead resend their closure without trusting common haves.
246    /// Relative requests from full clients preserve ordinary common exclusion.
247    /// Relative depth measures the nearest boundary from wanted history, then
248    /// extends that absolute cut (Git 2.55). Both passes share the budget.
249    /// Work metadata retains shallow/unshallow markers until the next selection.
250    /// All structural reads, including revisits after a shorter merge path,
251    /// consume one budget. Retries reset depths, exclusions and boundary markers.
252    ///
253    /// `history` contains common roots followed by the client's shallow IDs.
254    /// Each work/index slice needs one slot per repository object. Extra slots
255    /// remain untouched. The returned indices use provider enumeration order;
256    /// inspect only the first `object_count()` work slots for boundary updates.
257    /// A depth request may resend common objects, while an ordinary request
258    /// subtracts common history only as far as the client's declared boundaries.
259    ///
260    /// Empty wants, zero/oversized depth and absent or non-commit shallow IDs
261    /// return `InvalidRequest`. Missing wanted/common objects return
262    /// `ObjectNotFound`; short arrays or scratch return `BufferTooSmall`, and
263    /// exhausted structural-read budgets return `LimitExceeded`. No result is
264    /// published on failure; reuse the same arrays for a fresh attempt.
265    ///
266    /// This example selects a depth-one snapshot without its blob. The root
267    /// commit is a shallow boundary even though it has no parents, matching Git.
268    /// The source, validation index and per-request work arrays are independent.
269    ///
270    /// ```
271    /// # #[cfg(feature = "write")]
272    /// # fn main() -> git_zero_core::Result<()> {
273    /// use git_zero_core::{
274    ///     BlobFilter, FetchOptions, File, Repository, SelectionOptions, Snapshot,
275    ///     ValidatedRepository,
276    /// };
277    /// let files = [File { name: "hello.txt", contents: b"hello\n" }];
278    /// let (mut tree, mut commit) = ([0; 128], [0; 512]);
279    /// let snapshot = Snapshot::new(&files, &mut tree, &mut commit)?;
280    /// let (mut metadata, mut by_id) = ([None; 3], [0; 3]);
281    /// let repository = ValidatedRepository::new(
282    ///     &snapshot, &mut metadata, &mut by_id, &mut [0; 64], &mut [0; 128],
283    /// )?;
284    /// let (mut work, mut indices) = ([None; 3], [0; 3]);
285    /// let wanted = [repository.head()];
286    /// let selected = repository.select_bounded_fetch(
287    ///     &wanted, (core::iter::empty(), core::iter::empty()),
288    ///     &mut work, &mut indices, (&mut [0; 64], &mut [0; 128]),
289    ///     (SelectionOptions { graph_read_limit: 4096, include_tags: false },
290    ///      FetchOptions { depth: Some(1), filter: BlobFilter::None, ..FetchOptions::default() }),
291    /// )?;
292    /// assert_eq!(selected.len(), 2); // Commit and tree, with no blob.
293    /// let boundary = work.iter().flatten().find(|object| object.is_shallow()).unwrap();
294    /// assert_eq!(boundary.id, repository.head());
295    /// # Ok(())
296    /// # }
297    /// # #[cfg(not(feature = "write"))]
298    /// # fn main() {}
299    /// ```
300    pub fn select_bounded_fetch<'selected>(
301        &self,
302        roots: &[ObjectId],
303        history: (
304            impl IntoIterator<Item = Result<ObjectId>>,
305            impl IntoIterator<Item = Result<ObjectId>>,
306        ),
307        work: &mut [Option<ValidationMetadata>],
308        selected: &'selected mut [usize],
309        scratch: (&mut [u8], &mut [u8]),
310        options: (SelectionOptions, FetchOptions),
311    ) -> Result<&'selected [usize]> {
312        let (options, fetch) = options;
313        if fetch
314            .depth
315            .is_some_and(|depth| depth == 0 || depth > i32::MAX as u32)
316        {
317            return Err(Error::InvalidRequest);
318        }
319        let SelectionOptions {
320            graph_read_limit,
321            include_tags,
322        } = options;
323        let (read_scratch, parse_scratch) = scratch;
324        let (common, shallow) = history;
325        if roots.is_empty() {
326            return Err(Error::InvalidRequest);
327        }
328        let count = self.objects.len();
329        if work.len() < count || selected.len() < count {
330            return Err(Error::BufferTooSmall);
331        }
332        let mut selection = Selection {
333            repository: self,
334            work: &mut work[..count],
335            scratch: (read_scratch, parse_scratch),
336            remaining: graph_read_limit,
337            filter: BlobFilter::All,
338        };
339        let has_shallow = selection.prepare(shallow)?;
340        // A relative request from a full client is an ordinary incremental
341        // fetch. Its depth cannot expose missing history behind a boundary.
342        let deepening = fetch.deepens_history(has_shallow);
343        selection.exclude_common(common, deepening)?;
344        let nearest = match fetch.depth.filter(|_| deepening) {
345            Some(_) if fetch.deepen_relative => selection.nearest_boundary_depth(roots)?,
346            _ => None,
347        };
348        let history = fetch.history(has_shallow, nearest)?;
349        selection.filter = fetch.filter;
350        selection.walk_wants(roots, history)?;
351        filter_objects(selection.work, fetch.filter);
352        if include_tags {
353            selection.include_matching_tags()?;
354            selection.walk(history)?;
355            filter_objects(selection.work, fetch.filter);
356        }
357        let length = selected_indices(selection.work, selected);
358        Ok(&selected[..length])
359    }
360}
361
362// Publish both ordinary and revision selections in provider enumeration order.
363fn selected_indices(objects: &[Option<ValidationMetadata>], selected: &mut [usize]) -> usize {
364    let mut length = 0;
365    for (index, slot) in objects.iter().enumerate() {
366        if slot.expect("initialized metadata").reached {
367            selected[length] = index;
368            length += 1;
369        }
370    }
371    length
372}
373
374// One selection owns its caller buffers and a single shared read budget.
375// History, boundary probing and wanted/tag walks all use this same context.
376struct Selection<'request, 'index, 'repo, R: Repository + ?Sized> {
377    repository: &'request ValidatedRepository<'index, 'repo, R>,
378    work: &'request mut [Option<ValidationMetadata>],
379    scratch: (&'request mut [u8], &'request mut [u8]),
380    remaining: u64,
381    filter: BlobFilter,
382}
383
384impl<R: Repository + ?Sized> Selection<'_, '_, '_, R> {
385    fn walk(&mut self, history: History) -> Result<()> {
386        walk(
387            self.repository,
388            self.work,
389            self.repository.by_id,
390            (&mut *self.scratch.0, &mut *self.scratch.1),
391            Some(&mut self.remaining),
392            history,
393            self.filter,
394        )
395    }
396
397    fn prepare(&mut self, shallow: impl IntoIterator<Item = Result<ObjectId>>) -> Result<bool> {
398        let work = &mut *self.work;
399        work.copy_from_slice(self.repository.objects);
400        for slot in work.iter_mut() {
401            let object = slot.as_mut().expect("validated metadata");
402            object.reset_walk();
403            object.client_shallow = false;
404        }
405        let mut has_shallow = false;
406        for id in shallow {
407            let index = find(work, self.repository.by_id, id?).ok_or(Error::InvalidRequest)?;
408            let object = metadata_mut(work, index);
409            if object.info.kind != ObjectKind::Commit {
410                return Err(Error::InvalidRequest);
411            }
412            object.client_shallow = true;
413            has_shallow = true;
414        }
415        Ok(has_shallow)
416    }
417
418    fn exclude_common(
419        &mut self,
420        common: impl IntoIterator<Item = Result<ObjectId>>,
421        deepening: bool,
422    ) -> Result<()> {
423        let work = &mut *self.work;
424        for id in common {
425            let id = id?;
426            let index = find(work, self.repository.by_id, id).ok_or(Error::ObjectNotFound)?;
427            if !deepening {
428                reach(
429                    work,
430                    self.repository.by_id,
431                    id,
432                    metadata(work, index).info.kind,
433                )?;
434            }
435        }
436        self.walk(History {
437            limit: None,
438            client: true,
439            distance: false,
440        })?;
441        for slot in self.work.iter_mut() {
442            let object = slot.as_mut().expect("validated metadata");
443            object.excluded = object.reached;
444            object.reset_walk();
445        }
446        Ok(())
447    }
448
449    fn nearest_boundary_depth(&mut self, roots: &[ObjectId]) -> Result<Option<u32>> {
450        // Git 2.55 measures the nearest client boundary from the wants.
451        // Unrelated shallow branches cannot influence or extend this cut.
452        self.walk_wants(roots, History::boundary_probe())?;
453        // Git 2.55 get_shallow_commits returns no new boundaries when none
454        // match. Unlike Git 2.54, this leaves wanted history complete.
455        let limit = self
456            .work
457            .iter()
458            .flatten()
459            .filter(|object| object.reached && object.client_shallow)
460            .map(|object| object.depth)
461            .min();
462        for slot in self.work.iter_mut() {
463            slot.as_mut().expect("validated metadata").reset_walk();
464        }
465        Ok(limit)
466    }
467
468    fn walk_wants(&mut self, roots: &[ObjectId], history: History) -> Result<()> {
469        let work = &mut *self.work;
470        for &id in roots {
471            let index = find(work, self.repository.by_id, id).ok_or(Error::ObjectNotFound)?;
472            metadata_mut(work, index).wanted = true;
473            reach_at(
474                work,
475                self.repository.by_id,
476                id,
477                metadata(work, index).info.kind,
478                1,
479                true,
480            )?;
481        }
482        self.walk(history)
483    }
484
485    fn include_matching_tags(&mut self) -> Result<()> {
486        let work = &mut *self.work;
487        for reference in 0..self.repository.reference_count() {
488            let Some(peeled) = self.repository.reference_peeled(reference) else {
489                continue;
490            };
491            let index = find(work, self.repository.by_id, peeled).ok_or(Error::ObjectNotFound)?;
492            if metadata(work, index).reached {
493                let id = self
494                    .repository
495                    .reference_id(reference)
496                    .ok_or(Error::InvalidObject)?;
497                // include-tag adds the annotation outside the object filter.
498                // Its complete tag chain must survive the second policy pass.
499                let tag = find(work, self.repository.by_id, id).ok_or(Error::ObjectNotFound)?;
500                metadata_mut(work, tag).wanted = true;
501                reach_at(
502                    work,
503                    self.repository.by_id,
504                    id,
505                    ObjectKind::Tag,
506                    metadata(work, index).depth,
507                    false,
508                )?;
509            }
510        }
511        Ok(())
512    }
513}
514
515fn filter_objects(objects: &mut [Option<ValidationMetadata>], filter: BlobFilter) {
516    if matches!(filter, BlobFilter::All) {
517        return;
518    }
519    for slot in objects {
520        let object = slot.as_mut().expect("validated metadata");
521        if !filter.includes_at(object.info, object.wanted, object.depth.saturating_sub(1)) {
522            object.reached = false;
523        }
524    }
525}
526
527impl<R: Repository + ?Sized> Repository for ValidatedRepository<'_, '_, R> {
528    fn object_format(&self) -> ObjectFormat {
529        self.repository.object_format()
530    }
531    fn head(&self) -> ObjectId {
532        self.head
533    }
534    fn head_state(&self) -> Head<'_> {
535        self.repository.head_state()
536    }
537    fn object_count(&self) -> usize {
538        self.objects.len()
539    }
540    fn object(&self, index: usize) -> Option<Object<'_>> {
541        self.metadata(index)?;
542        self.repository.object(index)
543    }
544    fn object_info(&self, index: usize) -> Result<ObjectInfo> {
545        self.metadata(index)
546            .map(|object| object.info)
547            .ok_or(Error::ObjectNotFound)
548    }
549    fn object_id(&self, index: usize) -> Option<ObjectId> {
550        self.metadata(index).map(|object| object.id)
551    }
552    fn read_object(&self, index: usize, offset: u64, storage: &mut [u8]) -> Result<usize> {
553        self.object_info(index)?;
554        self.repository.read_object(index, offset, storage)
555    }
556    fn find_index(&self, id: ObjectId) -> Option<usize> {
557        find(self.objects, self.by_id, id)
558    }
559    fn head_ref(&self) -> &str {
560        self.repository.head_ref()
561    }
562    fn reference_count(&self) -> usize {
563        self.repository.reference_count()
564    }
565    fn reference_name(&self, index: usize) -> Option<&str> {
566        self.repository.reference_name(index)
567    }
568    fn reference_id(&self, index: usize) -> Option<ObjectId> {
569        self.repository.reference_id(index)
570    }
571    fn reference_symbolic_target(&self, index: usize) -> Option<&str> {
572        self.repository.reference_symbolic_target(index)
573    }
574    fn reference_peeled(&self, index: usize) -> Option<ObjectId> {
575        self.repository.reference_peeled(index)
576    }
577}
578
579// Hash and index the stable enumeration before traversing any graph edges.
580fn initialize_index(
581    repository: &(impl Repository + ?Sized),
582    objects: &mut [Option<ValidationMetadata>],
583    by_id: &mut [usize],
584    read_scratch: &mut [u8],
585) -> Result<()> {
586    for (index, slot) in objects.iter_mut().enumerate() {
587        *slot = Some(authenticated_metadata(repository, index, read_scratch)?);
588        by_id[index] = index;
589    }
590    by_id.sort_unstable_by_key(|&index| metadata(objects, index).id);
591    if by_id
592        .windows(2)
593        .any(|pair| metadata(objects, pair[0]).id == metadata(objects, pair[1]).id)
594    {
595        return Err(Error::InvalidObject);
596    }
597    Ok(())
598}
599
600// Authenticate each object before publishing its identity in the lookup index.
601fn authenticated_metadata(
602    repository: &(impl Repository + ?Sized),
603    index: usize,
604    read_scratch: &mut [u8],
605) -> Result<ValidationMetadata> {
606    let info = repository.object_info(index)?;
607    let id = repository.object_id(index).ok_or(Error::ObjectNotFound)?;
608    if ObjectHasher::new(repository, index)?.finish(read_scratch)? != id
609        || repository.object(index).is_some_and(|object| {
610            object.info() != info || object.id_with_format(repository.object_format()) != id
611        })
612    {
613        return Err(Error::InvalidObject);
614    }
615    Ok(ValidationMetadata {
616        id,
617        info,
618        reached: false,
619        checked: false,
620        excluded: false,
621        forced: false,
622        wanted: false,
623        depth: u32::MAX,
624        client_shallow: false,
625        shallow: false,
626    })
627}
628
629fn walk(
630    repository: &(impl Repository + ?Sized),
631    objects: &mut [Option<ValidationMetadata>],
632    by_id: &[usize],
633    scratch: (&mut [u8], &mut [u8]),
634    mut budget: Option<&mut u64>,
635    history: History,
636    filter: BlobFilter,
637) -> Result<()> {
638    let (read_scratch, parse_scratch) = scratch;
639    while let Some(index) = objects.iter().position(|slot| {
640        let object = slot.expect("initialized metadata");
641        object.reached && !object.checked
642    }) {
643        metadata_mut(objects, index).checked = true;
644        let object = *metadata(objects, index);
645        if object.info.kind == ObjectKind::Tree
646            && !filter.follows_tree(object.depth.saturating_sub(1))
647        {
648            continue;
649        }
650        let mut body = Body::budgeted(repository, index, read_scratch, budget.as_deref_mut())?;
651        validate_walk_object(
652            &mut body,
653            index,
654            objects,
655            by_id,
656            parse_scratch,
657            history,
658            filter,
659        )?;
660    }
661    Ok(())
662}
663
664fn validate_walk_object(
665    body: &mut Body<'_, '_, impl Repository + ?Sized>,
666    index: usize,
667    objects: &mut [Option<ValidationMetadata>],
668    by_id: &[usize],
669    scratch: &mut [u8],
670    history: History,
671    filter: BlobFilter,
672) -> Result<()> {
673    let object = *metadata(objects, index);
674    match object.info.kind {
675        ObjectKind::Commit => {
676            let stop = !history.follows_parents(object.depth, object.client_shallow);
677            validate_commit(
678                body,
679                scratch,
680                objects,
681                by_id,
682                (!stop).then_some(history.parent_depth(object.depth)),
683            )?;
684            metadata_mut(objects, index).shallow = stop;
685            Ok(())
686        }
687        ObjectKind::Tree => {
688            let child_depth = if matches!(filter, BlobFilter::TreeDepth(_))
689                && !(object.wanted && object.forced)
690            {
691                object.depth.saturating_add(1)
692            } else {
693                1
694            };
695            validate_tree(body, scratch, objects, by_id, object.forced, child_depth)
696        }
697        ObjectKind::Blob => Ok(()),
698        ObjectKind::Tag => validate_tag(
699            body,
700            scratch,
701            objects,
702            by_id,
703            object.depth,
704            object.forced,
705            object.wanted,
706        ),
707    }
708}
709
710fn validate_reference_target(
711    repository: &(impl Repository + ?Sized),
712    index: usize,
713    objects: &mut [Option<ValidationMetadata>],
714    by_id: &[usize],
715    read_scratch: &mut [u8],
716    parse_scratch: &mut [u8],
717) -> Result<()> {
718    let id = repository.reference_id(index).ok_or(Error::InvalidObject)?;
719    let position = find(objects, by_id, id).ok_or(Error::ObjectNotFound)?;
720    let kind = metadata(objects, position).info.kind;
721    let name = repository
722        .reference_name(index)
723        .ok_or(Error::InvalidReference)?;
724    if name.starts_with("refs/heads/") && kind != ObjectKind::Commit {
725        return Err(Error::InvalidObject);
726    }
727    reach(objects, by_id, id, kind)?;
728    let expected = if kind == ObjectKind::Tag {
729        Some(peel(
730            repository,
731            objects,
732            by_id,
733            id,
734            read_scratch,
735            parse_scratch,
736        )?)
737    } else {
738        None
739    };
740    if repository.reference_peeled(index) != expected {
741        return Err(Error::InvalidObject);
742    }
743    Ok(())
744}
745
746fn peel(
747    repository: &(impl Repository + ?Sized),
748    objects: &[Option<ValidationMetadata>],
749    by_id: &[usize],
750    mut id: ObjectId,
751    read_scratch: &mut [u8],
752    parse_scratch: &mut [u8],
753) -> Result<ObjectId> {
754    for _ in 0..objects.len() {
755        let index = find(objects, by_id, id).ok_or(Error::ObjectNotFound)?;
756        if metadata(objects, index).info.kind != ObjectKind::Tag {
757            return Ok(id);
758        }
759        let (target, kind) = tag_target(
760            &mut Body::new(repository, index, read_scratch)?,
761            parse_scratch,
762        )?;
763        let target_index = find(objects, by_id, target).ok_or(Error::ObjectNotFound)?;
764        if metadata(objects, target_index).info.kind != kind {
765            return Err(Error::InvalidObject);
766        }
767        id = target;
768    }
769    Err(Error::InvalidObject)
770}
771
772fn metadata(objects: &[Option<ValidationMetadata>], index: usize) -> &ValidationMetadata {
773    objects[index].as_ref().expect("initialized metadata")
774}
775
776fn metadata_mut(
777    objects: &mut [Option<ValidationMetadata>],
778    index: usize,
779) -> &mut ValidationMetadata {
780    objects[index].as_mut().expect("initialized metadata")
781}
782
783fn find(objects: &[Option<ValidationMetadata>], by_id: &[usize], id: ObjectId) -> Option<usize> {
784    let position = by_id
785        .binary_search_by_key(&id, |&index| metadata(objects, index).id)
786        .ok()?;
787    Some(by_id[position])
788}
789
790fn reach(
791    objects: &mut [Option<ValidationMetadata>],
792    by_id: &[usize],
793    id: ObjectId,
794    kind: ObjectKind,
795) -> Result<()> {
796    reach_at(objects, by_id, id, kind, 1, false)
797}
798
799fn reach_at(
800    objects: &mut [Option<ValidationMetadata>],
801    by_id: &[usize],
802    id: ObjectId,
803    kind: ObjectKind,
804    depth: u32,
805    forced: bool,
806) -> Result<()> {
807    if id.is_zero() {
808        return Err(Error::InvalidObject);
809    }
810    let index = find(objects, by_id, id).ok_or(Error::ObjectNotFound)?;
811    let object = metadata_mut(objects, index);
812    if object.info.kind != kind {
813        return Err(Error::InvalidObject);
814    }
815    // Explicit non-commit wants retain their tree/tag closure even when a
816    // reported commit references it. Common commits still cut tag history.
817    let forced = forced && kind != ObjectKind::Commit;
818    let newly_forced = forced && !object.forced;
819    object.forced |= forced;
820    if forced {
821        object.excluded = false;
822    }
823    if !object.excluded {
824        object.reached = true;
825        if depth < object.depth || newly_forced {
826            object.depth = object.depth.min(depth);
827            object.checked = false;
828        }
829    }
830    Ok(())
831}
832
833/// Buffered streaming cursor. Seek discards buffered bytes; logical offsets
834/// exclude unread bytes and remain 64-bit independently of the host usize.
835struct Body<'scratch, 'repo, R: Repository + ?Sized> {
836    reader: ObjectReader<'repo, R>,
837    scratch: &'scratch mut [u8],
838    position: usize,
839    length: usize,
840    budget: Option<&'scratch mut u64>,
841}
842
843impl<'scratch, 'repo, R: Repository + ?Sized> Body<'scratch, 'repo, R> {
844    fn new(repository: &'repo R, index: usize, scratch: &'scratch mut [u8]) -> Result<Self> {
845        Self::budgeted(repository, index, scratch, None)
846    }
847    fn budgeted(
848        repository: &'repo R,
849        index: usize,
850        scratch: &'scratch mut [u8],
851        budget: Option<&'scratch mut u64>,
852    ) -> Result<Self> {
853        Ok(Self {
854            reader: ObjectReader::new(repository, index)?,
855            scratch,
856            position: 0,
857            length: 0,
858            budget,
859        })
860    }
861    fn offset(&self) -> u64 {
862        self.reader.position() - (self.length - self.position) as u64
863    }
864    fn seek(&mut self, offset: u64) -> Result<()> {
865        self.reader.seek(offset)?;
866        self.position = 0;
867        self.length = 0;
868        Ok(())
869    }
870    fn byte(&mut self) -> Result<Option<u8>> {
871        if self.position == self.length {
872            self.refill()?;
873            if self.length == 0 {
874                return Ok(None);
875            }
876        }
877        let byte = self.scratch[self.position];
878        self.position += 1;
879        Ok(Some(byte))
880    }
881    // Only refilling touches the provider and debits the shared byte budget.
882    fn refill(&mut self) -> Result<()> {
883        if self.budget.as_deref() == Some(&0) && self.reader.remaining() != 0 {
884            return Err(Error::LimitExceeded);
885        }
886        let capacity = self
887            .budget
888            .as_deref()
889            .map_or(self.scratch.len(), |remaining| {
890                self.scratch
891                    .len()
892                    .min(usize::try_from(*remaining).unwrap_or(usize::MAX))
893            });
894        self.length = self.reader.read(&mut self.scratch[..capacity])?;
895        if let Some(remaining) = self.budget.as_deref_mut() {
896            *remaining -= self.length as u64;
897        }
898        self.position = 0;
899        Ok(())
900    }
901    fn until<'a>(&mut self, delimiter: u8, output: &'a mut [u8]) -> Result<&'a [u8]> {
902        let mut length = 0;
903        loop {
904            let byte = self.byte()?.ok_or(Error::InvalidObject)?;
905            if byte == delimiter {
906                return Ok(&output[..length]);
907            }
908            *output.get_mut(length).ok_or(Error::BufferTooSmall)? = byte;
909            length += 1;
910        }
911    }
912    fn id(&mut self) -> Result<ObjectId> {
913        let format = self.reader.object_format();
914        let mut bytes = [0; 32];
915        for byte in &mut bytes[..format.id_length()] {
916            *byte = self.byte()?.ok_or(Error::InvalidObject)?;
917        }
918        let id = ObjectId::from_bytes(format, &bytes[..format.id_length()])?;
919        if id.is_zero() {
920            return Err(Error::InvalidObject);
921        }
922        Ok(id)
923    }
924    fn line<'a>(&mut self, scratch: &'a mut [u8]) -> Result<&'a [u8]> {
925        let line = self.until(b'\n', scratch)?;
926        if line.contains(&0) {
927            return Err(Error::InvalidObject);
928        }
929        Ok(line)
930    }
931}
932
933// Required structural headers have an exact prefix and a nonempty value.
934fn header_value<'a>(line: &'a [u8], prefix: &[u8]) -> Result<&'a [u8]> {
935    line.strip_prefix(prefix)
936        .filter(|value| !value.is_empty())
937        .ok_or(Error::InvalidObject)
938}
939
940fn link(line: &[u8], prefix: &[u8], format: ObjectFormat) -> Result<ObjectId> {
941    let id = ObjectId::from_hex(header_value(line, prefix)?).map_err(|_| Error::InvalidObject)?;
942    if id.format() != format {
943        return Err(Error::InvalidObject);
944    }
945    Ok(id)
946}
947
948fn validate_commit(
949    body: &mut Body<'_, '_, impl Repository + ?Sized>,
950    scratch: &mut [u8],
951    objects: &mut [Option<ValidationMetadata>],
952    by_id: &[usize],
953    parent_depth: Option<u32>,
954) -> Result<()> {
955    let tree = commit_tree(body, scratch)?;
956    reach(objects, by_id, tree, ObjectKind::Tree)?;
957    commit_parents(body, scratch, |id| match parent_depth {
958        Some(depth) => reach_at(objects, by_id, id, ObjectKind::Commit, depth, false),
959        None => Ok(()),
960    })?;
961    validate_extra_headers(
962        body,
963        scratch,
964        &[b"tree", b"parent", b"author", b"committer"],
965    )
966}
967
968fn commit_tree(
969    body: &mut Body<'_, '_, impl Repository + ?Sized>,
970    scratch: &mut [u8],
971) -> Result<ObjectId> {
972    let format = body.reader.object_format();
973    link(body.line(scratch)?, b"tree ", format)
974}
975
976// Both graph validation and revision selection consume these structural headers
977// after the root tree. The caller decides which parents to traverse.
978fn commit_parents<'a>(
979    body: &mut Body<'_, '_, impl Repository + ?Sized>,
980    scratch: &'a mut [u8],
981    mut visit: impl FnMut(ObjectId) -> Result<()>,
982) -> Result<&'a [u8]> {
983    let format = body.reader.object_format();
984    let mut line = body.line(scratch)?;
985    while line.starts_with(b"parent ") {
986        visit(link(line, b"parent ", format)?)?;
987        line = body.line(scratch)?;
988    }
989    header_value(line, b"author ")?;
990    let line = body.line(scratch)?;
991    header_value(line, b"committer ")?;
992    Ok(line)
993}
994
995fn tag_target(
996    body: &mut Body<'_, '_, impl Repository + ?Sized>,
997    scratch: &mut [u8],
998) -> Result<(ObjectId, ObjectKind)> {
999    let format = body.reader.object_format();
1000    let id = link(body.line(scratch)?, b"object ", format)?;
1001    if id.is_zero() {
1002        return Err(Error::InvalidObject);
1003    }
1004    let kind = ObjectKind::from_tag_header(body.line(scratch)?)?;
1005    Ok((id, kind))
1006}
1007
1008fn validate_tag(
1009    body: &mut Body<'_, '_, impl Repository + ?Sized>,
1010    scratch: &mut [u8],
1011    objects: &mut [Option<ValidationMetadata>],
1012    by_id: &[usize],
1013    depth: u32,
1014    forced: bool,
1015    wanted: bool,
1016) -> Result<()> {
1017    let (id, kind) = tag_target(body, scratch)?;
1018    reach_at(objects, by_id, id, kind, depth, forced)?;
1019    if wanted {
1020        let index = find(objects, by_id, id).ok_or(Error::ObjectNotFound)?;
1021        metadata_mut(objects, index).wanted = true;
1022    }
1023    header_value(body.line(scratch)?, b"tag ")?;
1024    validate_extra_headers(body, scratch, &[b"object", b"type", b"tag"])
1025}
1026
1027fn validate_extra_headers(
1028    body: &mut Body<'_, '_, impl Repository + ?Sized>,
1029    scratch: &mut [u8],
1030    reserved: &[&[u8]],
1031) -> Result<()> {
1032    loop {
1033        let line = body.line(scratch)?;
1034        if line.is_empty() {
1035            return Ok(());
1036        }
1037        if line.starts_with(b" ") {
1038            continue;
1039        }
1040        let space = line
1041            .iter()
1042            .position(|&byte| byte == b' ')
1043            .ok_or(Error::InvalidObject)?;
1044        let name = &line[..space];
1045        if name.is_empty()
1046            || name.iter().any(|byte| byte.is_ascii_control())
1047            || reserved.contains(&name)
1048        {
1049            return Err(Error::InvalidObject);
1050        }
1051    }
1052}
1053
1054fn tree_entry<'a>(
1055    body: &mut Body<'_, '_, impl Repository + ?Sized>,
1056    scratch: &'a mut [u8],
1057) -> Result<(ObjectKind, &'a [u8], ObjectId)> {
1058    let mut mode = [0; 6];
1059    let kind = match body.until(b' ', &mut mode).map_err(|error| {
1060        if error == Error::BufferTooSmall {
1061            Error::InvalidObject
1062        } else {
1063            error
1064        }
1065    })? {
1066        b"40000" => ObjectKind::Tree,
1067        b"100644" | b"100755" | b"120000" => ObjectKind::Blob,
1068        b"160000" => ObjectKind::Commit, // External gitlink, not a graph edge.
1069        _ => return Err(Error::InvalidObject),
1070    };
1071    let name = body.until(0, scratch)?;
1072    validate_name(name)?;
1073    Ok((kind, name, body.id()?))
1074}
1075
1076fn check_duplicate_name(
1077    body: &mut Body<'_, '_, impl Repository + ?Sized>,
1078    scratch: &mut [u8],
1079    name: &[u8],
1080    before: u64,
1081) -> Result<()> {
1082    let resume = body.offset();
1083    body.seek(0)?;
1084    while body.offset() < before {
1085        let (_, previous, _) = tree_entry(body, scratch)?;
1086        if previous == name {
1087            return Err(Error::UnsortedFiles);
1088        }
1089    }
1090    body.seek(resume)
1091}
1092
1093fn check_tree_order(
1094    previous: &[u8],
1095    previous_kind: ObjectKind,
1096    name: &[u8],
1097    kind: ObjectKind,
1098) -> Result<()> {
1099    if previous == name
1100        || cmp_tree_names(
1101            previous,
1102            previous_kind == ObjectKind::Tree,
1103            name,
1104            kind == ObjectKind::Tree,
1105        ) != Ordering::Less
1106    {
1107        return Err(Error::UnsortedFiles);
1108    }
1109    Ok(())
1110}
1111
1112fn validate_tree(
1113    body: &mut Body<'_, '_, impl Repository + ?Sized>,
1114    scratch: &mut [u8],
1115    objects: &mut [Option<ValidationMetadata>],
1116    by_id: &[usize],
1117    forced: bool,
1118    child_depth: u32,
1119) -> Result<()> {
1120    let capacity = scratch.len() / 2;
1121    let (previous, rest) = scratch.split_at_mut(capacity);
1122    let current = &mut rest[..capacity];
1123    let mut previous_length = 0;
1124    let mut previous_kind = ObjectKind::Blob;
1125    while body.offset() < body.reader.info().size {
1126        let start = body.offset();
1127        let (kind, name, id) = tree_entry(body, current)?;
1128        // The initial empty name sorts before every validated nonempty entry.
1129        check_tree_order(&previous[..previous_length], previous_kind, name, kind)?;
1130        if kind != ObjectKind::Commit {
1131            reach_at(objects, by_id, id, kind, child_depth, forced)?;
1132        }
1133        // A file "foo", file "foo.bar", directory "foo" is sorted but
1134        // still duplicates a name. Reread earlier entries instead of keeping
1135        // an unbounded set of names or recursively visiting child trees.
1136        if kind == ObjectKind::Tree && start != 0 {
1137            check_duplicate_name(body, previous, name, start)?;
1138        }
1139        previous[..name.len()].copy_from_slice(name);
1140        previous_length = name.len();
1141        previous_kind = kind;
1142    }
1143    Ok(())
1144}