diff --git a/CHANGELOG.md b/CHANGELOG.md index cd90769c..b9b6c0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/tools/yabridgectl/Cargo.lock b/tools/yabridgectl/Cargo.lock index 21ef7c95..b5353d44 100644 --- a/tools/yabridgectl/Cargo.lock +++ b/tools/yabridgectl/Cargo.lock @@ -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", diff --git a/tools/yabridgectl/Cargo.toml b/tools/yabridgectl/Cargo.toml index af301b0c..b9a8f67e 100644 --- a/tools/yabridgectl/Cargo.toml +++ b/tools/yabridgectl/Cargo.toml @@ -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" } diff --git a/tools/yabridgectl/src/config.rs b/tools/yabridgectl/src/config.rs index 9e91c157..ed96859d 100644 --- a/tools/yabridgectl/src/config.rs +++ b/tools/yabridgectl/src/config.rs @@ -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> { let blacklist: HashSet<&Path> = self.blacklist.iter().map(|p| p.as_path()).collect(); diff --git a/tools/yabridgectl/src/files.rs b/tools/yabridgectl/src/files.rs index 392e920f..a5aebcb3 100644 --- a/tools/yabridgectl/src/files.rs +++ b/tools/yabridgectl/src/files.rs @@ -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 { - 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 // `.vst3/Contents//.vst3`, so diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs index 06d919ed..8e7d2b1f 100644 --- a/tools/yabridgectl/src/main.rs +++ b/tools/yabridgectl/src/main.rs @@ -26,6 +26,7 @@ use crate::config::Config; mod actions; mod config; mod files; +mod symbols; mod utils; mod vst3_moduleinfo; diff --git a/tools/yabridgectl/src/symbols.rs b/tools/yabridgectl/src/symbols.rs new file mode 100644 index 00000000..c890962c --- /dev/null +++ b/tools/yabridgectl/src/symbols.rs @@ -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 . + +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, + /// 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>(binary: P) -> Result { + 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>(binary: P) -> Result { + // 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>(binary: P) -> Result { + 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 }) +}