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, 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 local = false;
60 let mut entries = Vec::with_capacity(simple_metadata.iter().size_hint().0);
61 for (datum_index, datum) in simple_metadata.iter().enumerate() {
65 let version = rkyv::deserialize::<Version, rkyv::rancor::Error>(&datum.version)
66 .expect("archived version always deserializes");
67
68 local |= version.is_local();
69 debug_assert!(
70 entries
71 .last()
72 .is_none_or(|entry: &VersionMapLazyEntry| entry.version < version),
73 "simple metadata versions must be sorted and unique"
74 );
75 entries.push(VersionMapLazyEntry {
76 version,
77 dist: LazyPrioritizedDist {
78 flat: None,
79 simple: Some(SimplePrioritizedDist {
80 datum_index,
81 dist: OnceLock::new(),
82 }),
83 },
84 });
85 }
86 let mut map = VersionMapLazyIndex { entries };
87 if let Some(flat_index) = flat_index {
90 map = map.merge_flat(flat_index);
91 }
92 Self {
93 inner: VersionMapInner::Lazy(VersionMapLazy {
94 package_name: package_name.clone(),
95 map,
96 local,
97 simple_metadata,
98 no_binary: build_options.no_binary_package(package_name),
99 no_build: build_options.no_build_package(package_name),
100 index,
101 tags,
102 allowed_yanks,
103 hasher,
104 requires_python,
105 included_version_cutoff,
106 available_version_cutoff,
107 }),
108 }
109 }
110
111 #[instrument(skip_all, fields(package_name))]
112 pub(crate) fn from_flat_metadata(
113 flat_metadata: Vec<FlatIndexEntry>,
114 tags: Option<&Tags>,
115 hasher: &HashStrategy,
116 build_options: &BuildOptions,
117 ) -> Self {
118 let mut local = false;
119 let mut map = BTreeMap::new();
120
121 for (version, prioritized_dist) in
122 FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options)
123 {
124 local |= version.is_local();
125 map.insert(version, prioritized_dist);
126 }
127
128 Self {
129 inner: VersionMapInner::Eager(VersionMapEager { map, local }),
130 }
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}
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(dist, hashes, compatibility);
693 }
694 DistFilename::SourceDistFilename(filename) => {
695 let compatibility = self.source_dist_compatibility(
696 &filename,
697 hashes.as_slice(),
698 yanked,
699 excluded,
700 upload_time,
701 );
702 let dist = RegistrySourceDist {
703 name: filename.name.clone(),
704 version: filename.version.clone(),
705 ext: filename.extension,
706 file: Box::new(file),
707 index: self.index.clone(),
708 wheels: vec![],
709 size_is_authoritative: false,
710 };
711 priority_dist.insert_source(dist, hashes, compatibility);
712 }
713 }
714 }
715 if priority_dist.is_empty() {
716 None
717 } else {
718 Some(priority_dist)
719 }
720 };
721 simple.dist.get_or_init(get_or_init).as_ref()
722 }
723
724 fn source_dist_compatibility(
725 &self,
726 filename: &SourceDistFilename,
727 hashes: &[HashDigest],
728 yanked: Option<&Yanked>,
729 excluded: bool,
730 upload_time: Option<i64>,
731 ) -> SourceDistCompatibility {
732 if self.no_build {
734 return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
735 }
736
737 if excluded {
739 return SourceDistCompatibility::Incompatible(IncompatibleSource::ExcludeNewer(
740 upload_time,
741 ));
742 }
743
744 if let Some(yanked) = yanked {
746 if yanked.is_yanked()
747 && !self
748 .allowed_yanks
749 .contains(&filename.name, &filename.version)
750 {
751 return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
752 yanked.clone(),
753 ));
754 }
755 }
756
757 if !filename.extension.is_pep625_compliant() {
762 return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
763 }
764
765 let hash_policy = self.hasher.get_package(&filename.name, &filename.version);
767 let required_hashes = hash_policy.digests();
768 let hash = if required_hashes.is_empty() {
769 HashComparison::Matched
770 } else {
771 if hashes.is_empty() {
772 HashComparison::Missing
773 } else if hash_policy.matches(hashes) {
774 HashComparison::Matched
775 } else {
776 HashComparison::Mismatched
777 }
778 };
779
780 SourceDistCompatibility::Compatible(hash)
781 }
782
783 fn wheel_compatibility(
784 &self,
785 filename: &WheelFilename,
786 name: &PackageName,
787 version: &Version,
788 hashes: &[HashDigest],
789 yanked: Option<&Yanked>,
790 excluded: bool,
791 upload_time: Option<i64>,
792 ) -> WheelCompatibility {
793 if self.no_binary {
795 return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
796 }
797
798 if excluded {
800 return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time));
801 }
802
803 if let Some(yanked) = yanked {
805 if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
806 return WheelCompatibility::Incompatible(IncompatibleWheel::Yanked(yanked.clone()));
807 }
808 }
809
810 let priority = if let Some(tags) = &self.tags {
812 match filename.compatibility(tags) {
813 TagCompatibility::Incompatible(tag) => {
814 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
815 }
816 TagCompatibility::Compatible(priority) => Some(priority),
817 }
818 } else {
819 if !self.requires_python.matches_wheel_tag(filename) {
822 return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
823 IncompatibleTag::AbiPythonVersion,
824 ));
825 }
826 None
827 };
828
829 let hash_policy = self.hasher.get_package(name, version);
831 let required_hashes = hash_policy.digests();
832 let hash = if required_hashes.is_empty() {
833 HashComparison::Matched
834 } else {
835 if hashes.is_empty() {
836 HashComparison::Missing
837 } else if hash_policy.matches(hashes) {
838 HashComparison::Matched
839 } else {
840 HashComparison::Mismatched
841 }
842 };
843
844 let build_tag = filename.build_tag().cloned();
846
847 WheelCompatibility::Compatible(hash, priority, build_tag)
848 }
849}
850
851#[derive(Debug)]
853struct LazyPrioritizedDist {
854 flat: Option<PrioritizedDist>,
856 simple: Option<SimplePrioritizedDist>,
858}
859
860#[derive(Debug)]
862struct SimplePrioritizedDist {
863 datum_index: usize,
867 dist: OnceLock<Option<PrioritizedDist>>,
875}
876
877#[derive(Debug)]
879struct BoundingRange<'a> {
880 min: Bound<&'a Version>,
881 max: Bound<&'a Version>,
882}
883
884impl<'a> From<&'a Ranges<Version>> for BoundingRange<'a> {
885 fn from(value: &'a Ranges<Version>) -> Self {
886 let (min, max) = value
887 .bounding_range()
888 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
889 Self { min, max }
890 }
891}
892
893impl<'a> RangeBounds<Version> for BoundingRange<'a> {
894 fn start_bound(&self) -> Bound<&'a Version> {
895 self.min
896 }
897
898 fn end_bound(&self) -> Bound<&'a Version> {
899 self.max
900 }
901}