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