mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
[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:
@@ -98,6 +98,11 @@ Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
distro packaged version of yabridge.
|
||||
- Added support for the new VST 3.7.5 `moduleinfo.json` format to allow
|
||||
VST3 plugins to replace other VST3 and VST2 plugins with different class IDs.
|
||||
- Yabridgectl no longer depends on **winedump**. It now parses Windows PE32(+)
|
||||
binaries without requiring any external dependencies. Or at least, that's the
|
||||
idea. I've come across at least one binary this new parser can't handle
|
||||
(https://github.com/m4b/goblin/issues/307), so it will still fall back to
|
||||
winedump in some cases.
|
||||
- `yabridgectl status` now shows the locations where bridged VST2 and VST3
|
||||
plugins will be set up.
|
||||
- `yabridgectl sync --prune` now also considers broken symlinks.
|
||||
|
||||
Generated
+38
-11
@@ -2,15 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.57"
|
||||
@@ -198,6 +189,17 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c955ab4e0ad8c843ea653a3d143048b87490d9be56bd7132a435c2407846ac8f"
|
||||
dependencies = [
|
||||
"log",
|
||||
"plain",
|
||||
"scroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
@@ -302,6 +304,12 @@ version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.39"
|
||||
@@ -422,6 +430,26 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scroll"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
|
||||
dependencies = [
|
||||
"scroll_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scroll_derive"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
@@ -640,12 +668,11 @@ dependencies = [
|
||||
name = "yabridgectl"
|
||||
version = "3.8.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"colored",
|
||||
"goblin",
|
||||
"is_executable",
|
||||
"lazy_static",
|
||||
"promptly",
|
||||
"rayon",
|
||||
"reflink",
|
||||
|
||||
@@ -10,12 +10,11 @@ repository = "https://github.com/robbert-vdh/yabridge"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "0.7.18"
|
||||
anyhow = "1.0.52"
|
||||
clap = { version = "3.0.6", features = ["cargo", "env", "wrap_help"] }
|
||||
colored = "2.0.0"
|
||||
is_executable = "1.0.1"
|
||||
lazy_static = "1.4.0"
|
||||
goblin = { version = "0.5", default_features = false, features = ["std", "pe32", "pe64"] }
|
||||
promptly = "0.3.0"
|
||||
# Version 0.1.3 from crates.io assumes a 64-bit toolchain
|
||||
reflink = { git = "https://github.com/nicokoch/reflink", rev = "e8d93b465f5d9ad340cd052b64bbc77b8ee107e2" }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::config::Config;
|
||||
mod actions;
|
||||
mod config;
|
||||
mod files;
|
||||
mod symbols;
|
||||
mod utils;
|
||||
mod vst3_moduleinfo;
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
Reference in New Issue
Block a user