1const HEADER: &str = "Crate versions for";
2const SETUP_HEADER: &str = "Local registry set up for";
3const LINE_CHAR: char = 'ðŸ¶';
4
5mod combo;
6mod crate_versions;
7mod error;
8mod rust_versions;
9mod setup;
10
11pub use crate_versions::CrateVersions;
12pub use error::Error;
13pub use rust_versions::RustVersions;
14pub use setup::Setup;
15
16pub(crate) use combo::ComboIndex;
17
18use reqwest::blocking::ClientBuilder;
19use tame_index::index::RemoteSparseIndex;
20use tame_index::{IndexLocation, IndexUrl, SparseIndex};
21
22pub fn version_exists(crate_name: &str, version: &str) -> Result<bool, Error> {
45 let index = get_remote_combo_index()?;
46 version_exists_in_index(&index, crate_name, version)
47}
48
49pub fn list_versions(crate_name: &str) -> Result<Vec<String>, Error> {
68 let index = get_remote_combo_index()?;
69 list_versions_in_index(&index, crate_name)
70}
71
72pub(crate) fn version_exists_in_index(
73 index: &ComboIndex,
74 crate_name: &str,
75 version: &str,
76) -> Result<bool, Error> {
77 use tame_index::{KrateName, index::FileLock};
78
79 let lock = FileLock::unlocked();
80 let index_krate = index.krate(KrateName::crates_io(crate_name)?, true, &lock)?;
81
82 let Some(index_krate) = index_krate else {
83 return Err(Error::CrateNotFoundOnIndex);
84 };
85
86 Ok(index_krate
87 .versions
88 .iter()
89 .any(|v| v.version.as_str() == version))
90}
91
92pub(crate) fn list_versions_in_index(
93 index: &ComboIndex,
94 crate_name: &str,
95) -> Result<Vec<String>, Error> {
96 use tame_index::{KrateName, index::FileLock};
97
98 let lock = FileLock::unlocked();
99 let index_krate = index.krate(KrateName::crates_io(crate_name)?, true, &lock)?;
100
101 let Some(index_krate) = index_krate else {
102 return Err(Error::CrateNotFoundOnIndex);
103 };
104
105 Ok(index_krate
106 .versions
107 .iter()
108 .map(|v| v.version.to_string())
109 .collect())
110}
111
112pub(crate) fn get_remote_combo_index() -> Result<ComboIndex, tame_index::error::Error> {
113 let index = get_sparse_index()?;
114 let builder = get_client_builder();
115 let client = builder.build()?;
116
117 let remote_index = RemoteSparseIndex::new(index, client);
118
119 Ok(ComboIndex::from(remote_index))
120}
121
122pub(crate) fn get_sparse_index() -> Result<SparseIndex, tame_index::error::Error> {
123 let il = IndexLocation::new(IndexUrl::CratesIoSparse);
124 SparseIndex::new(il)
125}
126
127pub(crate) fn get_client_builder() -> ClientBuilder {
128 let rcs: rustls::RootCertStore = webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();
130 let client_config = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
131 rustls::crypto::ring::default_provider(),
133 ))
134 .with_protocol_versions(rustls::DEFAULT_VERSIONS)
135 .unwrap()
136 .with_root_certificates(rcs)
137 .with_no_client_auth();
138
139 reqwest::blocking::Client::builder()
140 .tls_backend_preconfigured(client_config)
143}
144
145#[cfg(test)]
146mod tests {
147
148 use std::vec;
149
150 use crate::ComboIndex;
151 use crate::get_remote_combo_index;
152 use tame_index::{PathBuf, index::LocalRegistry};
153 use tempfile::TempDir;
154
155 const TEST_REGISTRY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/registry");
156
157 pub(crate) fn get_temp_local_registry() -> (TempDir, String) {
158 let temp_dir = tempfile::tempdir().unwrap();
159 println!("Temp dir: {}", temp_dir.path().display());
160 let registry_path = temp_dir.path().join("registry");
161 let registry = registry_path.to_str().unwrap();
162
163 let options = fs_extra::dir::CopyOptions::new();
164
165 let from_path = vec![TEST_REGISTRY];
166
167 let _ = fs_extra::copy_items(&from_path, temp_dir.path().to_str().unwrap(), &options);
168 let _ = fs_extra::copy_items(&from_path, "/tmp/test/", &options);
169
170 (temp_dir, registry.to_string())
171 }
172
173 pub(crate) fn get_test_index(registry: &str) -> Result<ComboIndex, tame_index::error::Error> {
174 let local_registry = LocalRegistry::open(PathBuf::from(registry), false)?;
175
176 Ok(ComboIndex::from(local_registry))
177 }
178
179 #[test]
180 fn test_get_sparse_index_success() {
181 let result = get_remote_combo_index();
182 assert!(result.is_ok());
183 let index = result.unwrap();
184 assert!(matches!(index, ComboIndex::Sparse(_)));
185 }
186
187 #[test]
188 fn test_get_sparse_index_type() {
189 let result = get_remote_combo_index();
190 assert!(matches!(result, Ok(ComboIndex::Sparse(_))));
191 }
192
193 #[test]
194 fn test_sparse_index_error_handling() {
195 let result = get_remote_combo_index();
196 match result {
197 Ok(_) => (),
198 Err(e) => panic!("Expected Ok, got Err: {e:?}"),
199 }
200 }
201
202 #[test]
206 fn test_version_exists_crate_not_on_index() {
207 let result = crate::version_exists("kdeets-nonexistent-crate-xyzabc123", "1.0.0");
211 assert!(
212 matches!(result, Err(crate::Error::CrateNotFoundOnIndex)),
213 "Expected CrateNotFoundOnIndex, got {result:?}"
214 );
215 }
216
217 #[test]
218 fn test_list_versions_crate_not_on_index() {
219 let result = crate::list_versions("kdeets-nonexistent-crate-xyzabc123");
220 assert!(
221 matches!(result, Err(crate::Error::CrateNotFoundOnIndex)),
222 "Expected CrateNotFoundOnIndex, got {result:?}"
223 );
224 }
225
226 #[test]
227 fn test_version_exists_real_crate_known_version() {
228 let result = crate::version_exists("serde", "1.0.0");
229 assert!(result.is_ok(), "Expected Ok, got {result:?}");
230 assert!(
231 result.unwrap(),
232 "Expected serde 1.0.0 to exist on crates.io"
233 );
234 }
235
236 #[test]
237 fn test_version_exists_real_crate_nonexistent_version() {
238 let result = crate::version_exists("serde", "99.99.99");
239 assert!(result.is_ok(), "Expected Ok, got {result:?}");
240 assert!(!result.unwrap(), "Expected serde 99.99.99 to not exist");
241 }
242
243 #[test]
244 fn test_list_versions_real_crate() {
245 let result = crate::list_versions("serde");
246 assert!(result.is_ok(), "Expected Ok, got {result:?}");
247 let versions = result.unwrap();
248 assert!(
249 versions.contains(&"1.0.0".to_string()),
250 "Expected serde versions to contain 1.0.0"
251 );
252 }
253
254 #[test]
257 fn test_version_exists_known_version_returns_true() {
258 let (_temp_dir, registry) = get_temp_local_registry();
259 let index = get_test_index(®istry).unwrap();
260 let result = crate::version_exists_in_index(&index, "some_crate", "0.2.1");
262 assert!(result.is_ok(), "Expected Ok, got {result:?}");
263 assert!(result.unwrap(), "Expected version 0.2.1 to exist");
264 }
265
266 #[test]
267 fn test_version_exists_unknown_version_returns_false() {
268 let (_temp_dir, registry) = get_temp_local_registry();
269 let index = get_test_index(®istry).unwrap();
270 let result = crate::version_exists_in_index(&index, "some_crate", "99.99.99");
272 assert!(result.is_ok(), "Expected Ok, got {result:?}");
273 assert!(!result.unwrap(), "Expected version 99.99.99 to not exist");
274 }
275
276 #[test]
277 fn test_list_versions_contains_known_version() {
278 let (_temp_dir, registry) = get_temp_local_registry();
279 let index = get_test_index(®istry).unwrap();
280 let result = crate::list_versions_in_index(&index, "some_crate");
282 assert!(result.is_ok(), "Expected Ok, got {result:?}");
283 let versions = result.unwrap();
284 assert!(
285 versions.contains(&"0.2.1".to_string()),
286 "Expected versions to contain 0.2.1, got {versions:?}"
287 );
288 }
289
290 #[test]
291 fn test_version_exists_nonexistent_crate_returns_error() {
292 let (_temp_dir, registry) = get_temp_local_registry();
293 let index = get_test_index(®istry).unwrap();
294 let result = crate::version_exists_in_index(&index, "nonexistent-crate-xyz", "1.0.0");
296 assert!(
297 result.is_err(),
298 "Expected Err for nonexistent crate, got {result:?}"
299 );
300 }
301}