diff --git a/src/plugin/configuration.cpp b/src/plugin/configuration.cpp new file mode 100644 index 00000000..a7fd2984 --- /dev/null +++ b/src/plugin/configuration.cpp @@ -0,0 +1,71 @@ +// yabridge: a Wine VST bridge +// Copyright (C) 2020 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 . + +#include "configuration.h" + +#include +#include +#include + +#include "utils.h" + +namespace fs = boost::filesystem; + +Configuration::Configuration() {} + +Configuration::Configuration(const fs::path& config_path, + const fs::path& yabridge_path) + : Configuration() { + // Will throw a `toml::parsing_error` if the file cannot be parsed. Better + // to throw here rather than failing silently since syntax errors would + // otherwise be impossible to spot. + toml::table table = toml::parse_file(config_path.string()); + + const fs::path relative_path = + yabridge_path.lexically_relative(config_path.parent_path()); + for (const auto& [pattern, value] : table) { + // First try to match the glob pattern, allow matching an entire + // directory for ease of use. If none of the patterns in the file match + // the plugin path then everything will be left at the defaults. + if (fnmatch(pattern.c_str(), relative_path.c_str(), + FNM_PATHNAME | FNM_LEADING_DIR) != 0) { + continue; + } + + matched_file = config_path; + matched_pattern = pattern; + + // If the table is missing some fields then they will simply be left at + // their defaults + if (toml::table* config = value.as_table(); config != nullptr) { + group = (*config)["group"].value(); + } + + break; + } +} + +Configuration Configuration::load_for(const fs::path& yabridge_path) { + // First find the closest `yabridge.tmol` file for the plugin, falling back + // to default configuration settings if it doesn't exist + const std::optional config_file = + find_dominating_file("yabridge.toml", yabridge_path); + if (!config_file.has_value()) { + return Configuration(); + } + + return Configuration(config_file.value(), yabridge_path); +} diff --git a/src/plugin/configuration.h b/src/plugin/configuration.h index d9e96ccf..ca88a8f6 100644 --- a/src/plugin/configuration.h +++ b/src/plugin/configuration.h @@ -20,33 +20,81 @@ #include /** - * Starting from the starting file or directory, go up in the directory - * hierarchy until we find a file named `filename`. + * An object that's used to provide plugin-specific configuration. Right now + * this is only used to declare plugin groups. A plugin group is a set of + * plugins that will be hosted in the same process rather than individually so + * they can share resources. Configuration file loading works as follows: * - * @param filename The name of the file we're looking for. This can also be a - * directory name since directories are also files. - * @param starting_from The directory to start searching in. If this is a file, - * then start searching in the directory the file is located in. - * @param predicate The predicate to use to check if the path matches a file. - * Needed as an easy way to limit the search to directories only since C++17 - * does not have any built in coroutines or generators. + * 1. `Configuration::load_for(path)` gets called with a path to the copy of or + * symlink to `libyabridge.so` that the plugin host has tried to load. + * 2. We start looking for a file named `yabridge.toml` in the same directory as + * that `.so` file, iteratively continuing to search one directory higher + * until we either find the file or we reach the filesystem root. + * 3. If the file is found, then parse it as a TOML file and look for the first + * table whose key is a glob pattern that (partially) matches the relative + * path between the found `yabridge.toml` and the `.so` file. As a rule of + * thumb, if the `find -type f` command executed in Bash would list + * the `.so` file, then the following table in `yabridge.tmol` would also + * match the same `.so` file: * - * @return The path to the *file* found, or `std::nullopt` if the file could not - * be found. + * ```toml + * [""] + * group = "..." + * ``` + * 4. If one of these glob patterns could be matched with the relative path of + * the `.so` file then we'll use the settings specified in that section. + * Otherwise the default settings will be used. */ -template -std::optional find_dominating_file( - const std::string& filename, - boost::filesystem::path starting_dir, - F predicate = boost::filesystem::exists) { - while (starting_dir != "") { - const boost::filesystem::path candidate = starting_dir / filename; - if (predicate(candidate)) { - return candidate; - } +class Configuration { + /** + * Create an empty configuration object with default settings. + */ + Configuration(); - starting_dir = starting_dir.parent_path(); - } + /** + * Load the configuration for an instance of yabridge from a configuration + * file by matching the plugin's relative path to the glob patterns in that + * configuration file. Will leave the object empty if the plugin cannot be + * matched to any of the patterns. Not meant to be used directly. + * + * @throw toml::parsing_error If the file could not be parsed. + * + * @see Configuration::load_for + */ + Configuration(const boost::filesystem::path& config_path, + const boost::filesystem::path& yabridge_path); - return std::nullopt; -} + /** + * Load the configuration that belongs to a copy of or symlink to + * `libyabridge.so`. If no configuration file could be found then this will + * return an empty configuration object with default settings. + * + * @param yabridge_path The path to the .so file that's being loaded.by the + * VST host. This will be used both for the starting location of the + * search and to determine which section in the config file to use. + * + * @return Either a configuration object populated with values from matched + * glob pattern within the found configuration file, or an empty + * configuration object if no configuration file could be found or if the + * plugin could not be matched to any of the glob patterns in the + * configuration file. + */ + static Configuration load_for(const boost::filesystem::path& yabridge_path); + + /** + * The name of the plugin group that should be used for the plugin this + * configuration object was created for. If not set, then the plugin should + * be hosted individually instead. + */ + std::optional group; + + /** + * The path to the configuration file that was parsed. + */ + std::optional matched_file; + + /** + * The matched glob pattern in the above configuration file. + */ + std::optional matched_pattern; +}; diff --git a/src/plugin/utils.h b/src/plugin/utils.h index 9978511e..ad481852 100644 --- a/src/plugin/utils.h +++ b/src/plugin/utils.h @@ -141,3 +141,35 @@ std::string get_wine_version(); * using the user's default prefix. */ boost::process::environment set_wineprefix(); + +/** + * Starting from the starting file or directory, go up in the directory + * hierarchy until we find a file named `filename`. + * + * @param filename The name of the file we're looking for. This can also be a + * directory name since directories are also files. + * @param starting_from The directory to start searching in. If this is a file, + * then start searching in the directory the file is located in. + * @param predicate The predicate to use to check if the path matches a file. + * Needed as an easy way to limit the search to directories only since C++17 + * does not have any built in coroutines or generators. + * + * @return The path to the *file* found, or `std::nullopt` if the file could not + * be found. + */ +template +std::optional find_dominating_file( + const std::string& filename, + boost::filesystem::path starting_dir, + F predicate = boost::filesystem::exists) { + while (starting_dir != "") { + const boost::filesystem::path candidate = starting_dir / filename; + if (predicate(candidate)) { + return candidate; + } + + starting_dir = starting_dir.parent_path(); + } + + return std::nullopt; +}