diff --git a/tools/yabridgectl/Cargo.lock b/tools/yabridgectl/Cargo.lock
index 399922ea..21ef7c95 100644
--- a/tools/yabridgectl/Cargo.lock
+++ b/tools/yabridgectl/Cargo.lock
@@ -232,6 +232,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -395,6 +401,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "ryu"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f"
+
[[package]]
name = "same-file"
version = "1.0.6"
@@ -427,6 +439,17 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_jsonrc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b591e90bcce7185aa4f8c775c504456586ae0f7df49a4087a1ee4179d402b8a8"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
[[package]]
name = "strsim"
version = "0.10.0"
@@ -628,6 +651,7 @@ dependencies = [
"reflink",
"serde",
"serde_derive",
+ "serde_jsonrc",
"textwrap 0.11.0",
"toml",
"walkdir",
diff --git a/tools/yabridgectl/Cargo.toml b/tools/yabridgectl/Cargo.toml
index 80ae8f7f..af301b0c 100644
--- a/tools/yabridgectl/Cargo.toml
+++ b/tools/yabridgectl/Cargo.toml
@@ -22,6 +22,7 @@ reflink = { git = "https://github.com/nicokoch/reflink", rev = "e8d93b465f5d9ad3
rayon = "1.5.1"
serde = "1.0.133"
serde_derive = "1.0.133"
+serde_jsonrc = "0.1"
# NOTE: textwrap 0.12.0 up to at least 0.13.4 apply the subsequent indent after
# wrapping
textwrap = { version = "0.11.0", features = ["term_size"] }
diff --git a/tools/yabridgectl/src/main.rs b/tools/yabridgectl/src/main.rs
index 8d160952..06d919ed 100644
--- a/tools/yabridgectl/src/main.rs
+++ b/tools/yabridgectl/src/main.rs
@@ -27,6 +27,7 @@ mod actions;
mod config;
mod files;
mod utils;
+mod vst3_moduleinfo;
fn main() -> Result<()> {
// We'll modify our `PATH` environment variable so it matches up with
diff --git a/tools/yabridgectl/src/vst3_moduleinfo.rs b/tools/yabridgectl/src/vst3_moduleinfo.rs
new file mode 100644
index 00000000..95251eb5
--- /dev/null
+++ b/tools/yabridgectl/src/vst3_moduleinfo.rs
@@ -0,0 +1,122 @@
+// 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::{Context, Result};
+use serde_derive::{Deserialize, Serialize};
+use std::fmt::Write;
+
+/// Part of the VST3 `moduleinfo.json` file:
+///
+///
+/// Since we only need this to rewrite the UIDs, all the other fields are stored in this `other`
+/// map. And while this is technically supposed to be JSON5, `serde_jsonrc` can also parse trailing
+/// commands and comments and works a lot better.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ModuleInfo {
+ #[serde(rename = "Classes")]
+ classes: Vec,
+ #[serde(rename = "Compatibility")]
+ compatibility_mappings: Vec,
+ #[serde(flatten)]
+ other: serde_jsonrc::Map,
+}
+
+/// A single class object, we only care about the CID since we need to rewrite those.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct Class {
+ #[serde(rename = "CID")]
+ cid: String,
+ #[serde(flatten)]
+ other: serde_jsonrc::Map,
+}
+
+/// A mapping from old class IDs to new class IDs.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct CompatibilityMapping {
+ #[serde(rename = "New")]
+ new: String,
+ #[serde(rename = "Old")]
+ old: Vec,
+ // This will probably stay empty, but let's add it just in case the format changes.
+ #[serde(flatten)]
+ other: serde_jsonrc::Map,
+}
+
+impl ModuleInfo {
+ /// Rewrite the module info in place to switch between COM-style class ID byte orders and the
+ /// other style used on Linux and macOS. This is needed for cross platform plugin compatibility,
+ /// because someone at Steinberg was a genius.
+ pub fn rewrite_uid_byte_orders(&mut self) -> Result<()> {
+ for class in &mut self.classes {
+ class.cid = encode_hex_uid(&rewrite_uid_byte_order(&decode_hex_uid(&class.cid)?));
+ }
+
+ for mapping in &mut self.compatibility_mappings {
+ mapping.new = encode_hex_uid(&rewrite_uid_byte_order(&decode_hex_uid(&mapping.new)?));
+ for cid in &mut mapping.old {
+ *cid = encode_hex_uid(&rewrite_uid_byte_order(&decode_hex_uid(cid)?))
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Parse a hexadecimal UID from a string. Returns an error if the parsing failed.
+fn decode_hex_uid(hex_uid: &str) -> Result<[u8; 16]> {
+ if hex_uid.len() != 32 {
+ anyhow::bail!("Incorrect UID hex string length: {hex_uid:?}");
+ }
+
+ // `u8::from_str_radix` only works with str slices, and there's no way to iterate over strings
+ // in str slices, so iterating over indices and manually slicing is the only solution ehre
+ let mut uid = [0; 16];
+ for (idx, uid_byte) in uid.iter_mut().enumerate() {
+ let start_idx = idx * 2;
+ let end_idx = start_idx + 2;
+ *uid_byte = u8::from_str_radix(&hex_uid[start_idx..end_idx], 16)
+ .with_context(|| format!("Invalid hexadecimal string: {hex_uid:?}"))?;
+ }
+
+ Ok(uid)
+}
+
+/// Format a UID stored in a byte array as a 16 character hexadecimal string.
+fn encode_hex_uid(uid: &[u8; 16]) -> String {
+ let mut hex_uid = String::with_capacity(uid.len() * 2);
+ for b in uid {
+ write!(&mut hex_uid, "{:02X}", b).unwrap();
+ }
+
+ hex_uid
+}
+
+/// Switch between the COM and non-COM byte orders for a UID.
+fn rewrite_uid_byte_order(old_uid: &[u8; 16]) -> [u8; 16] {
+ let mut new_uid = *old_uid;
+
+ new_uid[0] = old_uid[3];
+ new_uid[1] = old_uid[2];
+ new_uid[2] = old_uid[1];
+ new_uid[3] = old_uid[0];
+
+ new_uid[4] = old_uid[5];
+ new_uid[5] = old_uid[4];
+ new_uid[6] = old_uid[7];
+ new_uid[7] = old_uid[6];
+
+ new_uid
+}