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