Skip to main content

kdeets_lib/
lib.rs

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
22/// Returns `true` if the given version of a crate exists in the crates.io index,
23/// or `false` if the crate is found but the version is absent.
24///
25/// Returns an error if the crate is not found on the index or if the index
26/// cannot be queried.
27///
28/// # Errors
29///
30/// Returns [`Error::CrateNotFoundOnIndex`] when the crate is absent from the index.
31/// Returns other [`Error`] variants on index access failures.
32///
33/// # Examples
34///
35/// ```no_run
36/// use kdeets_lib::version_exists;
37///
38/// # fn main() -> Result<(), kdeets_lib::Error> {
39/// let exists = version_exists("serde", "1.0.0")?;
40/// assert!(exists);
41/// # Ok(())
42/// # }
43/// ```
44pub 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
49/// Returns all published version strings for a crate from the crates.io index.
50///
51/// # Errors
52///
53/// Returns [`Error::CrateNotFoundOnIndex`] when the crate is absent from the index.
54/// Returns other [`Error`] variants on index access failures.
55///
56/// # Examples
57///
58/// ```no_run
59/// use kdeets_lib::list_versions;
60///
61/// # fn main() -> Result<(), kdeets_lib::Error> {
62/// let versions = list_versions("serde")?;
63/// assert!(versions.contains(&"1.0.0".to_string()));
64/// # Ok(())
65/// # }
66/// ```
67pub 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    // Create a certificate store using webpki_roots, which packages
129    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        // Use `ring` as the crypto provider
132        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        // Set the TLS backend. Note that this *requires* that the version of
141        // rustls is the same as the one reqwest is using
142        .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    // Network tests for the public API — exercise the full call chain:
203    // version_exists / list_versions → get_remote_combo_index → _in_index helper
204
205    #[test]
206    fn test_version_exists_crate_not_on_index() {
207        // A crate that does not exist on crates.io causes the sparse index to
208        // return Ok(None), exercising the CrateNotFoundOnIndex branch inside
209        // version_exists_in_index (the `let Some(...) else` path).
210        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    // Local-registry tests for the internal helpers
255
256    #[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(&registry).unwrap();
260        // some_crate 0.2.1 is present in the local test registry
261        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(&registry).unwrap();
270        // 99.99.99 does not exist for some_crate
271        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(&registry).unwrap();
280        // some_crate 0.2.1 is present in the local test registry
281        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(&registry).unwrap();
294        // nonexistent-crate-xyz is not in the local test registry
295        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}