1use crate::descriptor::{PluginDescriptor, PluginFormat};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug)]
6pub enum ScanResult {
7 Ok(Vec<PluginDescriptor>),
8 Invalid(String),
10 Error(String),
12}
13
14pub struct PluginScanner {
20 extra_paths: Vec<PathBuf>,
22}
23
24impl PluginScanner {
25 pub fn new() -> Self {
26 Self { extra_paths: Vec::new() }
27 }
28
29 pub fn add_path(&mut self, path: impl Into<PathBuf>) {
30 self.extra_paths.push(path.into());
31 }
32
33 pub fn clap_search_paths() -> Vec<PathBuf> {
35 let mut paths = Vec::new();
36
37 #[cfg(target_os = "linux")]
38 {
39 paths.push(PathBuf::from("/usr/lib/clap"));
40 paths.push(PathBuf::from("/usr/local/lib/clap"));
41 if let Ok(home) = std::env::var("HOME") {
42 paths.push(PathBuf::from(format!("{}/.clap", home)));
43 }
44 }
45
46 #[cfg(target_os = "macos")]
47 {
48 paths.push(PathBuf::from("/Library/Audio/Plug-Ins/CLAP"));
49 if let Ok(home) = std::env::var("HOME") {
50 paths.push(PathBuf::from(format!(
51 "{}/Library/Audio/Plug-Ins/CLAP", home
52 )));
53 }
54 }
55
56 #[cfg(target_os = "windows")]
57 {
58 if let Ok(program_files) = std::env::var("COMMONPROGRAMFILES") {
59 paths.push(PathBuf::from(format!("{}\\CLAP", program_files)));
60 }
61 if let Ok(local) = std::env::var("LOCALAPPDATA") {
62 paths.push(PathBuf::from(format!("{}\\Programs\\Common\\CLAP", local)));
63 }
64 }
65
66 paths
67 }
68
69 pub fn vst3_search_paths() -> Vec<PathBuf> {
71 let mut paths = Vec::new();
72
73 #[cfg(target_os = "linux")]
74 {
75 paths.push(PathBuf::from("/usr/lib/vst3"));
76 paths.push(PathBuf::from("/usr/local/lib/vst3"));
77 if let Ok(home) = std::env::var("HOME") {
78 paths.push(PathBuf::from(format!("{}/.vst3", home)));
79 }
80 }
81
82 #[cfg(target_os = "macos")]
83 {
84 paths.push(PathBuf::from("/Library/Audio/Plug-Ins/VST3"));
85 if let Ok(home) = std::env::var("HOME") {
86 paths.push(PathBuf::from(format!(
87 "{}/Library/Audio/Plug-Ins/VST3", home
88 )));
89 }
90 }
91
92 #[cfg(target_os = "windows")]
93 {
94 if let Ok(program_files) = std::env::var("PROGRAMFILES") {
95 paths.push(PathBuf::from(format!("{}\\Common Files\\VST3", program_files)));
96 }
97 }
98
99 paths
100 }
101
102 pub fn scan_format<'a>(
105 &'a self,
106 format: PluginFormat,
107 ) -> impl Iterator<Item = (PathBuf, ScanResult)> + 'a {
108 let standard = match format {
109 PluginFormat::Clap => Self::clap_search_paths(),
110 PluginFormat::Vst3 => Self::vst3_search_paths(),
111 };
112
113 let all_paths: Vec<PathBuf> = standard
114 .into_iter()
115 .chain(self.extra_paths.iter().cloned())
116 .collect();
117
118 all_paths.into_iter().flat_map(move |dir| {
119 Self::find_plugins_in_dir(&dir, format)
120 .into_iter()
121 .map(move |path| {
122 let result = Self::scan_file(&path, format);
123 (path, result)
124 })
125 })
126 }
127
128 pub fn find_plugins_in_dir(dir: &Path, format: PluginFormat) -> Vec<PathBuf> {
130 let ext = format.extension();
131 let mut found = Vec::new();
132
133 let Ok(entries) = std::fs::read_dir(dir) else { return found; };
134
135 for entry in entries.flatten() {
136 let path = entry.path();
137 if path.is_dir() {
138 if path.extension().map_or(false, |e| e == ext) {
140 found.push(path);
141 } else {
142 found.extend(Self::find_plugins_in_dir(&path, format));
143 }
144 } else if path.extension().map_or(false, |e| e == ext) {
145 found.push(path);
146 }
147 }
148
149 found
150 }
151
152 pub fn scan_file(path: &Path, format: PluginFormat) -> ScanResult {
158 if !path.exists() {
159 return ScanResult::Error(format!("Path does not exist: {}", path.display()));
160 }
161
162 let file_hash = Self::hash_file(path).unwrap_or(0);
165
166 match format {
167 PluginFormat::Clap => crate::bridge::clap::scan_clap_file(path, file_hash),
168 PluginFormat::Vst3 => crate::bridge::vst3::scan_vst3_file(path, file_hash),
169 }
170 }
171
172 fn hash_file(path: &Path) -> std::io::Result<u64> {
174 use std::io::Read;
175 let mut file = std::fs::File::open(path)?;
176 let mut buf = [0u8; 65536];
177 let n = file.read(&mut buf)?;
178
179 let mut hash: u64 = 0xcbf29ce484222325;
180 for &byte in &buf[..n] {
181 hash ^= byte as u64;
182 hash = hash.wrapping_mul(0x100000001b3);
183 }
184 Ok(hash)
185 }
186}
187
188impl Default for PluginScanner {
189 fn default() -> Self {
190 Self::new()
191 }
192}