1use std::collections::BTreeMap;
2use std::collections::Bound;
3use std::ops::RangeBounds;
4use std::sync::OnceLock;
5
6use jiff::Timestamp;
7use pubgrub::Ranges;
8use tracing::{instrument, trace};
9
10use uv_client::{FlatIndexEntry, OwnedArchive, SimpleDetailMetadata, VersionFiles};
11use uv_configuration::BuildOptions;
12use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
13use uv_distribution_types::{
14 HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, MinimumLibcVersion,
15 PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, RequiresPython,
16 SourceDistCompatibility, WheelCompatibility,
17};
18use uv_normalize::PackageName;
19use uv_pep440::Version;
20use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
21use uv_pypi_types::{HashDigest, ResolutionMetadata, Yanked};
22use uv_types::HashStrategy;
23use uv_warnings::warn_user_once;
24
25use crate::flat_index::FlatDistributions;
26use crate::yanks::AllowedYanks;
27
28#[derive(Debug)]
30pub struct VersionMap {
31 inner: VersionMapInner,
33}
34
35impl VersionMap {
36 #[instrument(skip_all, fields(package_name))]
46 pub(crate) fn from_simple_metadata(
47 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
48 package_name: &PackageName,
49 index: IndexUrl,
50 tags: Option<Tags>,
51 requires_python: RequiresPython,
52 allowed_yanks: AllowedYanks,
53 hasher: HashStrategy,
54 included_version_cutoff: Option<Timestamp>,
55 available_version_cutoff: Option<Timestamp>,
56 flat_index: Option<FlatDistributions>,
57 build_options: &BuildOptions,
58 minimum_libc_version: Option<MinimumLibcVersion>,
59 ) -> Self {
60 let mut local = false;
61 let mut entries = Vec::with_capacity(simple_metadata.iter().size_hint().0);
62 for (datum_index, datum) in simple_metadata.iter().enumerate() {
66 let version = rkyv::deserialize::<Version, rkyv::rancor::Error>(&datum.version)
67 .expect("archived version always deserializes");
68
69 local |= version.is_local();
70 debug_assert!(
71 entries
72 .last()
73 .is_none_or(|entry: &VersionMapLazyEntry| entry.version < version),
74 "simple metadata versions must be sorted and unique"
75 );
76 entries.push(VersionMapLazyEntry {
77 version,
78 dist: LazyPrioritizedDist {
79 flat: None,
80 simple: Some(SimplePrioritizedDist {
81 datum_index,
82 dist: OnceLock::new(),
83 }),
84 },
85 });
86 }
87 let mut map = VersionMapLazyIndex { entries };
88 if let Some(flat_index) = flat_index {
91 map = map.merge_flat(flat_index);
92 }
93 Self {
94 inner: VersionMapInner::Lazy(VersionMapLazy {
95 package_name: package_name.clone(),
96 map,
97 local,
98 simple_metadata,
99 no_binary: build_options.no_binary_package(package_name),
100 no_build: build_options.no_build_package(package_name),
101 index,
102 tags,
103 allowed_yanks,
104 hasher,
105 requires_python,
106 included_version_cutoff,
107 available_version_cutoff,
108 minimum_libc_version,
109 }),
110 }
111 }
112
113 #[instrument(skip_all, fields(package_name))]
114 pub(crate) fn from_flat_metadata(
115 flat_metadata: Vec<FlatIndexEntry>,
116 tags: Option<&Tags>,
117 hasher: &HashStrategy,
118 build_options: &BuildOptions,
119 minimum_libc_version: Option<MinimumLibcVersion>,
120 ) -> Self {
121 FlatDistributions::from_entries(
122 flat_metadata,
123 tags,
124 hasher,
125 build_options,
126 minimum_libc_version,
127 )
128 .into()
129 }
130
131 pub(crate) fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
133 match self.inner {
134 VersionMapInner::Eager(_) => None,
135 VersionMapInner::Lazy(ref lazy) => lazy.get_metadata(version),
136 }
137 }
138
139 pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
141 match self.inner {
142 VersionMapInner::Eager(ref eager) => eager.map.get(version),
143 VersionMapInner::Lazy(ref lazy) => lazy.get(version),
144 }
145 }
146
147 pub(crate) fn versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
149 match &self.inner {
150 VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
151 VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.map.keys()),
152 }
153 }
154
155 pub(crate) fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
163 match &self.inner {
164 VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
165 VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.included_versions()),
166 }
167 }
168
169 pub(crate) fn index(&self) -> Option<&IndexUrl> {
171 match &self.inner {
172 VersionMapInner::Eager(_) => None,
173 VersionMapInner::Lazy(lazy) => Some(&lazy.index),
174 }
175 }
176
177 pub(crate) fn included_version_cutoff(&self) -> Option<&Timestamp> {
179 match &self.inner {
180 VersionMapInner::Eager(_) => None,
181 VersionMapInner::Lazy(lazy) => lazy.included_version_cutoff.as_ref(),
182 }
183 }
184
185 pub(crate) fn iter(
192 &self,
193 range: &Ranges<Version>,
194 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
195 if let Some(version) = range.as_singleton() {
197 either::Either::Left(match self.inner {
198 VersionMapInner::Eager(ref eager) => {
199 either::Either::Left(eager.map.get_key_value(version).into_iter().map(
200 move |(version, dist)| {
201 let version_map_dist = VersionMapDistHandle {
202 inner: VersionMapDistHandleInner::Eager(dist),
203 };
204 (version, version_map_dist)
205 },
206 ))
207 }
208 VersionMapInner::Lazy(ref lazy) => {
209 either::Either::Right(lazy.map.get(version).into_iter().map(move |entry| {
210 let version_map_dist = VersionMapDistHandle {
211 inner: VersionMapDistHandleInner::Lazy {
212 lazy,
213 dist: &entry.dist,
214 },
215 };
216 (&entry.version, version_map_dist)
217 }))
218 }
219 })
220 } else {
221 either::Either::Right(match self.inner {
222 VersionMapInner::Eager(ref eager) => {
223 either::Either::Left(eager.map.range(BoundingRange::from(range)).map(
224 |(version, dist)| {
225 let version_map_dist = VersionMapDistHandle {
226 inner: VersionMapDistHandleInner::Eager(dist),
227 };
228 (version, version_map_dist)
229 },
230 ))
231 }
232 VersionMapInner::Lazy(ref lazy) => {
233 either::Either::Right(lazy.map.range(BoundingRange::from(range)).iter().map(
234 |entry| {
235 let version_map_dist = VersionMapDistHandle {
236 inner: VersionMapDistHandleInner::Lazy {
237 lazy,
238 dist: &entry.dist,
239 },
240 };
241 (&entry.version, version_map_dist)
242 },
243 ))
244 }
245 })
246 }
247 }
248
249 pub(crate) fn iter_included(
255 &self,
256 range: &Ranges<Version>,
257 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
258 self.iter(range).filter(|(_, dist)| dist.is_included())
259 }
260
261 pub(crate) fn hashes(&self, version: &Version) -> Option<&[HashDigest]> {
263 match self.inner {
264 VersionMapInner::Eager(ref eager) => {
265 eager.map.get(version).map(PrioritizedDist::hashes)
266 }
267 VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
268 }
269 }
270
271 pub(crate) fn len(&self) -> usize {
276 match self.inner {
277 VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
278 VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
279 }
280 }
281
282 pub(crate) fn local(&self) -> bool {
284 match self.inner {
285 VersionMapInner::Eager(ref map) => map.local,
286 VersionMapInner::Lazy(ref map) => map.local,
287 }
288 }
289}
290
291impl From<FlatDistributions> for VersionMap {
292 fn from(flat_index: FlatDistributions) -> Self {
293 let local = flat_index.iter().any(|(version, _)| version.is_local());
294 let map = flat_index.into();
295 Self {
296 inner: VersionMapInner::Eager(VersionMapEager { map, local }),
297 }
298 }
299}
300
301pub(crate) struct VersionMapDistHandle<'a> {
314 inner: VersionMapDistHandleInner<'a>,
315}
316
317enum VersionMapDistHandleInner<'a> {
318 Eager(&'a PrioritizedDist),
319 Lazy {
320 lazy: &'a VersionMapLazy,
321 dist: &'a LazyPrioritizedDist,
322 },
323}
324
325impl<'a> VersionMapDistHandle<'a> {
326 fn is_included(&self) -> bool {
328 match self.inner {
329 VersionMapDistHandleInner::Eager(_) => true,
330 VersionMapDistHandleInner::Lazy { lazy, dist } => match (&dist.flat, &dist.simple) {
331 (Some(_), _) => true,
332 (None, Some(simple)) => lazy.any_file_materializable(simple),
333 (None, None) => false,
334 },
335 }
336 }
337
338 pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
340 match self.inner {
341 VersionMapDistHandleInner::Eager(dist) => Some(dist),
342 VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
343 }
344 }
345}
346
347#[derive(Debug)]
349#[expect(clippy::large_enum_variant)]
350enum VersionMapInner {
351 Eager(VersionMapEager),
356 Lazy(VersionMapLazy),
362}
363
364#[derive(Debug)]
366struct VersionMapEager {
367 map: BTreeMap<Version, PrioritizedDist>,
369 local: bool,
371}
372
373#[derive(Debug)]
375struct VersionMapLazyEntry {
376 version: Version,
377 dist: LazyPrioritizedDist,
378}
379
380#[derive(Debug)]
382struct VersionMapLazyIndex {
383 entries: Vec<VersionMapLazyEntry>,
384}
385
386impl VersionMapLazyIndex {
387 fn merge_flat(self, flat_index: FlatDistributions) -> Self {
389 let flat_count = flat_index.iter().size_hint().0;
390 let mut merged = Vec::with_capacity(self.entries.len() + flat_count);
391 let mut simple = self.entries.into_iter().peekable();
392 let mut flat = flat_index.into_iter().peekable();
393 let flat_entry = |(version, dist)| VersionMapLazyEntry {
394 version,
395 dist: LazyPrioritizedDist {
396 flat: Some(dist),
397 simple: None,
398 },
399 };
400 while let (Some(simple_entry), Some((flat_version, _))) = (simple.peek(), flat.peek()) {
401 match simple_entry.version.cmp(flat_version) {
402 std::cmp::Ordering::Less => {
403 if let Some(entry) = simple.next() {
404 merged.push(entry);
405 }
406 }
407 std::cmp::Ordering::Greater => {
408 if let Some(entry) = flat.next() {
409 merged.push(flat_entry(entry));
410 }
411 }
412 std::cmp::Ordering::Equal => {
413 if let (Some(mut entry), Some((_, dist))) = (simple.next(), flat.next()) {
414 entry.dist.flat = Some(dist);
415 merged.push(entry);
416 }
417 }
418 }
419 }
420 merged.extend(simple);
421 merged.extend(flat.map(flat_entry));
422
423 Self { entries: merged }
424 }
425
426 fn get(&self, version: &Version) -> Option<&VersionMapLazyEntry> {
427 let index = self
428 .entries
429 .binary_search_by(|entry| entry.version.cmp(version))
430 .ok()?;
431 self.entries.get(index)
432 }
433
434 fn keys(&self) -> impl DoubleEndedIterator<Item = &Version> {
435 self.entries.iter().map(|entry| &entry.version)
436 }
437
438 fn range(&self, range: BoundingRange<'_>) -> &[VersionMapLazyEntry] {
439 let start = match range.min {
440 Bound::Included(version) => self
441 .entries
442 .partition_point(|entry| entry.version < *version),
443 Bound::Excluded(version) => self
444 .entries
445 .partition_point(|entry| entry.version <= *version),
446 Bound::Unbounded => 0,
447 };
448 let end = match range.max {
449 Bound::Included(version) => self
450 .entries
451 .partition_point(|entry| entry.version <= *version),
452 Bound::Excluded(version) => self
453 .entries
454 .partition_point(|entry| entry.version < *version),
455 Bound::Unbounded => self.entries.len(),
456 };
457 self.entries.get(start..end).unwrap_or_default()
458 }
459
460 fn len(&self) -> usize {
461 self.entries.len()
462 }
463}
464
465#[derive(Debug)]
474struct VersionMapLazy {
475 package_name: PackageName,
477 map: VersionMapLazyIndex,
479 local: bool,
481 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
484 no_binary: bool,
486 no_build: bool,
488 index: IndexUrl,
490 tags: Option<Tags>,
493 included_version_cutoff: Option<Timestamp>,
496 available_version_cutoff: Option<Timestamp>,
498 allowed_yanks: AllowedYanks,
500 hasher: HashStrategy,
502 requires_python: RequiresPython,
504 minimum_libc_version: Option<MinimumLibcVersion>,
506}
507
508impl VersionMapLazy {
509 fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
511 let archived = self
512 .map
513 .get(version)
514 .and_then(|entry| entry.dist.simple.as_ref())
515 .and_then(|simple| self.simple_metadata.datum(simple.datum_index))
516 .and_then(|datum| datum.metadata.as_deref())?;
517 Some(
518 rkyv::deserialize::<ResolutionMetadata, rkyv::rancor::Error>(archived)
519 .expect("archived metadata always deserializes"),
520 )
521 }
522
523 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
525 self.get_lazy(&self.map.get(version)?.dist)
526 }
527
528 fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
531 self.map.entries.iter().filter_map(move |entry| {
532 let included = match (&entry.dist.flat, &entry.dist.simple) {
533 (Some(_), _) => true,
535 (None, Some(simple)) => self.any_file_included(simple),
536 (None, None) => false,
537 };
538 included.then_some(&entry.version)
539 })
540 }
541
542 fn any_file_included(&self, simple: &SimplePrioritizedDist) -> bool {
547 if self.included_version_cutoff.is_none() && self.available_version_cutoff.is_none() {
548 return true;
549 }
550 let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
551 return false;
552 };
553 let files = &datum.files;
554 files
555 .wheels
556 .iter()
557 .chain(files.source_dists.iter())
558 .any(|file| {
559 let upload_time = file.upload_time_utc_ms();
560 let excluded = if let Some(cutoff) = &self.included_version_cutoff {
561 upload_time.is_none_or(|t| t >= cutoff.as_millisecond())
562 } else if let Some(cutoff) = &self.available_version_cutoff {
563 upload_time.is_some_and(|t| t >= cutoff.as_millisecond())
564 } else {
565 false
566 };
567 !excluded
568 })
569 }
570
571 fn any_file_materializable(&self, simple: &SimplePrioritizedDist) -> bool {
576 let Some(cutoff) = self
577 .included_version_cutoff
578 .as_ref()
579 .or(self.available_version_cutoff.as_ref())
580 else {
581 return true;
582 };
583 let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
584 return false;
585 };
586 datum
587 .files
588 .wheels
589 .iter()
590 .chain(datum.files.source_dists.iter())
591 .any(|file| {
592 file.upload_time_utc_ms()
593 .is_none_or(|upload_time| upload_time < cutoff.as_millisecond())
594 })
595 }
596
597 fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
603 match (&lazy_dist.flat, &lazy_dist.simple) {
604 (Some(flat), Some(simple)) => self.get_simple(Some(flat), simple),
605 (Some(flat), None) => Some(flat),
606 (None, Some(simple)) => self.get_simple(None, simple),
607 (None, None) => None,
608 }
609 }
610
611 fn get_simple<'p>(
616 &'p self,
617 init: Option<&'p PrioritizedDist>,
618 simple: &'p SimplePrioritizedDist,
619 ) -> Option<&'p PrioritizedDist> {
620 let get_or_init = || {
621 let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
622 &self
623 .simple_metadata
624 .datum(simple.datum_index)
625 .expect("index to lazy dist is correct")
626 .files,
627 )
628 .expect("archived version files always deserializes");
629 let mut priority_dist = init.cloned().unwrap_or_default();
630 for (filename, file) in files.all(&self.package_name) {
631 let (excluded, upload_time) = if let Some(included_version_cutoff) =
634 &self.included_version_cutoff
635 {
636 match file.upload_time_utc_ms.as_ref() {
637 Some(&upload_time)
638 if upload_time >= included_version_cutoff.as_millisecond() =>
639 {
640 trace!(
641 "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({included_version_cutoff})",
642 file.filename
643 );
644 (true, Some(upload_time))
645 }
646 None => {
647 warn_user_once!(
648 "`{}` is missing an upload date, but user provided: {included_version_cutoff}",
649 file.filename,
650 );
651 (true, None)
652 }
653 _ => (false, None),
654 }
655 } else if let Some(available_version_cutoff) = &self.available_version_cutoff {
656 match file.upload_time_utc_ms.as_ref() {
657 Some(&upload_time)
658 if upload_time >= available_version_cutoff.as_millisecond() =>
659 {
660 trace!(
661 "Excluding `{}` (uploaded {upload_time}) due to available version cutoff ({available_version_cutoff})",
662 file.filename
663 );
664 (true, Some(upload_time))
665 }
666 _ => (false, None),
667 }
668 } else {
669 (false, None)
670 };
671
672 let yanked = file.yanked.as_deref();
674 let hashes = file.hashes.clone();
675 match filename {
676 DistFilename::WheelFilename(filename) => {
677 let compatibility = self.wheel_compatibility(
678 &filename,
679 &filename.name,
680 &filename.version,
681 hashes.as_slice(),
682 yanked,
683 excluded,
684 upload_time,
685 );
686 let dist = RegistryBuiltWheel {
687 filename,
688 file: Box::new(file),
689 index: self.index.clone(),
690 size_is_authoritative: false,
691 };
692 priority_dist.insert_built(
693 dist,
694 hashes,
695 compatibility,
696 self.minimum_libc_version,
697 );
698 }
699 DistFilename::SourceDistFilename(filename) => {
700 let compatibility = self.source_dist_compatibility(
701 &filename,
702 hashes.as_slice(),
703 yanked,
704 excluded,
705 upload_time,
706 );
707 let dist = RegistrySourceDist {
708 name: filename.name.clone(),
709 version: filename.version.clone(),
710 ext: filename.extension,
711 file: Box::new(file),
712 index: self.index.clone(),
713 wheels: vec![],
714 size_is_authoritative: false,
715 };
716 priority_dist.insert_source(dist, hashes, compatibility);
717 }
718 }
719 }
720 if priority_dist.is_empty() {
721 None
722 } else {
723 Some(priority_dist)
724 }
725 };
726 simple.dist.get_or_init(get_or_init).as_ref()
727 }
728
729 fn source_dist_compatibility(
730 &self,
731 filename: &SourceDistFilename,
732 hashes: &[HashDigest],
733 yanked: Option<&Yanked>,
734 excluded: bool,
735 upload_time: Option<i64>,
736 ) -> SourceDistCompatibility {
737 if excluded {
739 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
740 upload_time,
741 ));
742 }
743
744 if self.no_build {
746 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
747 }
748
749 if let Some(yanked) = yanked {
751 if yanked.is_yanked()
752 && !self
753 .allowed_yanks
754 .contains(&filename.name, &filename.version)
755 {
756 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
757 yanked.clone(),
758 ));
759 }
760 }
761
762 if !filename.extension.is_pep625_compliant() {
767 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
768 }
769
770 let hash_policy = self
772 .hasher
773 .archive_policy_for_package(&filename.name, &filename.version);
774 let required_hashes = hash_policy.digests();
775 let hash = if required_hashes.is_empty() {
776 HashComparison::Matched
777 } else {
778 if hashes.is_empty() {
779 HashComparison::Missing
780 } else if hash_policy.matches(hashes) {
781 HashComparison::Matched
782 } else {
783 HashComparison::Mismatched
784 }
785 };
786
787 SourceDistCompatibility::Compatible(hash)
788 }
789
790 fn wheel_compatibility(
791 &self,
792 filename: &WheelFilename,
793 name: &PackageName,
794 version: &Version,
795 hashes: &[HashDigest],
796 yanked: Option<&Yanked>,
797 excluded: bool,
798 upload_time: Option<i64>,
799 ) -> WheelCompatibility {
800 if excluded {
802 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
803 }
804
805 if self.no_binary {
807 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
808 }
809
810 if let Some(yanked) = yanked {
812 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
813 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
814 }
815 }
816
817 let priority = if let Some(tags) = &self.tags {
819 match filename.compatibility(tags) {
820 TagCompatibility::Incompatible(tag) => {
821 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
822 }
823 TagCompatibility::Compatible(priority) => Some(priority),
824 }
825 } else {
826 if !self.requires_python.matches_wheel_tag(filename) {
829 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
830 IncompatibleTag::AbiPythonVersion,
831 ));
832 }
833 None
834 };
835
836 let hash_policy = self.hasher.archive_policy_for_package(name, version);
838 let required_hashes = hash_policy.digests();
839 let hash = if required_hashes.is_empty() {
840 HashComparison::Matched
841 } else {
842 if hashes.is_empty() {
843 HashComparison::Missing
844 } else if hash_policy.matches(hashes) {
845 HashComparison::Matched
846 } else {
847 HashComparison::Mismatched
848 }
849 };
850
851 let build_tag = filename.build_tag().cloned();
853
854 WheelCompatibility::Compatible(hash, priority, build_tag)
855 }
856}
857
858#[derive(Debug)]
860struct LazyPrioritizedDist {
861 flat: Option<PrioritizedDist>,
863 simple: Option<SimplePrioritizedDist>,
865}
866
867#[derive(Debug)]
869struct SimplePrioritizedDist {
870 datum_index: usize,
874 dist: OnceLock<Option<PrioritizedDist>>,
882}
883
884#[derive(Debug)]
886struct BoundingRange<'a> {
887 min: Bound<&'a Version>,
888 max: Bound<&'a Version>,
889}
890
891impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
892 fn from(value: &'a Ranges<Version>) -> Self {
893 let (min, max) = value
894 .bounding_range()
895 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
896 Self { min, max }
897 }
898}
899
900impl<'a> RangeBounds<Version> for BoundingRange<'a> {
901 fn start_bound(&self) -> Bound<&'a Version> {
902 self.min
903 }
904
905 fn end_bound(&self) -> Bound<&'a Version> {
906 self.max
907 }
908}