[yabridgectl] Parse without winedump when possible

This avoids an external dependency and speeds up the indexing process.
So far I found a single plugin that could not be parsed this way, so the
winedump based method is still there as a backup.
This commit is contained in:
Robbert van der Helm
2022-05-23 12:57:05 +02:00
parent 4a845ec952
commit 1f35081bad
7 changed files with 197 additions and 51 deletions
+1 -2
View File
@@ -297,8 +297,7 @@ impl Config {
})
}
/// Search for VST2 and VST3 plugins in all of the registered plugins directories. This will
/// return an error if `winedump` could not be called.
/// Search for VST2 and VST3 plugins in all of the registered plugins directories.
pub fn search_directories(&self) -> Result<BTreeMap<&Path, SearchResults>> {
let blacklist: HashSet<&Path> = self.blacklist.iter().map(|p| p.as_path()).collect();
+23 -36
View File
@@ -16,17 +16,15 @@
//! 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 anyhow::Result;
use rayon::prelude::*;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
use crate::config::{yabridge_vst2_home, yabridge_vst3_home, Config, YabridgeFiles};
use crate::symbols::parse_pe32_binary;
use crate::utils::get_file_type;
/// Stores the results from searching through a directory. We'll search for Windows VST2 plugin
@@ -511,31 +509,10 @@ pub fn index(directory: &Path, blacklist: &HashSet<&Path>) -> SearchIndex {
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.
/// all invalid files, such as regular `.dll` libraries.
pub fn search(self) -> Result<SearchResults> {
lazy_static! {
static ref VST2_AUTOMATON: AhoCorasick =
AhoCorasick::new_auto_configured(&["VSTPluginMain", "main"]);
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);
const VST2_ENTRY_POINTS: [&str; 2] = ["VSTPluginMain", "main"];
const VST3_ENTRY_POINTS: [&str; 1] = ["GetPluginFactory"];
// 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
@@ -544,13 +521,18 @@ impl SearchIndex {
.dll_files
.into_par_iter()
.map(|(path, subdirectory)| {
let architecture = if DLL32_AUTOMATON.is_match(pe32_info(&path)?) {
LibArchitecture::Lib32
} else {
let info = parse_pe32_binary(&path)?;
let architecture = if info.is_64_bit {
LibArchitecture::Lib64
} else {
LibArchitecture::Lib32
};
if VST2_AUTOMATON.is_match(exported_functions(&path)?) {
if info
.exports
.into_iter()
.any(|symbol| VST2_ENTRY_POINTS.contains(&symbol.as_str()))
{
Ok(Ok(Vst2Plugin {
path,
architecture,
@@ -570,13 +552,18 @@ impl SearchIndex {
.vst3_files
.into_par_iter()
.map(|(module_path, subdirectory)| {
let architecture = if DLL32_AUTOMATON.is_match(pe32_info(&module_path)?) {
LibArchitecture::Lib32
} else {
let info = parse_pe32_binary(&module_path)?;
let architecture = if info.is_64_bit {
LibArchitecture::Lib64
} else {
LibArchitecture::Lib32
};
if VST3_AUTOMATON.is_match(exported_functions(&module_path)?) {
if info
.exports
.into_iter()
.any(|symbol| VST3_ENTRY_POINTS.contains(&symbol.as_str()))
{
// 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
// `<plugin_name>.vst3/Contents/<architecture_string>/<plugin_name>.vst3`, so
+1
View File
@@ -26,6 +26,7 @@ use crate::config::Config;
mod actions;
mod config;
mod files;
mod symbols;
mod utils;
mod vst3_moduleinfo;
+128
View File
@@ -0,0 +1,128 @@
// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2022 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 <https://www.gnu.org/licenses/>.
use anyhow::{anyhow, bail, Context, Result};
use std::io::BufRead;
use std::{path::Path, process::Command};
use crate::utils;
/// Some information parsed from a PE32(+) binary. This is needed for setting up yabridge for
/// Windows plugin libraries.
pub struct Pe32Info {
/// Names of the symbols exported from the binary.
pub exports: Vec<String>,
/// Whether the binary is 64-bit (in technically, whether it's a PE32+ binary instead of PE32).
pub is_64_bit: bool,
}
/// Check whether a PE32(+) binary exports the specified symbol. Used to detect the plugin formats
/// supported by a plugin library. Returns an error if the binary cuuld not be read, either because
/// it's not a PE32+ binary or because goblin could not read it and winedump is not installed. This
/// function will also parse non-native binaries.
pub fn parse_pe32_binary<P: AsRef<Path>>(binary: P) -> Result<Pe32Info> {
parse_pe32_goblin(&binary).or_else(|err| {
parse_pe32_winedump(binary)
.with_context(|| format!("Parsing with goblin also failed: {err}"))
})
}
/// Parse using goblin. For the 700 plugin libraries I tested this only didn't work with one of
/// them: https://github.com/m4b/goblin/issues/307
fn parse_pe32_goblin<P: AsRef<Path>>(binary: P) -> Result<Pe32Info> {
// The original version of this function also supports ELF and Mach architectures, but we don't
// need those things here
let bytes = utils::read(&binary)?;
let obj = goblin::pe::PE::parse(&bytes).with_context(|| {
format!(
"Could not parse '{}' as a PE32(+) binary",
binary.as_ref().display()
)
})?;
Ok(Pe32Info {
exports: obj
.exports
.into_iter()
.filter_map(|export| export.name.map(String::from))
.collect(),
is_64_bit: obj.is_64,
})
}
/// A fallback for if goblin can't parse a file. This is kind of a bruteforce approach and it will
/// be a lot slower, but it should also be very rare that this gets invoked at all.
fn parse_pe32_winedump<P: AsRef<Path>>(binary: P) -> Result<Pe32Info> {
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)
};
// The previous version where we only used winedump used Aho-Corsaick automatons for more
// efficient searching, but since this function should in theory never be called we don't even
// try
let basic_info = winedump(&[], binary.as_ref())?;
let is_64_bit = basic_info
.lines()
.find_map(|line| match line {
Ok(line) => {
// NOTE: This always assumes x86 = 32-bit, and everything else = 64-bit
let machine_type = line.trim_start().strip_prefix("Machine:")?.trim();
if machine_type.starts_with("014C") {
Some(false)
} else {
Some(true)
}
}
Err(_) => None,
})
.ok_or_else(|| anyhow!("Winedump output did not contain a 'Machine:' line"))?;
// And we'll just parse _all_ exported functions. Previously we would only check whether this
// contrained certain entries, but efficiency isn't too important here anyways since this is
// only a fallback.
let exported_functions = winedump(&["-j", "export"], binary.as_ref())?;
let mut found_exports_table = false;
let mut exports = Vec::new();
for line in exported_functions.lines() {
let line = line?;
let line = line.trim();
// The exports table starts after a header line and ends when reacing an empty line
if found_exports_table {
if line.is_empty() {
break;
}
// Each line is in the format ` d34db33f 1 symbol_name`
match line.split_ascii_whitespace().nth(2) {
Some(export) => exports.push(String::from(export)),
None => bail!("Malforced winedump export list line: '{line}'"),
}
} else if line.starts_with("Entry Pt") {
found_exports_table = true;
}
}
Ok(Pe32Info { exports, is_64_bit })
}