Skip to main content

thirdpass_rs_lib/
lib.rs

1use anyhow::{format_err, Context, Result};
2use std::io::Read;
3
4mod cargo;
5
6/// Rust package registry extension backed by crates.io and Cargo metadata.
7#[derive(Clone, Debug)]
8pub struct RsExtension {
9    name_: String,
10    registry_host_names_: Vec<String>,
11}
12
13impl thirdpass_core::extension::FromLib for RsExtension {
14    fn new() -> Self {
15        Self {
16            name_: "rs".to_string(),
17            registry_host_names_: vec![cargo::get_registry_host_name()],
18        }
19    }
20}
21
22impl thirdpass_core::extension::Extension for RsExtension {
23    fn name(&self) -> String {
24        self.name_.clone()
25    }
26
27    fn registries(&self) -> Vec<String> {
28        self.registry_host_names_.clone()
29    }
30
31    fn review_target_policy(&self) -> thirdpass_core::extension::ReviewTargetPolicy {
32        thirdpass_core::extension::ReviewTargetPolicy {
33            excluded_exact_paths: vec![
34                ".cargo_vcs_info.json".to_string(),
35                "Cargo.lock".to_string(),
36            ],
37        }
38    }
39
40    /// Returns resolved dependencies for a crates.io package release.
41    fn identify_package_dependencies(
42        &self,
43        package_name: &str,
44        package_version: &Option<&str>,
45        _extension_args: &[String],
46    ) -> Result<Vec<thirdpass_core::extension::PackageDependencies>> {
47        let package_version = match package_version {
48            Some(version) => version.to_string(),
49            None => get_latest_version(package_name)?
50                .ok_or(format_err!("Failed to find latest package version."))?,
51        };
52        let dependencies = cargo::get_package_dependencies(package_name, &package_version)?;
53        Ok(vec![thirdpass_core::extension::PackageDependencies {
54            package_version: Ok(package_version),
55            registry_host_name: cargo::get_registry_host_name(),
56            dependencies,
57        }])
58    }
59
60    fn identify_file_defined_dependencies(
61        &self,
62        working_directory: &std::path::Path,
63        _extension_args: &[String],
64    ) -> Result<Vec<thirdpass_core::extension::FileDefinedDependencies>> {
65        let dependency_set = match cargo::get_file_defined_dependencies(working_directory)? {
66            Some(dependency_set) => dependency_set,
67            None => return Ok(Vec::new()),
68        };
69
70        Ok(vec![thirdpass_core::extension::FileDefinedDependencies {
71            path: dependency_set.path,
72            registry_host_name: cargo::get_registry_host_name(),
73            dependencies: dependency_set.dependencies,
74        }])
75    }
76
77    fn registries_package_metadata(
78        &self,
79        package_name: &str,
80        package_version: &Option<&str>,
81    ) -> Result<Vec<thirdpass_core::extension::RegistryPackageMetadata>> {
82        let entry_json = get_registry_entry_json(package_name)?;
83        let package_version = match package_version {
84            Some(version) => {
85                if !registry_entry_has_version(&entry_json, version) {
86                    return Err(format_err!(
87                        "Package version not found in crates.io registry: {} {}",
88                        package_name,
89                        version
90                    ));
91                }
92                version.to_string()
93            }
94            None => get_latest_version_from_entry_json(&entry_json)
95                .ok_or(format_err!("Failed to find latest package version."))?,
96        };
97
98        let registry_host_name = self
99            .registries()
100            .first()
101            .ok_or(format_err!(
102                "Code error: vector of registry host names is empty."
103            ))?
104            .clone();
105        let human_url = get_registry_human_url(package_name, &package_version)?;
106        let artifact_url = get_archive_url(package_name, &package_version)?;
107
108        Ok(vec![thirdpass_core::extension::RegistryPackageMetadata {
109            registry_host_name,
110            human_url: human_url.to_string(),
111            artifact_url: artifact_url.to_string(),
112            is_primary: true,
113            package_version,
114        }])
115    }
116}
117
118fn get_latest_version(package_name: &str) -> Result<Option<String>> {
119    let entry_json = get_registry_entry_json(package_name)?;
120    Ok(get_latest_version_from_entry_json(&entry_json))
121}
122
123fn get_latest_version_from_entry_json(registry_entry_json: &serde_json::Value) -> Option<String> {
124    let crate_entry = registry_entry_json.get("crate")?;
125    for field in &["max_stable_version", "max_version", "newest_version"] {
126        if let Some(version) = crate_entry.get(field).and_then(|value| value.as_str()) {
127            if !version.is_empty() {
128                return Some(version.to_string());
129            }
130        }
131    }
132    None
133}
134
135fn registry_entry_has_version(registry_entry_json: &serde_json::Value, version: &str) -> bool {
136    registry_entry_json["versions"]
137        .as_array()
138        .map(|versions| {
139            versions
140                .iter()
141                .any(|entry| entry["num"].as_str() == Some(version))
142        })
143        .unwrap_or_default()
144}
145
146fn get_registry_entry_json(package_name: &str) -> Result<serde_json::Value> {
147    let url = format!("https://crates.io/api/v1/crates/{}", package_name);
148    let client = reqwest::blocking::Client::builder()
149        .user_agent(format!("thirdpass-rs/{}", env!("CARGO_PKG_VERSION")))
150        .build()?;
151    let mut result = client.get(&url).send()?.error_for_status()?;
152    let mut body = String::new();
153    result.read_to_string(&mut body)?;
154
155    serde_json::from_str(&body).context(format!("JSON was not well-formatted:\n{}", body))
156}
157
158fn get_registry_human_url(package_name: &str, package_version: &str) -> Result<url::Url> {
159    Ok(url::Url::parse(&format!(
160        "https://crates.io/crates/{}/{}",
161        package_name, package_version
162    ))?)
163}
164
165fn get_archive_url(package_name: &str, package_version: &str) -> Result<url::Url> {
166    Ok(url::Url::parse(&format!(
167        "https://static.crates.io/crates/{}/{}-{}.crate",
168        package_name, package_name, package_version
169    ))?)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use thirdpass_core::extension::{Extension, FromLib};
176
177    #[test]
178    fn latest_version_prefers_stable_version() {
179        let registry_entry_json = serde_json::json!({
180            "crate": {
181                "max_stable_version": "1.2.3",
182                "max_version": "2.0.0-alpha.1",
183                "newest_version": "2.0.0-alpha.1"
184            }
185        });
186
187        assert_eq!(
188            get_latest_version_from_entry_json(&registry_entry_json),
189            Some("1.2.3".to_string())
190        );
191    }
192
193    #[test]
194    fn registry_entry_version_lookup_matches_version_numbers() {
195        let registry_entry_json = serde_json::json!({
196            "versions": [
197                { "num": "1.0.0" },
198                { "num": "1.1.0" }
199            ]
200        });
201
202        assert!(registry_entry_has_version(&registry_entry_json, "1.1.0"));
203        assert!(!registry_entry_has_version(&registry_entry_json, "1.2.0"));
204    }
205
206    #[test]
207    fn registry_urls_match_crates_io_routes() -> Result<()> {
208        assert_eq!(
209            get_registry_human_url("serde", "1.0.0")?.as_str(),
210            "https://crates.io/crates/serde/1.0.0"
211        );
212        assert_eq!(
213            get_archive_url("serde", "1.0.0")?.as_str(),
214            "https://static.crates.io/crates/serde/serde-1.0.0.crate"
215        );
216        Ok(())
217    }
218
219    #[test]
220    fn review_target_policy_skips_generated_cargo_metadata() {
221        let policy = RsExtension::new().review_target_policy();
222
223        assert!(policy.excludes_exact_path(".cargo_vcs_info.json"));
224        assert!(policy.excludes_exact_path("Cargo.lock"));
225        assert!(!policy.excludes_exact_path("Cargo.toml"));
226    }
227}