Skip to main content

maolan_engine/plugins/
mod.rs

1pub mod clap_proc;
2pub mod ipc;
3#[cfg(all(unix, not(target_os = "macos")))]
4pub mod lv2_proc;
5pub mod types;
6pub mod vst3_proc;
7
8pub use types::*;
9
10use serde::de::DeserializeOwned;
11
12#[derive(serde::Deserialize)]
13struct ScanDiagnostic {
14    message: String,
15    plugin_uri: Option<String>,
16    plugin_name: Option<String>,
17    bundle_uri: Option<String>,
18}
19
20#[derive(serde::Deserialize)]
21struct ScanOutput<T> {
22    data: T,
23    errors: Vec<ScanDiagnostic>,
24    warnings: Vec<ScanDiagnostic>,
25}
26
27use crate::message::PluginKind;
28
29pub fn resolve_plugin_identifier(kind: PluginKind, identifier: &str) -> Result<String, String> {
30    if identifier.is_empty() {
31        return Err("plugin identifier is empty".to_string());
32    }
33    if identifier.contains('/')
34        || identifier.contains('\\')
35        || identifier.contains("::")
36        || identifier.contains('#')
37        || identifier.contains("://")
38        || identifier.starts_with("file:")
39        || std::path::Path::new(identifier).exists()
40    {
41        return Ok(identifier.to_string());
42    }
43
44    match kind {
45        PluginKind::Clap => {
46            let plugins = scan_plugins::<ClapPluginInfo>("clap")
47                .map_err(|e| format!("failed to scan CLAP plugins: {e}"))?;
48            plugins
49                .into_iter()
50                .find(|p| !p.id.is_empty() && p.id == identifier)
51                .map(|p| p.path)
52                .ok_or_else(|| format!("CLAP plugin ID not found: {identifier}"))
53        }
54        PluginKind::Vst3 => {
55            let plugins = scan_plugins::<Vst3PluginInfo>("vst3")
56                .map_err(|e| format!("failed to scan VST3 plugins: {e}"))?;
57            plugins
58                .into_iter()
59                .find(|p| !p.id.is_empty() && p.id == identifier)
60                .map(|p| p.path)
61                .ok_or_else(|| format!("VST3 plugin ID not found: {identifier}"))
62        }
63        #[cfg(all(unix, not(target_os = "macos")))]
64        PluginKind::Lv2 => {
65            let plugins = scan_plugins::<Lv2PluginInfo>("lv2")
66                .map_err(|e| format!("failed to scan LV2 plugins: {e}"))?;
67            plugins
68                .into_iter()
69                .find(|p| p.uri == identifier)
70                .map(|p| p.uri)
71                .ok_or_else(|| format!("LV2 plugin URI not found: {identifier}"))
72        }
73    }
74}
75
76pub fn scan_plugins<T: DeserializeOwned>(format: &str) -> Result<Vec<T>, String> {
77    let host_bin = ipc::find_plugin_host_binary().ok_or("maolan-plugin-host binary not found")?;
78
79    let mut cmd = std::process::Command::new(&host_bin);
80    cmd.arg("--scan")
81        .arg("--format")
82        .arg(format)
83        .arg("--path")
84        .arg("--system");
85    ipc::append_parent_log_level(&mut cmd);
86
87    let output = cmd
88        .output()
89        .map_err(|e| format!("failed to spawn plugin-host scanner: {e}"))?;
90
91    if !output.status.success() {
92        let stderr = String::from_utf8_lossy(&output.stderr);
93        return Err(format!(
94            "plugin-host scanner exited with code {:?}: {stderr}",
95            output.status.code()
96        ));
97    }
98
99    let json = String::from_utf8_lossy(&output.stdout);
100    let parsed: ScanOutput<Vec<T>> =
101        serde_json::from_str(&json).map_err(|e| format!("failed to parse scan JSON: {e}"))?;
102
103    for error in &parsed.errors {
104        tracing::error!(
105            message = %error.message,
106            plugin_uri = ?error.plugin_uri,
107            plugin_name = ?error.plugin_name,
108            bundle_uri = ?error.bundle_uri,
109            "plugin scan error"
110        );
111    }
112    for warning in &parsed.warnings {
113        tracing::warn!(
114            message = %warning.message,
115            plugin_uri = ?warning.plugin_uri,
116            plugin_name = ?warning.plugin_name,
117            bundle_uri = ?warning.bundle_uri,
118            "plugin scan warning"
119        );
120    }
121
122    Ok(parsed.data)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::ScanOutput;
128
129    #[test]
130    fn scan_output_parses_wrapper() {
131        let json = r#"{
132            "errors": [
133                {
134                    "message": "error: failed to open manifest.ttl",
135                    "bundle_uri": "file:///tmp/broken.lv2/"
136                }
137            ],
138            "warnings": [
139                {
140                    "message": "warning: duplicate version",
141                    "plugin_uri": "http://example.com/plugin"
142                }
143            ],
144            "data": [{"name": "Test", "path": "/tmp/test.clap", "capabilities": null}]
145        }"#;
146        let output: ScanOutput<Vec<serde_json::Value>> = serde_json::from_str(json).unwrap();
147        assert_eq!(output.errors.len(), 1);
148        assert_eq!(
149            output.errors[0].message,
150            "error: failed to open manifest.ttl"
151        );
152        assert_eq!(
153            output.errors[0].bundle_uri,
154            Some("file:///tmp/broken.lv2/".to_string())
155        );
156        assert_eq!(output.warnings.len(), 1);
157        assert_eq!(
158            output.warnings[0].plugin_uri,
159            Some("http://example.com/plugin".to_string())
160        );
161        assert_eq!(output.data.len(), 1);
162        assert_eq!(output.data[0]["name"], "Test");
163    }
164}