1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3use std::ops::Deref;
4use std::path::Path;
5use std::str::FromStr;
6use std::sync::{Arc, LazyLock, RwLock};
7
8use http::StatusCode;
9use itertools::Either;
10use rustc_hash::{FxHashMap, FxHashSet};
11use thiserror::Error;
12use url::ParseError;
13use uv_auth::RealmRef;
14use uv_cache_key::CanonicalUrl;
15use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
16use uv_pypi_types::HashAlgorithm;
17use uv_redacted::SafeUrl;
18use uv_redacted::policy::{SerializationPolicy, Stripped};
19use uv_warnings::warn_user;
20
21use crate::{ExcludeNewerOverride, Index, IndexStatusCodeStrategy, Verbatim};
22
23pub static PYPI_URL: LazyLock<SafeUrl> =
24 LazyLock::new(|| SafeUrl::parse("https://pypi.org/simple").unwrap());
25
26static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| {
27 Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url(
28 PYPI_URL.clone().with_policy(Stripped),
29 ))))
30});
31
32#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
34pub enum IndexUrl {
35 Pypi(Arc<VerbatimUrl<Stripped>>),
36 Url(Arc<VerbatimUrl<Stripped>>),
37 Path(Arc<VerbatimUrl<Stripped>>),
38}
39
40impl IndexUrl {
41 pub fn parse(path: &str, root_dir: Option<&Path>) -> Result<Self, IndexUrlError> {
46 let url = VerbatimUrl::from_url_or_path(path, root_dir)?;
47 Ok(Self::from(url))
48 }
49
50 pub fn root(&self) -> Option<SafeUrl> {
55 let mut segments = self.url().path_segments()?;
56 let last = match segments.next_back()? {
57 "" => segments.next_back()?,
59 segment => segment,
60 };
61
62 if !(last.eq_ignore_ascii_case("simple") || last.eq_ignore_ascii_case("+simple")) {
64 return None;
65 }
66
67 let mut url = self.url().clone();
68 url.path_segments_mut().ok()?.pop_if_empty().pop();
69 Some(url)
70 }
71}
72
73#[cfg(feature = "schemars")]
74impl schemars::JsonSchema for IndexUrl {
75 fn schema_name() -> Cow<'static, str> {
76 Cow::Borrowed("IndexUrl")
77 }
78
79 fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
80 schemars::json_schema!({
81 "type": "string",
82 "description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path."
83 })
84 }
85}
86
87impl IndexUrl {
88 #[inline]
89 fn inner(&self) -> &VerbatimUrl<Stripped> {
90 match self {
91 Self::Pypi(url) | Self::Url(url) | Self::Path(url) => url,
92 }
93 }
94
95 pub fn url(&self) -> &SafeUrl {
97 self.inner().raw().as_url()
98 }
99
100 pub fn into_url(self) -> SafeUrl<Stripped> {
102 match self {
103 Self::Pypi(url) | Self::Url(url) | Self::Path(url) => {
104 Arc::unwrap_or_clone(url).into_url()
105 }
106 }
107 }
108
109 pub fn without_credentials(&self) -> Cow<'_, SafeUrl> {
111 let url = self.url();
112 if url.username().is_empty() && url.password().is_none() {
113 Cow::Borrowed(url)
114 } else {
115 let mut url = url.clone();
116 let _ = url.set_username("");
117 let _ = url.set_password(None);
118 Cow::Owned(url)
119 }
120 }
121
122 pub fn warn_on_disambiguated_relative_path(&self) {
127 let Self::Path(verbatim_url) = &self else {
128 return;
129 };
130
131 if let Some(path) = verbatim_url.given()
132 && !is_disambiguated_path(path)
133 {
134 if cfg!(windows) {
135 warn_user!(
136 "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future"
137 );
138 } else {
139 warn_user!(
140 "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future"
141 );
142 }
143 }
144 }
145}
146
147impl Display for IndexUrl {
148 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
149 Display::fmt(self.inner(), f)
150 }
151}
152
153impl Verbatim for IndexUrl {
154 fn verbatim(&self) -> Cow<'_, str> {
155 self.inner().verbatim()
156 }
157}
158
159fn is_disambiguated_path(path: &str) -> bool {
165 if cfg!(windows) {
166 if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') {
167 return true;
168 }
169 }
170 if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() {
171 return true;
172 }
173 if let Some((scheme, _)) = split_scheme(path) {
175 return Scheme::parse(scheme).is_some();
176 }
177 false
179}
180
181#[derive(Error, Debug)]
183pub enum IndexUrlError {
184 #[error(transparent)]
185 Io(#[from] std::io::Error),
186 #[error(transparent)]
187 Url(#[from] ParseError),
188 #[error(transparent)]
189 VerbatimUrl(#[from] VerbatimUrlError),
190}
191
192impl FromStr for IndexUrl {
193 type Err = IndexUrlError;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 Self::parse(s, None)
197 }
198}
199
200impl serde::ser::Serialize for IndexUrl {
201 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202 where
203 S: serde::ser::Serializer,
204 {
205 self.inner().serialize(serializer)
206 }
207}
208
209impl<'de> serde::de::Deserialize<'de> for IndexUrl {
210 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211 where
212 D: serde::de::Deserializer<'de>,
213 {
214 struct Visitor;
215
216 impl serde::de::Visitor<'_> for Visitor {
217 type Value = IndexUrl;
218
219 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
220 f.write_str("a string")
221 }
222
223 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
224 IndexUrl::from_str(v).map_err(serde::de::Error::custom)
225 }
226 }
227
228 deserializer.deserialize_str(Visitor)
229 }
230}
231
232impl<P: SerializationPolicy> From<VerbatimUrl<P>> for IndexUrl {
233 fn from(url: VerbatimUrl<P>) -> Self {
234 let url = url.with_policy(Stripped);
235 if url.scheme() == "file" {
236 Self::Path(Arc::new(url))
237 } else if *url.raw().as_url() == *PYPI_URL {
238 Self::Pypi(Arc::new(url))
239 } else {
240 Self::Url(Arc::new(url))
241 }
242 }
243}
244
245impl From<IndexUrl> for SafeUrl {
246 fn from(index: IndexUrl) -> Self {
247 index.into_url().into_url()
248 }
249}
250
251impl Deref for IndexUrl {
252 type Target = SafeUrl;
253
254 fn deref(&self) -> &Self::Target {
255 self.url()
256 }
257}
258
259#[derive(Default, Debug, Clone, PartialEq, Eq)]
264pub struct IndexLocations {
265 indexes: Vec<Index>,
266 flat_index: Vec<Index>,
267 no_index: bool,
268}
269
270impl IndexLocations {
271 pub fn new(indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
273 Self {
274 indexes,
275 flat_index,
276 no_index,
277 }
278 }
279
280 #[must_use]
287 pub fn combine(self, indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
288 Self {
289 indexes: self.indexes.into_iter().chain(indexes).collect(),
290 flat_index: self.flat_index.into_iter().chain(flat_index).collect(),
291 no_index: self.no_index || no_index,
292 }
293 }
294
295 pub fn is_none(&self) -> bool {
298 *self == Self::default()
299 }
300}
301
302fn is_same_index(a: &IndexUrl, b: &IndexUrl) -> bool {
304 RealmRef::from(b.url()) == RealmRef::from(a.url())
305 && CanonicalUrl::new(a.url().clone()) == CanonicalUrl::new(b.url().clone())
306}
307
308impl<'a> IndexLocations {
309 fn configured_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
311 let mut seen = FxHashSet::default();
312 self.indexes
313 .iter()
314 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
315 }
316
317 pub fn default_index(&'a self) -> Option<&'a Index> {
323 if self.no_index {
324 None
325 } else {
326 self.configured_indexes()
327 .find(|index| index.default)
328 .or_else(|| Some(&DEFAULT_INDEX))
329 }
330 }
331
332 pub fn implicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
336 if self.no_index {
337 Either::Left(std::iter::empty())
338 } else {
339 Either::Right(
340 self.configured_indexes()
341 .filter(|index| !index.default && !index.explicit),
342 )
343 }
344 }
345
346 pub fn explicit_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
350 if self.no_index {
351 Either::Left(std::iter::empty())
352 } else {
353 Either::Right(self.configured_indexes().filter(|index| index.explicit))
354 }
355 }
356
357 pub fn indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
366 self.implicit_indexes()
367 .chain(self.default_index())
368 .filter(|index| !index.explicit)
369 }
370
371 pub fn fetch_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
375 let mut seen = FxHashSet::default();
376 self.indexes()
377 .filter(move |index| seen.insert(index.raw_url()))
378 }
379
380 pub fn simple_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
384 if self.no_index {
385 Either::Left(std::iter::empty())
386 } else {
387 Either::Right(self.configured_indexes())
388 }
389 }
390
391 pub fn flat_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
393 self.flat_index.iter()
394 }
395
396 pub fn no_index(&self) -> bool {
398 self.no_index
399 }
400
401 pub fn allowed_indexes(&'a self) -> Vec<&'a Index> {
408 if self.no_index {
409 self.flat_index.iter().rev().collect()
410 } else {
411 let mut indexes = vec![];
412
413 let mut seen = FxHashSet::default();
414 let mut default = false;
415 for index in {
416 self.indexes
417 .iter()
418 .chain(self.flat_index.iter())
419 .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name)))
420 } {
421 if index.default {
422 if default {
423 continue;
424 }
425 default = true;
426 }
427 indexes.push(index);
428 }
429 if !default {
430 indexes.push(&*DEFAULT_INDEX);
431 }
432
433 indexes.reverse();
434 indexes
435 }
436 }
437
438 pub fn known_indexes(&'a self) -> impl Iterator<Item = &'a Index> {
447 if self.no_index {
448 Either::Left(self.flat_index.iter().rev())
449 } else {
450 Either::Right(
451 std::iter::once(&*DEFAULT_INDEX)
452 .chain(self.flat_index.iter().rev())
453 .chain(self.indexes.iter().rev()),
454 )
455 }
456 }
457
458 pub fn defined_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
468 if self.no_index {
469 return Either::Left(std::iter::empty());
470 }
471
472 let (non_default, default) = self
473 .configured_indexes()
474 .partition::<Vec<_>, _>(|index| !index.default);
475
476 Either::Right(non_default.into_iter().chain(default))
477 }
478
479 fn index_for_url(&self, url: &IndexUrl) -> Option<&Index> {
481 self.indexes
482 .iter()
483 .find(|index| is_same_index(index.url(), url))
484 }
485
486 pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy {
488 self.index_for_url(url).map_or(
489 IndexStatusCodeStrategy::Default,
490 Index::status_code_strategy,
491 )
492 }
493
494 pub fn ignores_error_code_for(&self, url: &IndexUrl, status_code: StatusCode) -> bool {
496 self.index_for_url(url)
497 .is_some_and(|index| index.ignores_error_code(status_code))
498 }
499
500 pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
502 self.index_for_url(url)
503 .and_then(Index::simple_api_cache_control)
504 }
505
506 pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
508 self.index_for_url(url)
509 .and_then(Index::artifact_cache_control)
510 }
511
512 pub fn configured_files_cache_control_for(&self, url: &IndexUrl) -> Option<http::HeaderValue> {
514 self.index_for_url(url)
515 .and_then(|index| index.cache_control.as_ref())
516 .and_then(|cache_control| cache_control.files.clone())
517 }
518
519 pub fn hash_algorithm_for(&self, url: &IndexUrl) -> Option<HashAlgorithm> {
521 self.index_for_url(url)
522 .and_then(|index| index.hash_algorithm.map(HashAlgorithm::from))
523 }
524
525 pub fn exclude_newer_for(&self, url: &IndexUrl) -> Option<&ExcludeNewerOverride> {
527 self.index_for_url(url).and_then(Index::exclude_newer)
528 }
529}
530
531impl From<&IndexLocations> for uv_auth::Indexes {
532 fn from(index_locations: &IndexLocations) -> Self {
533 Self::from_indexes(index_locations.allowed_indexes().into_iter().map(|index| {
534 let mut url = index.url().url().clone();
535 url.set_username("").ok();
536 url.set_password(None).ok();
537 let mut root_url = index.url().root().unwrap_or_else(|| url.clone());
538 root_url.set_username("").ok();
539 root_url.set_password(None).ok();
540 uv_auth::Index {
541 url,
542 root_url,
543 auth_policy: index.authenticate,
544 }
545 }))
546 }
547}
548
549bitflags::bitflags! {
550 #[derive(Debug, Copy, Clone)]
551 struct Flags: u8 {
552 const NO_RANGE_REQUESTS = 1;
554 const UNAUTHORIZED = 1 << 2;
556 const FORBIDDEN = 1 << 1;
558 }
559}
560
561#[derive(Debug, Default, Clone)]
567pub struct IndexCapabilities(Arc<RwLock<FxHashMap<IndexUrl, Flags>>>);
568
569impl IndexCapabilities {
570 pub fn supports_range_requests(&self, index_url: &IndexUrl) -> bool {
572 !self
573 .0
574 .read()
575 .unwrap()
576 .get(index_url)
577 .is_some_and(|flags| flags.intersects(Flags::NO_RANGE_REQUESTS))
578 }
579
580 pub fn set_no_range_requests(&self, index_url: IndexUrl) {
582 self.0
583 .write()
584 .unwrap()
585 .entry(index_url)
586 .or_insert(Flags::empty())
587 .insert(Flags::NO_RANGE_REQUESTS);
588 }
589
590 pub fn unauthorized(&self, index_url: &IndexUrl) -> bool {
592 self.0
593 .read()
594 .unwrap()
595 .get(index_url)
596 .is_some_and(|flags| flags.intersects(Flags::UNAUTHORIZED))
597 }
598
599 pub(crate) fn set_unauthorized(&self, index_url: IndexUrl) {
601 self.0
602 .write()
603 .unwrap()
604 .entry(index_url)
605 .or_insert(Flags::empty())
606 .insert(Flags::UNAUTHORIZED);
607 }
608
609 pub fn forbidden(&self, index_url: &IndexUrl) -> bool {
611 self.0
612 .read()
613 .unwrap()
614 .get(index_url)
615 .is_some_and(|flags| flags.intersects(Flags::FORBIDDEN))
616 }
617
618 pub(crate) fn set_forbidden(&self, index_url: IndexUrl) {
620 self.0
621 .write()
622 .unwrap()
623 .entry(index_url)
624 .or_insert(Flags::empty())
625 .insert(Flags::FORBIDDEN);
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use std::error::Error;
632
633 use super::*;
634 use crate::{IndexCacheControl, IndexFormat, IndexName};
635 use http::HeaderValue;
636
637 fn index_urls<'a>(indexes: impl IntoIterator<Item = &'a Index>) -> Vec<&'a str> {
638 indexes
639 .into_iter()
640 .map(|index| index.url().url().as_str())
641 .collect()
642 }
643
644 #[test]
645 fn test_index_url_parse_valid_paths() {
646 assert!(is_disambiguated_path("/absolute/path"));
648 assert!(is_disambiguated_path("./relative/path"));
650 assert!(is_disambiguated_path("../../relative/path"));
651 if cfg!(windows) {
652 assert!(is_disambiguated_path("C:/absolute/path"));
654 assert!(is_disambiguated_path(".\\relative\\path"));
656 assert!(is_disambiguated_path("..\\..\\relative\\path"));
657 }
658 }
659
660 #[test]
661 fn test_index_url_parse_ambiguous_paths() {
662 assert!(!is_disambiguated_path("index"));
664 assert!(!is_disambiguated_path("relative/path"));
666 }
667
668 #[test]
669 fn test_index_url_parse_with_schemes() {
670 assert!(is_disambiguated_path("file:///absolute/path"));
671 assert!(is_disambiguated_path("https://registry.com/simple/"));
672 assert!(is_disambiguated_path(
673 "git+https://github.com/example/repo.git"
674 ));
675 }
676
677 #[test]
678 fn named_indexes_use_first_definition() -> Result<(), Box<dyn Error>> {
679 let first = Index::from_str("shared=https://first.example.com/simple")?;
680 let mut shadowed = Index::from_str("shared=https://shadowed.example.com/simple")?;
681 shadowed.explicit = true;
682 let mut explicit = Index::from_str("explicit=https://explicit.example.com/simple")?;
683 explicit.explicit = true;
684 let shadowed_implicit =
685 Index::from_str("explicit=https://shadowed-implicit.example.com/simple")?;
686 let mut default = Index::from_str("default=https://default.example.com/simple")?;
687 default.default = true;
688
689 let locations = IndexLocations::new(
690 vec![first, shadowed, explicit, shadowed_implicit, default],
691 vec![],
692 false,
693 );
694
695 assert_eq!(
696 index_urls(locations.simple_indexes()),
697 [
698 "https://first.example.com/simple",
699 "https://explicit.example.com/simple",
700 "https://default.example.com/simple",
701 ]
702 );
703 assert_eq!(
704 index_urls(locations.implicit_indexes()),
705 ["https://first.example.com/simple"]
706 );
707 assert_eq!(
708 index_urls(locations.explicit_indexes()),
709 ["https://explicit.example.com/simple"]
710 );
711 assert_eq!(
712 index_urls(locations.indexes()),
713 [
714 "https://first.example.com/simple",
715 "https://default.example.com/simple",
716 ]
717 );
718 assert_eq!(
719 index_urls(locations.defined_indexes()),
720 [
721 "https://first.example.com/simple",
722 "https://explicit.example.com/simple",
723 "https://default.example.com/simple",
724 ]
725 );
726
727 Ok(())
728 }
729
730 #[test]
731 fn unnamed_indexes_are_not_deduplicated() -> Result<(), Box<dyn Error>> {
732 let first = Index::from_str("https://first.example.com/simple")?;
733 let repeated = Index::from_str("https://first.example.com/simple")?;
734 let last = Index::from_str("https://last.example.com/simple")?;
735 let locations = IndexLocations::new(vec![first, repeated, last], vec![], false);
736 let expected = [
737 "https://first.example.com/simple",
738 "https://first.example.com/simple",
739 "https://last.example.com/simple",
740 ];
741
742 assert_eq!(index_urls(locations.simple_indexes()), expected);
743 assert_eq!(index_urls(locations.implicit_indexes()), expected);
744 assert_eq!(index_urls(locations.defined_indexes()), expected);
745 assert_eq!(
746 index_urls(locations.fetch_indexes()),
747 [
748 "https://first.example.com/simple",
749 "https://last.example.com/simple",
750 "https://pypi.org/simple",
751 ]
752 );
753
754 Ok(())
755 }
756
757 #[test]
758 fn shadowed_default_falls_back_to_pypi() -> Result<(), Box<dyn Error>> {
759 let first = Index::from_str("shared=https://first.example.com/simple")?;
760 let mut shadowed = Index::from_str("shared=https://shadowed.example.com/simple")?;
761 shadowed.default = true;
762 let locations = IndexLocations::new(vec![first, shadowed], vec![], false);
763
764 assert_eq!(
765 index_urls(locations.default_index()),
766 ["https://pypi.org/simple"]
767 );
768 assert_eq!(
769 index_urls(locations.indexes()),
770 [
771 "https://first.example.com/simple",
772 "https://pypi.org/simple",
773 ]
774 );
775
776 Ok(())
777 }
778
779 #[test]
780 fn fetch_indexes_deduplicates_raw_urls() {
781 let url = IndexUrl::from_str("https://index.example.com/simple").unwrap();
782 let mut first = Index::from(url.clone());
783 first.name = Some(IndexName::from_str("first").unwrap());
784 let mut second = Index::from(url);
785 second.name = Some(IndexName::from_str("second").unwrap());
786 second.default = true;
787 let locations = IndexLocations::new(vec![first, second], Vec::new(), false);
788
789 assert_eq!(locations.indexes().count(), 2);
790 assert_eq!(locations.fetch_indexes().count(), 1);
791 }
792
793 #[test]
794 fn test_cache_control_lookup() {
795 use std::str::FromStr;
796
797 use crate::IndexFormat;
798 use crate::index_name::IndexName;
799
800 let indexes = vec![
801 Index {
802 name: Some(IndexName::from_str("index1").unwrap()),
803 url: IndexUrl::from_str("https://index1.example.com/simple").unwrap(),
804 cache_control: Some(crate::IndexCacheControl {
805 api: Some(HeaderValue::from_static("max-age=300")),
806 files: Some(HeaderValue::from_static("max-age=1800")),
807 }),
808 explicit: false,
809 default: false,
810 origin: None,
811 format: IndexFormat::Simple,
812 publish_url: None,
813 authenticate: uv_auth::AuthPolicy::default(),
814 ignore_error_codes: None,
815 hash_algorithm: None,
816 exclude_newer: None,
817 },
818 Index {
819 name: Some(IndexName::from_str("index2").unwrap()),
820 url: IndexUrl::from_str("https://index2.example.com/simple").unwrap(),
821 cache_control: None,
822 explicit: false,
823 default: false,
824 origin: None,
825 format: IndexFormat::Simple,
826 publish_url: None,
827 authenticate: uv_auth::AuthPolicy::default(),
828 ignore_error_codes: None,
829 hash_algorithm: None,
830 exclude_newer: None,
831 },
832 ];
833
834 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
835
836 let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap();
837 assert_eq!(
838 index_locations.simple_api_cache_control_for(&url1),
839 Some(HeaderValue::from_static("max-age=300"))
840 );
841 assert_eq!(
842 index_locations.artifact_cache_control_for(&url1),
843 Some(HeaderValue::from_static("max-age=1800"))
844 );
845
846 let url2 = IndexUrl::from_str("https://index2.example.com/simple").unwrap();
847 assert_eq!(index_locations.simple_api_cache_control_for(&url2), None);
848 assert_eq!(index_locations.artifact_cache_control_for(&url2), None);
849
850 let url3 = IndexUrl::from_str("https://index3.example.com/simple").unwrap();
851 assert_eq!(index_locations.simple_api_cache_control_for(&url3), None);
852 assert_eq!(index_locations.artifact_cache_control_for(&url3), None);
853 }
854
855 #[test]
856 fn test_pytorch_default_cache_control() {
857 let indexes = vec![Index {
859 name: Some(IndexName::from_str("pytorch").unwrap()),
860 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
861 cache_control: None, explicit: false,
863 default: false,
864 origin: None,
865 format: IndexFormat::Simple,
866 publish_url: None,
867 authenticate: uv_auth::AuthPolicy::default(),
868 ignore_error_codes: None,
869 hash_algorithm: None,
870 exclude_newer: None,
871 }];
872
873 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
874
875 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
876
877 assert_eq!(
878 index_locations.simple_api_cache_control_for(&pytorch_url),
879 None
880 );
881 assert_eq!(
882 index_locations.artifact_cache_control_for(&pytorch_url),
883 Some(HeaderValue::from_static(
884 "max-age=365000000, immutable, public",
885 ))
886 );
887 }
888
889 #[test]
890 fn test_pytorch_user_override_cache_control() {
891 let indexes = vec![Index {
893 name: Some(IndexName::from_str("pytorch").unwrap()),
894 url: IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(),
895 cache_control: Some(IndexCacheControl {
896 api: Some(HeaderValue::from_static("no-cache")),
897 files: Some(HeaderValue::from_static("max-age=3600")),
898 }),
899 explicit: false,
900 default: false,
901 origin: None,
902 format: IndexFormat::Simple,
903 publish_url: None,
904 authenticate: uv_auth::AuthPolicy::default(),
905 ignore_error_codes: None,
906 hash_algorithm: None,
907 exclude_newer: None,
908 }];
909
910 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
911
912 let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap();
913
914 assert_eq!(
915 index_locations.simple_api_cache_control_for(&pytorch_url),
916 Some(HeaderValue::from_static("no-cache"))
917 );
918 assert_eq!(
919 index_locations.artifact_cache_control_for(&pytorch_url),
920 Some(HeaderValue::from_static("max-age=3600"))
921 );
922 }
923
924 #[test]
925 fn test_nvidia_default_cache_control() {
926 let indexes = vec![Index {
928 name: Some(IndexName::from_str("nvidia").unwrap()),
929 url: IndexUrl::from_str("https://pypi.nvidia.com").unwrap(),
930 cache_control: None, explicit: false,
932 default: false,
933 origin: None,
934 format: IndexFormat::Simple,
935 publish_url: None,
936 authenticate: uv_auth::AuthPolicy::default(),
937 ignore_error_codes: None,
938 hash_algorithm: None,
939 exclude_newer: None,
940 }];
941
942 let index_locations = IndexLocations::new(indexes, Vec::new(), false);
943
944 let nvidia_url = IndexUrl::from_str("https://pypi.nvidia.com").unwrap();
945
946 assert_eq!(
947 index_locations.simple_api_cache_control_for(&nvidia_url),
948 None
949 );
950 assert_eq!(
951 index_locations.artifact_cache_control_for(&nvidia_url),
952 Some(HeaderValue::from_static(
953 "max-age=365000000, immutable, public",
954 ))
955 );
956 }
957}