1use anyhow::{format_err, Context, Result};
2use std::io::Read;
3
4mod cargo;
5
6#[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 fn identify_package_dependencies(
42 &self,
43 package_name: &str,
44 package_version: &Option<&str>,
45 _extension_args: &Vec<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::PathBuf,
63 _extension_args: &Vec<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 Ok(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(®istry_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(®istry_entry_json, "1.1.0"));
203 assert!(!registry_entry_has_version(®istry_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}