1use std::fmt::{self, Display, Formatter};
2
3use rustc_hash::FxHashSet;
4use uv_redacted::SafeUrl;
5
6#[derive(
8 Copy,
9 Clone,
10 Debug,
11 Default,
12 Hash,
13 Eq,
14 PartialEq,
15 Ord,
16 PartialOrd,
17 serde::Serialize,
18 serde::Deserialize,
19)]
20#[serde(rename_all = "kebab-case")]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22pub enum AuthPolicy {
23 #[default]
29 Auto,
30 Always,
35 Never,
39}
40
41impl Display for AuthPolicy {
42 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
43 match self {
44 Self::Auto => write!(f, "auto"),
45 Self::Always => write!(f, "always"),
46 Self::Never => write!(f, "never"),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Hash, Eq, PartialEq)]
55pub struct Index {
56 pub url: SafeUrl,
57 pub root_url: SafeUrl,
60 pub auth_policy: AuthPolicy,
61}
62
63impl Index {
64 fn is_prefix_for(&self, url: &SafeUrl) -> bool {
65 if self.root_url.scheme() != url.scheme()
66 || self.root_url.host_str() != url.host_str()
67 || self.root_url.port_or_known_default() != url.port_or_known_default()
68 {
69 return false;
70 }
71
72 is_path_prefix(self.root_url.path(), url.path())
73 }
74}
75
76pub(crate) fn is_path_prefix(prefix: &str, path: &str) -> bool {
81 if prefix == path {
82 return true;
83 }
84
85 let Some(suffix) = path.strip_prefix(prefix) else {
86 return false;
87 };
88
89 prefix.ends_with('/') || suffix.starts_with('/')
90}
91
92#[derive(Debug, Default, Clone, Eq, PartialEq)]
97pub struct Indexes(FxHashSet<Index>);
98
99impl Indexes {
100 pub fn new() -> Self {
101 Self(FxHashSet::default())
102 }
103
104 pub fn from_indexes(urls: impl IntoIterator<Item = Index>) -> Self {
106 let mut index_urls = Self::new();
107 for url in urls {
108 index_urls.0.insert(url);
109 }
110 index_urls
111 }
112
113 pub(crate) fn index_for(&self, url: &SafeUrl) -> Option<&Index> {
115 self.find_prefix_index(url)
116 }
117
118 pub(crate) fn auth_policy_for(&self, url: &SafeUrl) -> AuthPolicy {
120 self.find_prefix_index(url)
121 .map(|index| index.auth_policy)
122 .unwrap_or(AuthPolicy::Auto)
123 }
124
125 fn find_prefix_index(&self, url: &SafeUrl) -> Option<&Index> {
126 self.0.iter().find(|&index| index.is_prefix_for(url))
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn index(root_url: &str, auth_policy: AuthPolicy) -> Index {
135 let root_url = SafeUrl::parse(root_url).unwrap();
136 Index {
137 url: root_url.clone(),
138 root_url,
139 auth_policy,
140 }
141 }
142
143 #[test]
144 fn test_index_path_prefix_requires_segment_boundary() {
145 let index = index("https://example.com/simple", AuthPolicy::Always);
146
147 for url in [
148 "https://example.com/simple",
149 "https://example.com/simple/",
150 "https://example.com/simple/anyio",
151 ] {
152 assert!(
153 index.is_prefix_for(&SafeUrl::parse(url).unwrap()),
154 "Failed to match URL with prefix: {url}"
155 );
156 }
157
158 for url in [
159 "https://example.com/simpleevil",
160 "https://example.com/simple-evil",
161 "https://example.com/simpl",
162 ] {
163 assert!(
164 !index.is_prefix_for(&SafeUrl::parse(url).unwrap()),
165 "Should not match URL with partial path segment: {url}"
166 );
167 }
168 }
169}