thirdpass_core/extension/
process.rs1use anyhow::{format_err, Context, Result};
2
3use super::common;
4
5#[derive(serde::Serialize, serde::Deserialize)]
7pub struct StaticData {
8 pub name: String,
10 pub registry_host_names: Vec<String>,
12 #[serde(default)]
14 pub review_target_policy: common::ReviewTargetPolicy,
15}
16
17#[derive(Debug, Clone)]
23pub struct ProcessExtension {
24 process_path_: std::path::PathBuf,
25 name_: String,
26 registry_host_names_: Vec<String>,
27 review_target_policy_: common::ReviewTargetPolicy,
28}
29
30impl common::FromProcess for ProcessExtension {
31 fn from_process(
32 process_path: &std::path::Path,
33 extension_config_path: &std::path::Path,
34 ) -> Result<Self>
35 where
36 Self: Sized,
37 {
38 let static_data = refresh_static_data(process_path, extension_config_path)?;
39
40 Ok(ProcessExtension {
41 process_path_: process_path.to_path_buf(),
42 name_: static_data.name,
43 registry_host_names_: static_data.registry_host_names,
44 review_target_policy_: static_data.review_target_policy,
45 })
46 }
47}
48
49impl common::Extension for ProcessExtension {
50 fn name(&self) -> String {
51 self.name_.clone()
52 }
53
54 fn registries(&self) -> Vec<String> {
55 self.registry_host_names_.clone()
56 }
57
58 fn review_target_policy(&self) -> common::ReviewTargetPolicy {
59 self.review_target_policy_.clone()
60 }
61
62 fn identify_package_dependencies(
66 &self,
67 package_name: &str,
68 package_version: &Option<&str>,
69 extension_args: &[String],
70 ) -> Result<Vec<common::PackageDependencies>> {
71 let mut args = vec![
72 super::commands::identify_package_dependencies::COMMAND_NAME,
73 "--package-name",
74 package_name,
75 ];
76 if let Some(package_version) = package_version {
77 args.push("--package-version");
78 args.push(package_version);
79 }
80 for extension_arg in extension_args {
81 args.push("--extension-args");
82 args.push(extension_arg);
83 }
84 let output: Box<Vec<common::PackageDependencies>> =
85 run_process(&self.process_path_, &args)?;
86 Ok(*output)
87 }
88
89 fn identify_file_defined_dependencies(
91 &self,
92 working_directory: &std::path::Path,
93 extension_args: &[String],
94 ) -> Result<Vec<common::FileDefinedDependencies>> {
95 let working_directory = working_directory.to_str().ok_or(format_err!(
96 "Failed to parse path into string: {}",
97 working_directory.display()
98 ))?;
99 let mut args = vec![
100 super::commands::identify_file_defined_dependencies::COMMAND_NAME,
101 "--working-directory",
102 working_directory,
103 ];
104 for extension_arg in extension_args {
105 args.push("--extension-args");
106 args.push(extension_arg);
107 }
108 let output: Box<Vec<common::FileDefinedDependencies>> =
109 run_process(&self.process_path_, &args)?;
110 Ok(*output)
111 }
112
113 fn registries_package_metadata(
115 &self,
116 package_name: &str,
117 package_version: &Option<&str>,
118 ) -> Result<Vec<common::RegistryPackageMetadata>> {
119 let mut args = vec![
120 super::commands::registries_package_metadata::COMMAND_NAME,
121 package_name,
122 ];
123 if let Some(package_version) = package_version {
124 args.push(*package_version);
125 }
126
127 let output: Box<Vec<common::RegistryPackageMetadata>> =
128 run_process(&self.process_path_, &args)?;
129 Ok(*output)
130 }
131}
132
133fn refresh_static_data(
134 process_path: &std::path::Path,
135 extension_config_path: &std::path::Path,
136) -> Result<StaticData> {
137 let static_data: Box<StaticData> = run_process(process_path, &["static-data"])?;
138 let static_data = *static_data;
139
140 if let Some(parent) = extension_config_path.parent() {
141 std::fs::create_dir_all(parent).context(format!(
142 "Can't create extension config directory: {}",
143 parent.display()
144 ))?;
145 }
146
147 let file = std::fs::OpenOptions::new()
148 .write(true)
149 .create(true)
150 .truncate(true)
151 .open(extension_config_path)
152 .context(format!(
153 "Can't open/create file for writing: {}",
154 extension_config_path.display()
155 ))?;
156 let writer = std::io::BufWriter::new(file);
157 serde_yaml::to_writer(writer, &static_data)?;
158
159 Ok(static_data)
160}
161
162#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
164pub struct ProcessResult<T> {
165 pub ok: Option<T>,
167 pub err: Option<String>,
169}
170
171fn run_process<T>(process_path: &std::path::Path, args: &[&str]) -> Result<Box<T>>
172where
173 for<'de> T: serde::Deserialize<'de>,
174{
175 log::debug!(
176 "Executing extensions process call with arguments\n{:?}",
177 args
178 );
179 let process = process_path.to_str().ok_or(format_err!(
180 "Failed to parse string from process path: {}",
181 process_path.display()
182 ))?;
183 let handle = std::process::Command::new(process)
184 .args(args)
185 .stdin(std::process::Stdio::null())
186 .stderr(std::process::Stdio::piped())
187 .stdout(std::process::Stdio::piped())
188 .output()?;
189
190 let stdout = String::from_utf8_lossy(&handle.stdout);
191 let process_result: ProcessResult<T> = serde_json::from_str(&stdout)?;
192
193 if let Some(result) = process_result.ok {
194 Ok(Box::new(result))
195 } else if let Some(result) = process_result.err {
196 Err(format_err!(result))
197 } else {
198 Err(format_err!("Failed to find ok or err result from process."))
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::extension::common::{Extension, FromProcess, ReviewTargetPolicy};
206 use serde_json::json;
207
208 #[test]
209 fn static_data_serializes_review_target_policy() {
210 let data = StaticData {
211 name: "rs".to_string(),
212 registry_host_names: vec!["crates.io".to_string()],
213 review_target_policy: ReviewTargetPolicy {
214 excluded_exact_paths: vec!["Cargo.lock".to_string()],
215 },
216 };
217
218 let serialized = serde_json::to_value(&data).expect("failed to serialize static data");
219
220 assert_eq!(
221 serialized,
222 json!({
223 "name": "rs",
224 "registry_host_names": ["crates.io"],
225 "review_target_policy": {
226 "excluded_exact_paths": ["Cargo.lock"]
227 }
228 })
229 );
230 }
231
232 #[test]
233 fn static_data_defaults_empty_review_target_policy() {
234 let data: StaticData = serde_json::from_value(json!({
235 "name": "rs",
236 "registry_host_names": ["crates.io"]
237 }))
238 .expect("failed to deserialize static data");
239
240 assert_eq!(data.review_target_policy, ReviewTargetPolicy::default());
241 }
242
243 #[cfg(unix)]
244 #[test]
245 fn process_extension_refreshes_cached_static_data() {
246 let tmp = tempfile::tempdir().expect("failed to create tempdir");
247 let config_path = tmp.path().join("config").join("thirdpass-rs.yaml");
248 let process_path = tmp.path().join("thirdpass-rs");
249 let stale_data = StaticData {
250 name: "stale".to_string(),
251 registry_host_names: vec!["stale.example".to_string()],
252 review_target_policy: ReviewTargetPolicy::default(),
253 };
254 std::fs::create_dir_all(config_path.parent().expect("config path has parent"))
255 .expect("failed to create config directory");
256 std::fs::write(
257 &config_path,
258 serde_yaml::to_string(&stale_data).expect("failed to serialize stale static data"),
259 )
260 .expect("failed to write stale static data");
261 std::fs::write(
262 &process_path,
263 "#!/bin/sh\nprintf '%s\\n' '{\"ok\":{\"name\":\"rs\",\"registry_host_names\":[\"crates.io\"],\"review_target_policy\":{\"excluded_exact_paths\":[\"Cargo.lock\"]}}}'\n",
264 )
265 .expect("failed to write process extension fixture");
266
267 use std::os::unix::fs::PermissionsExt;
268 let permissions = std::fs::Permissions::from_mode(0o755);
269 std::fs::set_permissions(&process_path, permissions)
270 .expect("failed to make process extension executable");
271
272 let extension = ProcessExtension::from_process(&process_path, &config_path)
273 .expect("failed to load extension");
274 let cached_data_file =
275 std::fs::File::open(&config_path).expect("failed to open refreshed static data cache");
276 let cached_data: StaticData = serde_yaml::from_reader(cached_data_file)
277 .expect("failed to read refreshed static data cache");
278
279 let expected_policy = ReviewTargetPolicy {
280 excluded_exact_paths: vec!["Cargo.lock".to_string()],
281 };
282
283 assert_eq!(extension.name(), "rs");
284 assert_eq!(extension.registries(), vec!["crates.io"]);
285 assert_eq!(extension.review_target_policy(), expected_policy);
286 assert_eq!(cached_data.name, "rs");
287 assert_eq!(cached_data.registry_host_names, vec!["crates.io"]);
288 assert_eq!(cached_data.review_target_policy, expected_policy);
289 }
290}