ephemeris_release/adapters/cargo/
registry.rs1use std::ops::Range;
4
5use ephemeris_token::Token;
6use facet::Facet;
7use reqwest::Url;
8
9use crate::PackageRegistry;
10use crate::adapters::registry::{HttpRegistry, RegistryError};
11
12#[derive(Debug)]
25pub struct CargoRegistry {
26 http: HttpRegistry,
27 base: Url,
28 route: Route,
29}
30
31#[derive(Debug, Clone, Copy)]
32enum Route {
33 CratesIo,
34 SparseIndex,
35}
36
37#[derive(Facet)]
38struct IndexEntry {
39 vers: String,
40}
41
42impl CargoRegistry {
43 pub fn new(base: Url, token: Option<&Token>) -> Result<Self, RegistryError> {
53 Self::build(base, token, Route::CratesIo)
54 }
55
56 pub fn sparse_index(base: Url, token: Option<&Token>) -> Result<Self, RegistryError> {
64 Self::build(base, token, Route::SparseIndex)
65 }
66
67 fn build(mut base: Url, token: Option<&Token>, route: Route) -> Result<Self, RegistryError> {
68 if !base.path().ends_with('/') {
69 base.set_path(&format!("{}/", base.path()));
70 }
71 Ok(Self {
72 http: HttpRegistry::new("token", token)?,
73 base,
74 route,
75 })
76 }
77
78 async fn get(&self, path: String) -> Result<Option<String>, RegistryError> {
79 let url = self
80 .base
81 .join(&path)
82 .map_err(|source| RegistryError::InvalidPath { path, source })?;
83 self.http.get(&url).await
84 }
85}
86
87impl PackageRegistry for CargoRegistry {
88 type Error = RegistryError;
89
90 async fn package_exists(&self, package: &str, version: &str) -> Result<bool, Self::Error> {
91 match self.route {
92 Route::CratesIo => Ok(self
93 .get(format!("api/v1/crates/{package}/{version}"))
94 .await?
95 .is_some()),
96 Route::SparseIndex => {
97 let Some(body) = self.get(index_path(package)).await? else {
98 return Ok(false);
99 };
100 for line in body.lines().filter(|line| !line.trim().is_empty()) {
101 let entry: IndexEntry =
102 facet_json::from_str(line).map_err(RegistryError::Body)?;
103 if entry.vers == version {
104 return Ok(true);
105 }
106 }
107 Ok(false)
108 }
109 }
110 }
111
112 fn base_url(&self) -> Option<&Url> {
113 Some(&self.base)
114 }
115}
116
117fn index_path(name: &str) -> String {
118 let name = name.to_ascii_lowercase();
119 let chars = |range: Range<usize>| {
120 name.chars()
121 .skip(range.start)
122 .take(range.len())
123 .collect::<String>()
124 };
125 match name.chars().count() {
126 len @ (0..=2) => format!("{len}/{name}"),
127 3 => format!("3/{}/{name}", chars(0..1)),
128 _ => format!("{}/{}/{name}", chars(0..2), chars(2..4)),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use ephemeris_loopback::{Response, Server};
135 use ephemeris_token::Token;
136 use reqwest::Url;
137
138 use super::{CargoRegistry, RegistryError, index_path};
139 use crate::PackageRegistry as _;
140
141 const INDEX: &str = concat!(
142 "{\"name\":\"core\",\"vers\":\"1.0.0\",\"deps\":[],\"cksum\":\"x\",\"yanked\":false}\n",
143 "{\"name\":\"core\",\"vers\":\"1.1.0\",\"deps\":[],\"cksum\":\"x\",\"yanked\":true}\n",
144 );
145
146 #[tokio::test]
147 async fn lookup_goes_under_a_nested_registry_base_with_the_token() {
148 let server = Server::start(vec![Response::new(200, "{}")]);
149 let base = Url::parse(&format!("{}api/packages/acme/cargo", server.url())).unwrap();
150 let registry = CargoRegistry::new(base, Some(&Token::new("secret"))).unwrap();
151
152 assert!(registry.package_exists("core", "1.2.3").await.unwrap());
153
154 let requests = server.requests();
155 assert_eq!(requests.len(), 1);
156 assert_eq!(
157 requests[0].path,
158 "/api/packages/acme/cargo/api/v1/crates/core/1.2.3"
159 );
160 assert_eq!(requests[0].header("authorization"), Some("token secret"));
161 }
162
163 #[tokio::test]
164 async fn missing_version_is_absent_and_anonymous_without_a_token() {
165 let server = Server::start(vec![Response::new(404, "{}")]);
166 let registry = CargoRegistry::new(server.url(), None).unwrap();
167
168 assert!(!registry.package_exists("core", "1.2.3").await.unwrap());
169
170 let requests = server.requests();
171 assert_eq!(requests[0].path, "/api/v1/crates/core/1.2.3");
172 assert_eq!(requests[0].header("authorization"), None);
173 }
174
175 #[tokio::test]
176 async fn server_errors_are_retried() {
177 let server = Server::start(vec![Response::new(503, "busy"), Response::new(200, "{}")]);
178 let registry = CargoRegistry::new(server.url(), None).unwrap();
179
180 assert!(registry.package_exists("core", "1.2.3").await.unwrap());
181 assert_eq!(server.requests().len(), 2);
182 }
183
184 #[tokio::test]
185 async fn client_errors_carry_status_and_body_without_a_retry() {
186 let server = Server::start(vec![Response::new(400, "bad crate name")]);
187 let registry = CargoRegistry::new(server.url(), None).unwrap();
188
189 let error = registry.package_exists("core", "1.2.3").await.unwrap_err();
190
191 assert!(matches!(
192 &error,
193 RegistryError::Response { status: 400, body } if body == "bad crate name"
194 ));
195 assert_eq!(server.requests().len(), 1);
196 }
197
198 #[test]
199 fn index_path_follows_the_name_length() {
200 assert_eq!(index_path("a"), "1/a");
201 assert_eq!(index_path("ab"), "2/ab");
202 assert_eq!(index_path("abc"), "3/a/abc");
203 assert_eq!(index_path("abcd"), "ab/cd/abcd");
204 assert_eq!(index_path("Ql-oe"), "ql/-o/ql-oe");
205 }
206
207 #[tokio::test]
208 async fn index_lists_a_present_version() {
209 let server = Server::start(vec![Response::new(200, INDEX)]);
210 let base = Url::parse(&format!("{}api/packages/acme/cargo", server.url())).unwrap();
211 let registry = CargoRegistry::sparse_index(base, Some(&Token::new("secret"))).unwrap();
212
213 assert!(registry.package_exists("core", "1.0.0").await.unwrap());
214
215 let requests = server.requests();
216 assert_eq!(requests[0].path, "/api/packages/acme/cargo/co/re/core");
217 assert_eq!(requests[0].header("authorization"), Some("token secret"));
218 }
219
220 #[tokio::test]
221 async fn index_without_the_version_is_absent() {
222 let server = Server::start(vec![Response::new(200, INDEX)]);
223 let registry = CargoRegistry::sparse_index(server.url(), None).unwrap();
224
225 assert!(!registry.package_exists("core", "2.0.0").await.unwrap());
226 }
227
228 #[tokio::test]
229 async fn yanked_index_version_is_present() {
230 let server = Server::start(vec![Response::new(200, INDEX)]);
231 let registry = CargoRegistry::sparse_index(server.url(), None).unwrap();
232
233 assert!(registry.package_exists("core", "1.1.0").await.unwrap());
234 }
235
236 #[tokio::test]
237 async fn missing_index_is_absent() {
238 let server = Server::start(vec![Response::new(404, "")]);
239 let registry = CargoRegistry::sparse_index(server.url(), None).unwrap();
240
241 assert!(!registry.package_exists("core", "1.0.0").await.unwrap());
242 }
243}