Skip to main content

plugin_host/
scanner.rs

1use crate::descriptor::{PluginDescriptor, PluginFormat};
2use std::path::{Path, PathBuf};
3
4/// Resultado de escanear un archivo de plugin.
5#[derive(Debug)]
6pub enum ScanResult {
7    Ok(Vec<PluginDescriptor>),
8    /// El archivo existe pero no es un plugin válido
9    Invalid(String),
10    /// Error al abrir la librería
11    Error(String),
12}
13
14/// Escaneador de plugins del sistema.
15///
16/// Busca en las rutas estándar del OS y en rutas personalizadas.
17/// El escaneo es pesado (abre librerías) — se hace en background
18/// y los resultados se cachean en el `PluginRegistry`.
19pub struct PluginScanner {
20    /// Rutas adicionales definidas por el usuario
21    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    /// Rutas estándar donde buscar plugins CLAP según el OS.
34    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    /// Rutas estándar donde buscar plugins VST3.
70    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    /// Escanea todas las rutas estándar + extras para un formato dado.
103    /// Devuelve un iterator de (path, ScanResult).
104    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    /// Encuentra todos los archivos de plugin en un directorio (recursivo).
129    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                // VST3 son bundles (carpetas .vst3)
139                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    /// Escanea un archivo de plugin específico y extrae sus descriptores.
153    ///
154    /// En producción esto abre la librería y llama a su factory.
155    /// Aquí retornamos una implementación stub que será completada
156    /// por ClapBridge y Vst3Bridge.
157    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        // El hash del archivo permite detectar si el plugin cambió
163        // y necesita re-escanearse
164        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    /// Hash rápido del archivo para detectar cambios (FNV-1a sobre los primeros 64KB).
173    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}