// yabridge: a Wine VST bridge // Copyright (C) 2020-2021 Robbert van der Helm // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Functions to index plugins and to set up yabridge for those plugins. use aho_corasick::AhoCorasick; use anyhow::{Context, Result}; use lazy_static::lazy_static; use rayon::prelude::*; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::process::Command; use walkdir::WalkDir; use crate::config::yabridge_vst3_home; use crate::utils::get_file_type; /// Stores the results from searching through a directory. We'll search for Windows VST2 plugin /// `.dll` files, Windows VST3 plugin modules, and native Linux `.so` files inside of a directory. /// These `.so` files are kept track of so we can report the current installation status of VST2 /// plugins and to be able to prune orphan files. Since VST3 plugins have to be instaleld in /// `~/.vst3`, these orphan files are only relevant for VST2 plugins. #[derive(Debug)] pub struct SearchResults { /// Absolute paths to the found VST2 `.dll` files. pub vst2_files: Vec, /// Absolute paths to found VST3 modules. Either legacy `.vst3` DLL files or VST 3.6.10 bundles. pub vst3_modules: Vec, /// `.dll` files skipped over during the search. Used for printing statistics and shown when /// running `yabridgectl sync --verbose`. pub skipped_files: Vec, /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. pub so_files: Vec, } /// The results of the first step of the search process. We'll first index all possibly relevant /// files in a directory before filtering them down to a `SearchResults` object. #[derive(Debug)] pub struct SearchIndex { /// Any `.dll` file. pub dll_files: Vec, /// Any `.vst3` file or directory. This can be either a legacy `.vst3` DLL module or a VST /// 3.6.10 module (or some kind of random other file, of course). pub vst3_files: Vec, /// Absolute paths to any `.so` files inside of the directory, and whether they're a symlink or /// a regular file. pub so_files: Vec, } /// Native `.so` files and VST3 bundle directories we found during a search. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeFile { Symlink(PathBuf), Regular(PathBuf), Directory(PathBuf), } impl NativeFile { /// Return the path of a found `.so` file. pub fn path(&self) -> &Path { match &self { NativeFile::Symlink(path) | NativeFile::Regular(path) | NativeFile::Directory(path) => { path } } } } /// VST3 modules we found during a serach. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Vst3Module { /// The actual VST3 module and its type. pub module: Vst3ModuleType, /// The architecture of the VST3 module. pub architecture: LibArchitecture, /// The VST3 subdirectory the orignal module was in, if any. This is usually used to group /// plugisn by the same manufacturer together. We detect this by looking for a parent `VST3` /// directory. If we can't find that, this will be `None`. pub subdirectory: Option, } /// The type of the VST3 module. VST 3.6.10 style bundles require slightly different handling #[derive(Debug, Clone, PartialEq, Eq)] pub enum Vst3ModuleType { /// Old, pre-VST 3.6.10 style `.vst3` modules. These are simply `.dll` files with a different p /// refix. Even though this is a legacy format, almost all VST3 plugins in the wild still use /// this format. Legacy(PathBuf), /// A VST 3.6.10 bundle, with the same format as the VST3 bundles used on Linux and macOS. These /// kinds of bundles can come with resource files and presets, which should also be symlinked to /// `~/.vst3/` Bundle(PathBuf), } impl Vst3Module { /// Get the path to the Windows VST3 plugin. This can be either a file or a directory depending /// on the type of moudle. pub fn original_path(&self) -> &Path { match &self.module { Vst3ModuleType::Legacy(path) | Vst3ModuleType::Bundle(path) => path, } } /// Get the name of the module as a string. Should be in the format `Plugin Name.vst3`. pub fn original_module_name(&self) -> &str { match &self.module { Vst3ModuleType::Legacy(path) | Vst3ModuleType::Bundle(path) => path .file_name() .unwrap() .to_str() .expect("VST3 module name contains invalid UTF-8"), } } /// Get the path to the actual `.vst3` module file. pub fn original_module_path(&self) -> PathBuf { match &self.module { Vst3ModuleType::Legacy(path) => path.to_owned(), Vst3ModuleType::Bundle(bundle_home) => { let mut path = bundle_home.join("Contents"); path.push(self.architecture.vst_arch()); path.push(self.original_module_name()); path } } } /// If this was a VST 3.6.10 style bundle, then return the path to the `Resources` directory if /// it has one. pub fn original_resources_dir(&self) -> Option { match &self.module { Vst3ModuleType::Bundle(bundle_home) => { let mut path = bundle_home.join("Contents"); path.push("Resources"); if path.exists() { Some(path) } else { None } } Vst3ModuleType::Legacy(_) => None, } } /// Get the path to the bundle in `~/.vst3` corresponding to the bridged version of this module. /// We will try to recreate the original subdirectory structure so plugins are still grouped by /// manufacturer. /// /// FIXME: How do we solve naming clashes from the same VST3 plugin being installed to multiple /// Wine prefixes? pub fn target_bundle_home(&self) -> PathBuf { match &self.subdirectory { Some(directory) => yabridge_vst3_home() .join(directory) .join(self.original_module_name()), None => yabridge_vst3_home().join(self.original_module_name()), } } /// Get the path to the `libyabridge.so` file in `~/.vst3` corresponding to the bridged version /// of this module. pub fn target_native_module_path(&self) -> PathBuf { let native_module_name = match &self.module { Vst3ModuleType::Legacy(path) | Vst3ModuleType::Bundle(path) => path .with_extension("so") .file_name() .unwrap() .to_str() .expect("VST3 module name contains invalid UTF-8") .to_owned(), }; let mut path = self.target_bundle_home(); path.push("Contents"); path.push("x86_64-linux"); path.push(native_module_name); path } /// Get the path to where we'll symlink `original_module_path`. This is part of the merged VST3 /// bundle in `~/.vst3/yabridge`. pub fn target_windows_module_path(&self) -> PathBuf { let mut path = self.target_bundle_home(); path.push("Contents"); path.push(self.architecture.vst_arch()); path.push(self.original_module_name()); path } /// If the Windows VST3 plugin we're bridging was in a VST 3.6.10 style bundle and had a /// resources directory, then we'll symlink that directory to here so the host can access all /// its original resources. pub fn target_resources_dir(&self) -> PathBuf { let mut path = self.target_bundle_home(); path.push("Contents"); path.push("Resources"); path } } /// The architecture of a `.dll` file. Needed so we can create a merged bundle for VST3 plugins. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] pub enum LibArchitecture { Dll32, Dll64, } impl Display for LibArchitecture { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { LibArchitecture::Dll32 => write!(f, "32-bit"), LibArchitecture::Dll64 => write!(f, "64-bit"), } } } impl LibArchitecture { /// Get the corresponding VST3 architecture directory name. See /// https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html. pub fn vst_arch(&self) -> &str { match &self { LibArchitecture::Dll32 => "x86-win", LibArchitecture::Dll64 => "x86_64-win", } } } impl SearchResults { /// For every found VST2 plugin and VST3 module, find the associated copy or symlink of /// `libyabridge-{vst2,vst3}.so`. The returned hashmap will contain a `None` value for plugins /// that have not yet been set up. pub fn installation_status(&self) -> BTreeMap> { let so_files: HashMap<&Path, &NativeFile> = self .so_files .iter() .map(|file| (file.path(), file)) .collect(); // Do this for the VST2 plugins let mut installation_status: BTreeMap> = self .vst2_files .iter() .map( |path| match so_files.get(path.with_extension("so").as_path()) { Some(&file_type) => (path.clone(), Some(file_type.clone())), None => (path.clone(), None), }, ) .collect(); // And for VST3 modules. We have not stored the paths to the corresponding `.so` files yet // because they are not in any of the directories we're indexing. installation_status.extend(self.vst3_modules.iter().map(|module| { ( module.original_path().to_owned(), get_file_type(module.target_native_module_path()), ) })); installation_status } /// Find all `.so` files in the search results that do not belong to a VST2 plugin `.dll` file. /// We cannot yet do the same thing for VST3 plguins because they will all be installed in /// `~/.vst3`. pub fn vst2_orphans(&self) -> Vec<&NativeFile> { // We need to store these in a map so we can easily entries with corresponding `.dll` files let mut orphans: HashMap<&Path, &NativeFile> = self .so_files .iter() .map(|file_type| (file_type.path(), file_type)) .collect(); for vst2_path in &self.vst2_files { orphans.remove(vst2_path.with_extension("so").as_path()); } orphans.values().cloned().collect() } } /// Find all `.dll`, `.vst3` and `.so` files under a directory. These results can be filtered down /// to actual VST2 plugins and VST3 modules using `search()`. pub fn index(directory: &Path) -> SearchIndex { let mut dll_files: Vec = Vec::new(); let mut vst3_files: Vec = Vec::new(); let mut so_files: Vec = Vec::new(); // XXX: We're silently skipping directories and files we don't have permission to read. This // sounds like the expected behavior, but I"m not entirely sure. for (file_idx, entry) in WalkDir::new(directory) .follow_links(true) .into_iter() .filter_map(|e| e.ok()) .filter(|e| !e.file_type().is_dir()) .enumerate() { // This is a bit of an odd warning, but I can see it happening that someone adds their // entire home directory by accident. Removing the home directory would cause yabridgectl to // scan for leftover `.so` files, which would of course take an enternity. This warning will // at least tell the user what's happening and that they can safely cancel the scan. if file_idx == 100_000 { eprintln!( "Indexed over 100.000 files, press Ctrl+C to cancel this operation if this was not \ intentional." ) } match entry.path().extension().and_then(|os| os.to_str()) { Some("dll") => dll_files.push(entry.into_path()), Some("vst3") => vst3_files.push(entry.into_path()), Some("so") => { if entry.path_is_symlink() { so_files.push(NativeFile::Symlink(entry.into_path())); } else { so_files.push(NativeFile::Regular(entry.into_path())); } } _ => (), } } SearchIndex { dll_files, vst3_files, so_files, } } impl SearchIndex { /// Filter these indexing results down to actual VST2 plugins and VST3 modules. This will skip /// all invalid files, such as regular `.dll` libraries. Will return an error if `winedump` /// could not be found. pub fn search(self) -> Result { lazy_static! { static ref VST2_AUTOMATON: AhoCorasick = AhoCorasick::new_auto_configured(&["VSTPluginMain", "main", "main_plugin"]); static ref VST3_AUTOMATON: AhoCorasick = AhoCorasick::new_auto_configured(&["GetPluginFactory"]); static ref DLL32_AUTOMATON: AhoCorasick = AhoCorasick::new_auto_configured(&["Machine: 014C"]); } let winedump = |args: &[&str], path: &Path| { Command::new("winedump") .args(args) .arg(path) .output() .context( "Could not find 'winedump'. In some distributions this is part of a seperate \ Wine tools package.", ) .map(|output| output.stdout) }; let pe32_info = |path: &Path| winedump(&[], path); let exported_functions = |path: &Path| winedump(&["-j", "export"], path); // We'll have to figure out which `.dll` files are VST2 plugins and which should be skipped // by checking whether the file contains one of the VST2 entry point functions. This vector // will contain an `Err(path)` if `path` was not a valid VST2 plugin. let is_vst2_plugin: Vec> = self .dll_files .into_par_iter() .map(|path| { if VST2_AUTOMATON.is_match(exported_functions(&path)?) { Ok(Ok(path)) } else { Ok(Err(path)) } }) .collect::>()?; // We need to do the same thing with VST3 plugins. The added difficulty here is that we have // to figure out of the `.vst3` file is a legacy standalone VST3 module, or part of a VST // 3.6.10 bundle. We also need to know the plugin's architecture because we're going to // create a univeral VST3 bundle. let is_vst3_module: Vec> = self .vst3_files .into_par_iter() .map(|module_path| { let architecture = if DLL32_AUTOMATON.is_match(pe32_info(&module_path)?) { LibArchitecture::Dll32 } else { LibArchitecture::Dll64 }; if VST3_AUTOMATON.is_match(exported_functions(&module_path)?) { // Now we'll have to figure out if the plugin is part of a VST 3.6.10 style // bundle or a legacy `.vst3` DLL file. A WIndows VST3 bundle contains at least // `.vst3/Contents//.vst3`, so // we'll just go up a few directories and then reconstruct that bundle. let module_name = module_path.file_name(); let bundle_root = module_path .parent() .and_then(|arch_dir| arch_dir.parent()) .and_then(|contents_dir| contents_dir.parent()); let module_is_in_bundle = bundle_root .and_then(|bundle_root| bundle_root.parent()) .zip(module_name) .map(|(path, module_name)| { // Now reconstruct the path to the original file again as if it were in // a bundle let mut reconstructed_path = path.join(module_name); reconstructed_path.push("Contents"); reconstructed_path.push(architecture.vst_arch()); reconstructed_path.push(module_name); reconstructed_path.exists() }) .unwrap_or(false); let (module, module_home) = if module_is_in_bundle { ( Vst3ModuleType::Bundle(bundle_root.unwrap().to_owned()), bundle_root.unwrap(), ) } else { ( Vst3ModuleType::Legacy(module_path.clone()), module_path.as_path(), ) }; // We want to recreate the original subdirectory structure, so plugins are still // grouped by manufacturer let vst3_directory = module_home.ancestors().find(|path| { path.file_name() .and_then(|name| name.to_str()) .map(|name| name.to_lowercase().as_str() == "vst3") .unwrap_or(false) }); let subdirectory = match vst3_directory { Some(directory) => module_home .strip_prefix(directory) .ok() // We should of coruse pop the `.vst3` directory .and_then(|suffix| suffix.parent()) .map(|subdirectory| subdirectory.to_owned()), None => None, }; Ok(Ok(Vst3Module { module, architecture, subdirectory, })) } else { Ok(Err(module_path)) } }) .collect::>()?; let mut skipped_files: Vec = Vec::new(); let mut vst2_files: Vec = Vec::new(); for dandidate in is_vst2_plugin { match dandidate { Ok(path) => vst2_files.push(path), Err(path) => skipped_files.push(path), } } let mut vst3_modules: Vec = Vec::new(); for candidate in is_vst3_module { match candidate { Ok(module) => vst3_modules.push(module), Err(path) => skipped_files.push(path), } } Ok(SearchResults { vst2_files, vst3_modules, skipped_files, so_files: self.so_files, }) } }