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, WheelFilename};
13use uv_distribution_types::{
14 HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
15 RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility,
16 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 ) -> Self {
59 let mut stable = false;
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 stable |= version.is_stable();
70 local |= version.is_local();
71 debug_assert!(
72 entries
73 .last()
74 .is_none_or(|entry: &VersionMapLazyEntry| entry.version < version),
75 "simple metadata versions must be sorted and unique"
76 );
77 entries.push(VersionMapLazyEntry {
78 version,
79 dist: LazyPrioritizedDist {
80 flat: None,
81 simple: Some(SimplePrioritizedDist {
82 datum_index,
83 dist: OnceLock::new(),
84 }),
85 },
86 });
87 }
88 let mut map = VersionMapLazyIndex { entries };
89 if let Some(flat_index) = flat_index {
92 stable |= flat_index.iter().any(|(version, _)| version.is_stable());
93 map = map.merge_flat(flat_index);
94 }
95 Self {
96 inner: VersionMapInner::Lazy(VersionMapLazy {
97 package_name: package_name.clone(),
98 map,
99 stable,
100 local,
101 simple_metadata,
102 no_binary: build_options.no_binary_package(package_name),
103 no_build: build_options.no_build_package(package_name),
104 index,
105 tags,
106 allowed_yanks,
107 hasher,
108 requires_python,
109 included_version_cutoff,
110 available_version_cutoff,
111 }),
112 }
113 }
114
115 #[instrument(skip_all, fields(package_name))]
116 pub(crate) fn from_flat_metadata(
117 flat_metadata: Vec<FlatIndexEntry>,
118 tags: Option<&Tags>,
119 hasher: &HashStrategy,
120 build_options: &BuildOptions,
121 ) -> Self {
122 let mut stable = false;
123 let mut local = false;
124 let mut map = BTreeMap::new();
125
126 for (version, prioritized_dist) in
127 FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options)
128 {
129 stable |= version.is_stable();
130 local |= version.is_local();
131 map.insert(version, prioritized_dist);
132 }
133
134 Self {
135 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
136 }
137 }
138
139 pub(crate) fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
141 match self.inner {
142 VersionMapInner::Eager(_) => None,
143 VersionMapInner::Lazy(ref lazy) => lazy.get_metadata(version),
144 }
145 }
146
147 pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
149 match self.inner {
150 VersionMapInner::Eager(ref eager) => eager.map.get(version),
151 VersionMapInner::Lazy(ref lazy) => lazy.get(version),
152 }
153 }
154
155 pub(crate) fn versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
157 match &self.inner {
158 VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
159 VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.map.keys()),
160 }
161 }
162
163 pub(crate) fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
171 match &self.inner {
172 VersionMapInner::Eager(eager) => either::Either::Left(eager.map.keys()),
173 VersionMapInner::Lazy(lazy) => either::Either::Right(lazy.included_versions()),
174 }
175 }
176
177 pub(crate) fn index(&self) -> Option<&IndexUrl> {
179 match &self.inner {
180 VersionMapInner::Eager(_) => None,
181 VersionMapInner::Lazy(lazy) => Some(&lazy.index),
182 }
183 }
184
185 pub(crate) fn included_version_cutoff(&self) -> Option<&Timestamp> {
187 match &self.inner {
188 VersionMapInner::Eager(_) => None,
189 VersionMapInner::Lazy(lazy) => lazy.included_version_cutoff.as_ref(),
190 }
191 }
192
193 pub(crate) fn iter(
200 &self,
201 range: &Ranges<Version>,
202 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
203 if let Some(version) = range.as_singleton() {
205 either::Either::Left(match self.inner {
206 VersionMapInner::Eager(ref eager) => {
207 either::Either::Left(eager.map.get_key_value(version).into_iter().map(
208 move |(version, dist)| {
209 let version_map_dist = VersionMapDistHandle {
210 inner: VersionMapDistHandleInner::Eager(dist),
211 };
212 (version, version_map_dist)
213 },
214 ))
215 }
216 VersionMapInner::Lazy(ref lazy) => {
217 either::Either::Right(lazy.map.get(version).into_iter().map(move |entry| {
218 let version_map_dist = VersionMapDistHandle {
219 inner: VersionMapDistHandleInner::Lazy {
220 lazy,
221 dist: &entry.dist,
222 },
223 };
224 (&entry.version, version_map_dist)
225 }))
226 }
227 })
228 } else {
229 either::Either::Right(match self.inner {
230 VersionMapInner::Eager(ref eager) => {
231 either::Either::Left(eager.map.range(BoundingRange::from(range)).map(
232 |(version, dist)| {
233 let version_map_dist = VersionMapDistHandle {
234 inner: VersionMapDistHandleInner::Eager(dist),
235 };
236 (version, version_map_dist)
237 },
238 ))
239 }
240 VersionMapInner::Lazy(ref lazy) => {
241 either::Either::Right(lazy.map.range(BoundingRange::from(range)).iter().map(
242 |entry| {
243 let version_map_dist = VersionMapDistHandle {
244 inner: VersionMapDistHandleInner::Lazy {
245 lazy,
246 dist: &entry.dist,
247 },
248 };
249 (&entry.version, version_map_dist)
250 },
251 ))
252 }
253 })
254 }
255 }
256
257 pub(crate) fn iter_included(
263 &self,
264 range: &Ranges<Version>,
265 ) -> impl DoubleEndedIterator<Item = (&Version, VersionMapDistHandle<'_>)> {
266 self.iter(range).filter(|(_, dist)| dist.is_included())
267 }
268
269 pub(crate) fn hashes(&self, version: &Version) -> Option<&[HashDigest]> {
271 match self.inner {
272 VersionMapInner::Eager(ref eager) => {
273 eager.map.get(version).map(PrioritizedDist::hashes)
274 }
275 VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(PrioritizedDist::hashes),
276 }
277 }
278
279 pub(crate) fn len(&self) -> usize {
284 match self.inner {
285 VersionMapInner::Eager(VersionMapEager { ref map, .. }) => map.len(),
286 VersionMapInner::Lazy(VersionMapLazy { ref map, .. }) => map.len(),
287 }
288 }
289
290 pub(crate) fn stable(&self) -> bool {
292 match self.inner {
293 VersionMapInner::Eager(ref map) => map.stable,
294 VersionMapInner::Lazy(ref map) => map.stable,
295 }
296 }
297
298 pub(crate) fn local(&self) -> bool {
300 match self.inner {
301 VersionMapInner::Eager(ref map) => map.local,
302 VersionMapInner::Lazy(ref map) => map.local,
303 }
304 }
305}
306
307impl From<FlatDistributions> for VersionMap {
308 fn from(flat_index: FlatDistributions) -> Self {
309 let stable = flat_index.iter().any(|(version, _)| version.is_stable());
310 let local = flat_index.iter().any(|(version, _)| version.is_local());
311 let map = flat_index.into();
312 Self {
313 inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
314 }
315 }
316}
317
318pub(crate) struct VersionMapDistHandle<'a> {
331 inner: VersionMapDistHandleInner<'a>,
332}
333
334enum VersionMapDistHandleInner<'a> {
335 Eager(&'a PrioritizedDist),
336 Lazy {
337 lazy: &'a VersionMapLazy,
338 dist: &'a LazyPrioritizedDist,
339 },
340}
341
342impl<'a> VersionMapDistHandle<'a> {
343 fn is_included(&self) -> bool {
345 match self.inner {
346 VersionMapDistHandleInner::Eager(_) => true,
347 VersionMapDistHandleInner::Lazy { lazy, dist } => match (&dist.flat, &dist.simple) {
348 (Some(_), _) => true,
349 (None, Some(simple)) => lazy.any_file_materializable(simple),
350 (None, None) => false,
351 },
352 }
353 }
354
355 pub(crate) fn prioritized_dist(&self) -> Option<&'a PrioritizedDist> {
357 match self.inner {
358 VersionMapDistHandleInner::Eager(dist) => Some(dist),
359 VersionMapDistHandleInner::Lazy { lazy, dist } => Some(lazy.get_lazy(dist)?),
360 }
361 }
362}
363
364#[derive(Debug)]
366#[expect(clippy::large_enum_variant)]
367enum VersionMapInner {
368 Eager(VersionMapEager),
373 Lazy(VersionMapLazy),
379}
380
381#[derive(Debug)]
383struct VersionMapEager {
384 map: BTreeMap<Version, PrioritizedDist>,
386 stable: bool,
388 local: bool,
390}
391
392#[derive(Debug)]
394struct VersionMapLazyEntry {
395 version: Version,
396 dist: LazyPrioritizedDist,
397}
398
399#[derive(Debug)]
401struct VersionMapLazyIndex {
402 entries: Vec<VersionMapLazyEntry>,
403}
404
405impl VersionMapLazyIndex {
406 fn merge_flat(self, flat_index: FlatDistributions) -> Self {
408 let flat_count = flat_index.iter().size_hint().0;
409 let mut merged = Vec::with_capacity(self.entries.len() + flat_count);
410 let mut simple = self.entries.into_iter().peekable();
411 let mut flat = flat_index.into_iter().peekable();
412 let flat_entry = |(version, dist)| VersionMapLazyEntry {
413 version,
414 dist: LazyPrioritizedDist {
415 flat: Some(dist),
416 simple: None,
417 },
418 };
419 while let (Some(simple_entry), Some((flat_version, _))) = (simple.peek(), flat.peek()) {
420 match simple_entry.version.cmp(flat_version) {
421 std::cmp::Ordering::Less => {
422 if let Some(entry) = simple.next() {
423 merged.push(entry);
424 }
425 }
426 std::cmp::Ordering::Greater => {
427 if let Some(entry) = flat.next() {
428 merged.push(flat_entry(entry));
429 }
430 }
431 std::cmp::Ordering::Equal => {
432 if let (Some(mut entry), Some((_, dist))) = (simple.next(), flat.next()) {
433 entry.dist.flat = Some(dist);
434 merged.push(entry);
435 }
436 }
437 }
438 }
439 merged.extend(simple);
440 merged.extend(flat.map(flat_entry));
441
442 Self { entries: merged }
443 }
444
445 fn get(&self, version: &Version) -> Option<&VersionMapLazyEntry> {
446 let index = self
447 .entries
448 .binary_search_by(|entry| entry.version.cmp(version))
449 .ok()?;
450 self.entries.get(index)
451 }
452
453 fn keys(&self) -> impl DoubleEndedIterator<Item = &Version> {
454 self.entries.iter().map(|entry| &entry.version)
455 }
456
457 fn range(&self, range: BoundingRange<'_>) -> &[VersionMapLazyEntry] {
458 let start = match range.min {
459 Bound::Included(version) => self
460 .entries
461 .partition_point(|entry| entry.version < *version),
462 Bound::Excluded(version) => self
463 .entries
464 .partition_point(|entry| entry.version <= *version),
465 Bound::Unbounded => 0,
466 };
467 let end = match range.max {
468 Bound::Included(version) => self
469 .entries
470 .partition_point(|entry| entry.version <= *version),
471 Bound::Excluded(version) => self
472 .entries
473 .partition_point(|entry| entry.version < *version),
474 Bound::Unbounded => self.entries.len(),
475 };
476 self.entries.get(start..end).unwrap_or_default()
477 }
478
479 fn len(&self) -> usize {
480 self.entries.len()
481 }
482}
483
484#[derive(Debug)]
493struct VersionMapLazy {
494 package_name: PackageName,
496 map: VersionMapLazyIndex,
498 stable: bool,
500 local: bool,
502 simple_metadata: OwnedArchive<SimpleDetailMetadata>,
505 no_binary: bool,
507 no_build: bool,
509 index: IndexUrl,
511 tags: Option<Tags>,
514 included_version_cutoff: Option<Timestamp>,
517 available_version_cutoff: Option<Timestamp>,
519 allowed_yanks: AllowedYanks,
521 hasher: HashStrategy,
523 requires_python: RequiresPython,
525}
526
527impl VersionMapLazy {
528 fn get_metadata(&self, version: &Version) -> Option<ResolutionMetadata> {
530 let archived = self
531 .map
532 .get(version)
533 .and_then(|entry| entry.dist.simple.as_ref())
534 .and_then(|simple| self.simple_metadata.datum(simple.datum_index))
535 .and_then(|datum| datum.metadata.as_deref())?;
536 Some(
537 rkyv::deserialize::<ResolutionMetadata, rkyv::rancor::Error>(archived)
538 .expect("archived metadata always deserializes"),
539 )
540 }
541
542 fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
544 self.get_lazy(&self.map.get(version)?.dist)
545 }
546
547 fn included_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
550 self.map.entries.iter().filter_map(move |entry| {
551 let included = match (&entry.dist.flat, &entry.dist.simple) {
552 (Some(_), _) => true,
554 (None, Some(simple)) => self.any_file_included(simple),
555 (None, None) => false,
556 };
557 included.then_some(&entry.version)
558 })
559 }
560
561 fn any_file_included(&self, simple: &SimplePrioritizedDist) -> bool {
566 if self.included_version_cutoff.is_none() && self.available_version_cutoff.is_none() {
567 return true;
568 }
569 let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
570 return false;
571 };
572 let files = &datum.files;
573 files
574 .wheels
575 .iter()
576 .chain(files.source_dists.iter())
577 .any(|file| {
578 let upload_time = file.upload_time_utc_ms();
579 let excluded = if let Some(cutoff) = &self.included_version_cutoff {
580 upload_time.is_none_or(|t| t >= cutoff.as_millisecond())
581 } else if let Some(cutoff) = &self.available_version_cutoff {
582 upload_time.is_some_and(|t| t >= cutoff.as_millisecond())
583 } else {
584 false
585 };
586 !excluded
587 })
588 }
589
590 fn any_file_materializable(&self, simple: &SimplePrioritizedDist) -> bool {
595 let Some(cutoff) = self
596 .included_version_cutoff
597 .as_ref()
598 .or(self.available_version_cutoff.as_ref())
599 else {
600 return true;
601 };
602 let Some(datum) = self.simple_metadata.datum(simple.datum_index) else {
603 return false;
604 };
605 datum
606 .files
607 .wheels
608 .iter()
609 .chain(datum.files.source_dists.iter())
610 .any(|file| {
611 file.upload_time_utc_ms()
612 .is_none_or(|upload_time| upload_time < cutoff.as_millisecond())
613 })
614 }
615
616 fn get_lazy<'p>(&'p self, lazy_dist: &'p LazyPrioritizedDist) -> Option<&'p PrioritizedDist> {
622 match (&lazy_dist.flat, &lazy_dist.simple) {
623 (Some(flat), Some(simple)) => self.get_simple(Some(flat), simple),
624 (Some(flat), None) => Some(flat),
625 (None, Some(simple)) => self.get_simple(None, simple),
626 (None, None) => None,
627 }
628 }
629
630 fn get_simple<'p>(
635 &'p self,
636 init: Option<&'p PrioritizedDist>,
637 simple: &'p SimplePrioritizedDist,
638 ) -> Option<&'p PrioritizedDist> {
639 let get_or_init = || {
640 let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(
641 &self
642 .simple_metadata
643 .datum(simple.datum_index)
644 .expect("index to lazy dist is correct")
645 .files,
646 )
647 .expect("archived version files always deserializes");
648 let mut priority_dist = init.cloned().unwrap_or_default();
649 for (filename, file) in files.all(&self.package_name) {
650 let (excluded, upload_time) = if let Some(included_version_cutoff) =
653 &self.included_version_cutoff
654 {
655 match file.upload_time_utc_ms.as_ref() {
656 Some(&upload_time)
657 if upload_time >= included_version_cutoff.as_millisecond() =>
658 {
659 trace!(
660 "Excluding `{}` (uploaded {upload_time}) due to exclude-newer ({included_version_cutoff})",
661 file.filename
662 );
663 (true, Some(upload_time))
664 }
665 None => {
666 warn_user_once!(
667 "{} is missing an upload date, but user provided: {included_version_cutoff}",
668 file.filename,
669 );
670 (true, None)
671 }
672 _ => (false, None),
673 }
674 } else if let Some(available_version_cutoff) = &self.available_version_cutoff {
675 match file.upload_time_utc_ms.as_ref() {
676 Some(&upload_time)
677 if upload_time >= available_version_cutoff.as_millisecond() =>
678 {
679 trace!(
680 "Excluding `{}` (uploaded {upload_time}) due to available version cutoff ({available_version_cutoff})",
681 file.filename
682 );
683 (true, Some(upload_time))
684 }
685 _ => (false, None),
686 }
687 } else {
688 (false, None)
689 };
690
691 let yanked = file.yanked.as_deref();
693 let hashes = file.hashes.clone();
694 match filename {
695 DistFilename::WheelFilename(filename) => {
696 let compatibility = self.wheel_compatibility(
697 &filename,
698 &filename.name,
699 &filename.version,
700 hashes.as_slice(),
701 yanked,
702 excluded,
703 upload_time,
704 );
705 let dist = RegistryBuiltWheel {
706 filename,
707 file: Box::new(file),
708 index: self.index.clone(),
709 };
710 priority_dist.insert_built(dist, hashes, compatibility);
711 }
712 DistFilename::SourceDistFilename(filename) => {
713 let compatibility = self.source_dist_compatibility(
714 &filename.name,
715 &filename.version,
716 hashes.as_slice(),
717 yanked,
718 excluded,
719 upload_time,
720 );
721 let dist = RegistrySourceDist {
722 name: filename.name.clone(),
723 version: filename.version.clone(),
724 ext: filename.extension,
725 file: Box::new(file),
726 index: self.index.clone(),
727 wheels: vec![],
728 };
729 priority_dist.insert_source(dist, hashes, compatibility);
730 }
731 }
732 }
733 if priority_dist.is_empty() {
734 None
735 } else {
736 Some(priority_dist)
737 }
738 };
739 simple.dist.get_or_init(get_or_init).as_ref()
740 }
741
742 fn source_dist_compatibility(
743 &self,
744 name: &PackageName,
745 version: &Version,
746 hashes: &[HashDigest],
747 yanked: Option<&Yanked>,
748 excluded: bool,
749 upload_time: Option<i64>,
750 ) -> SourceDistCompatibility {
751 if self.no_build {
753 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
754 }
755
756 if excluded {
758 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
759 upload_time,
760 ));
761 }
762
763 if let Some(yanked) = yanked {
765 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
766 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
767 yanked.clone(),
768 ));
769 }
770 }
771
772 let hash_policy = self.hasher.get_package(name, 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 self.no_binary {
802 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
803 }
804
805 if excluded {
807 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
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.get_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}