Skip to main content

thirdpass_core/extension/
process.rs

1use anyhow::{format_err, Context, Result};
2
3use super::common;
4
5#[derive(serde::Serialize, serde::Deserialize)]
6pub struct StaticData {
7    /// Extension short name.
8    pub name: String,
9    /// Registry hosts supported by the extension.
10    pub registry_host_names: Vec<String>,
11    /// Automatic review target selection policy.
12    #[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    /// Returns a list of dependencies for the given package.
77    ///
78    /// Returns one package dependencies structure per registry.
79    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    /// Returns a list of local package dependencies specification files.
104    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    /// Given a package name and version, queries the remote registry for package metadata.
128    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}