From 0224ca1e248723586dcdb095c689525232fa201a Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:07:58 +0200 Subject: [PATCH] Reject CLAP port layouts beyond host limits instead of truncating Fixes #435 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/clap-host/src/ffi.rs | 25 ++++++-- crates/clap-host/src/plugin.rs | 17 ++++- crates/clap-host/tests/common/mod.rs | 2 +- crates/clap-host/tests/fixture/src/lib.rs | 78 +++++++++++++++++++++-- crates/clap-host/tests/fixture_e2e.rs | 23 ++++++- crates/engine/tests/clap_e2e.rs | 2 +- 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/crates/clap-host/src/ffi.rs b/crates/clap-host/src/ffi.rs index 82f57bf3..99062807 100644 --- a/crates/clap-host/src/ffi.rs +++ b/crates/clap-host/src/ffi.rs @@ -1241,25 +1241,38 @@ impl Instance { ok.then(|| read_name(&buf)).filter(|s| !s.is_empty()) } - pub fn audio_ports(&self, is_input: bool) -> Vec { + /// The plugin's audio ports in one direction. A layout beyond `MAX_PORTS` ports or + /// `MAX_PORT_CHANNELS` channels per port is an error: we never hand a plugin fewer ports or + /// channels than it advertised. + pub fn audio_ports(&self, is_input: bool) -> Result, ClapError> { // SAFETY: see `params`. - let Some(p) = (unsafe { self.ports.as_ref() }) else { return Vec::new() }; - let (Some(count), Some(get)) = (p.count, p.get) else { return Vec::new() }; + let Some(p) = (unsafe { self.ports.as_ref() }) else { return Ok(Vec::new()) }; + let (Some(count), Some(get)) = (p.count, p.get) else { return Ok(Vec::new()) }; + let dir = if is_input { "input" } else { "output" }; // SAFETY: vtable call with its instance. - let n = unsafe { count(self.plugin, is_input) }.min(MAX_PORTS); + let n = unsafe { count(self.plugin, is_input) }; + if n > MAX_PORTS { + return Err(ClapError::Process(format!("{n} audio {dir} ports (at most {MAX_PORTS} supported)"))); + } let mut out = Vec::new(); for i in 0..n { // SAFETY: plain data; all-zero (null port_type pointer) is valid. let mut info: clap_audio_port_info = unsafe { std::mem::zeroed() }; // SAFETY: index below the count; `info` is writable. if unsafe { get(self.plugin, i, is_input, &mut info) } { - out.push(PortInfo { channels: info.channel_count.min(MAX_PORT_CHANNELS), main: info.flags & CLAP_AUDIO_PORT_IS_MAIN != 0 }); + if info.channel_count > MAX_PORT_CHANNELS { + return Err(ClapError::Process(format!( + "audio {dir} port {i} has {} channels (at most {MAX_PORT_CHANNELS} supported)", + info.channel_count + ))); + } + out.push(PortInfo { channels: info.channel_count, main: info.flags & CLAP_AUDIO_PORT_IS_MAIN != 0 }); } else { // Keep port indices aligned: an unreadable port still needs a (silent) buffer slot. out.push(PortInfo { channels: 0, main: false }); } } - out + Ok(out) } pub fn note_port(&self) -> Option { diff --git a/crates/clap-host/src/plugin.rs b/crates/clap-host/src/plugin.rs index 89f8dbf8..2fbf2726 100644 --- a/crates/clap-host/src/plugin.rs +++ b/crates/clap-host/src/plugin.rs @@ -113,8 +113,21 @@ impl Plugin for ClapPlugin { self.channels = channels.clamp(1, MAX_CHANNELS); self.inst.deactivate(); self.inst.service_callback(); - let ins = self.inst.audio_ports(true); - let outs = self.inst.audio_ports(false); + let ports = self.inst.audio_ports(true).and_then(|ins| Ok((ins, self.inst.audio_ports(false)?))); + let (ins, outs) = match ports { + Ok(p) => p, + Err(e) => { + // Unsupported layout: stay inactive (`process` passes audio through untouched). + log::warn!("{}: {e}", self.info.id); + self.inputs = PortBuffers::new(&[], 0); + self.outputs = PortBuffers::new(&[], 0); + self.in_main = None; + self.out_main = None; + self.max_block = 0; + self.latency = 0; + return; + } + }; self.in_main = pick_main(&ins); self.out_main = pick_main(&outs); self.inputs = PortBuffers::new(&ins.iter().map(|p| p.channels).collect::>(), mb); diff --git a/crates/clap-host/tests/common/mod.rs b/crates/clap-host/tests/common/mod.rs index 2da23955..1318104d 100644 --- a/crates/clap-host/tests/common/mod.rs +++ b/crates/clap-host/tests/common/mod.rs @@ -40,7 +40,7 @@ pub fn fixture_dir() -> &'static Path { std::fs::create_dir_all(dir.join("vendor")).unwrap(); std::fs::copy(&built, dir.join("vendor/Fixture.clap")).unwrap(); } - assert_eq!(soundcraft_clap_host::add_search_dir(&dir), 2); + assert_eq!(soundcraft_clap_host::add_search_dir(&dir), 5); dir }) } diff --git a/crates/clap-host/tests/fixture/src/lib.rs b/crates/clap-host/tests/fixture/src/lib.rs index 85a7999b..b88f434f 100644 --- a/crates/clap-host/tests/fixture/src/lib.rs +++ b/crates/clap-host/tests/fixture/src/lib.rs @@ -6,6 +6,11 @@ //! to the velocity of the held note (0 when none), switching at the event's sample offset. A //! MIDI CC 7 (volume) sets the constant to its value / 127. //! +//! - `org.soundcraft.test.wide16`, `.wide17ports`, `.wide65ch`: effects advertising wide layouts +//! (16 ports of 64 channels, 17 ports of 2 channels, one port of 65 channels per direction). +//! `process` writes 1.0 into every output channel it was given when the host's buffers match the +//! advertised layout, -1.0 when they do not. +//! //! The gain plugin also implements `clap.state` (gain and mode as two little-endian f64s) and a //! floating-only `clap.gui` without a real window: showing it "edits" the gain to 0.25 in the GUI, //! reported as an output parameter event by the next `process`. @@ -36,6 +41,27 @@ unsafe impl Sync for Features {} static GAIN_FEATURES: Features<3> = Features([c"audio-effect".as_ptr(), c"stereo".as_ptr(), std::ptr::null()]); static SYNTH_FEATURES: Features<3> = Features([c"instrument".as_ptr(), c"synthesizer".as_ptr(), std::ptr::null()]); +static WIDE_FEATURES: Features<2> = Features([c"audio-effect".as_ptr(), std::ptr::null()]); + +const fn wide_desc(id: &'static CStr, name: &'static CStr) -> clap_plugin_descriptor { + clap_plugin_descriptor { + clap_version: CLAP_VERSION, + id: id.as_ptr(), + name: name.as_ptr(), + vendor: c"SoundCraft tests".as_ptr(), + url: std::ptr::null(), + manual_url: std::ptr::null(), + support_url: std::ptr::null(), + version: c"1.0.0".as_ptr(), + description: std::ptr::null(), + features: WIDE_FEATURES.0.as_ptr(), + } +} + +static WIDE16_DESC: clap_plugin_descriptor = wide_desc(c"org.soundcraft.test.wide16", c"Fixture Wide 16x64"); +static WIDE17_DESC: clap_plugin_descriptor = wide_desc(c"org.soundcraft.test.wide17ports", c"Fixture Wide 17 ports"); +static WIDE65_DESC: clap_plugin_descriptor = wide_desc(c"org.soundcraft.test.wide65ch", c"Fixture Wide 65 channels"); + static GAIN_DESC: clap_plugin_descriptor = clap_plugin_descriptor { clap_version: CLAP_VERSION, id: c"org.soundcraft.test.gain".as_ptr(), @@ -66,6 +92,8 @@ struct Fx { plugin: clap_plugin, host: *const clap_host, synth: bool, + /// `(ports per direction, channels per port)` for the wide fixtures. + wide: Option<(u32, u32)>, gain: f64, mode: f64, level: f64, @@ -168,6 +196,22 @@ unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) -> let n = pr.frames_count as usize; let ev = unsafe { &*pr.in_events }; let count = unsafe { (ev.size.unwrap())(pr.in_events) }; + if let Some((ports, channels)) = f.wide { + let fits = |bufs: *const clap_audio_buffer, n: u32| { + n == ports && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.channel_count == channels) + }; + let ok = fits(pr.audio_inputs, pr.audio_inputs_count) && fits(pr.audio_outputs, pr.audio_outputs_count); + if pr.audio_outputs_count > 0 { + let out = unsafe { &*pr.audio_outputs }; + for c in 0..out.channel_count as usize { + let o = unsafe { *out.data32.add(c) }; + for i in 0..n { + unsafe { *o.add(i) = if ok { 1.0 } else { -1.0 } }; + } + } + } + return CLAP_PROCESS_CONTINUE; + } if pr.audio_outputs_count != 1 { return CLAP_PROCESS_ERROR; } @@ -324,10 +368,26 @@ static PARAMS: clap_plugin_params = clap_plugin_params { }; unsafe extern "C" fn ports_count(p: *const clap_plugin, is_input: bool) -> u32 { + if let Some((ports, _)) = unsafe { fx(p) }.wide { + return ports; + } if is_input && unsafe { fx(p) }.synth { 0 } else { 1 } } unsafe extern "C" fn ports_get(p: *const clap_plugin, index: u32, is_input: bool, info: *mut clap_audio_port_info) -> bool { + if let Some((ports, channels)) = unsafe { fx(p) }.wide { + if index >= ports { + return false; + } + let info = unsafe { &mut *info }; + info.id = index * 2 + u32::from(is_input); + write_name(&mut info.name, c"wide"); + info.flags = if index == 0 { CLAP_AUDIO_PORT_IS_MAIN } else { 0 }; + info.channel_count = channels; + info.port_type = std::ptr::null(); + info.in_place_pair = clap_sys::id::CLAP_INVALID_ID; + return true; + } if index != 0 || (is_input && unsafe { fx(p) }.synth) { return false; } @@ -498,23 +558,32 @@ unsafe extern "C" fn on_main_thread(_p: *const clap_plugin) {} // ---- factory + entry ---- unsafe extern "C" fn factory_count(_f: *const clap_plugin_factory) -> u32 { - 2 + 5 } unsafe extern "C" fn factory_desc(_f: *const clap_plugin_factory, i: u32) -> *const clap_plugin_descriptor { match i { 0 => &GAIN_DESC, 1 => &SYNTH_DESC, + 2 => &WIDE16_DESC, + 3 => &WIDE17_DESC, + 4 => &WIDE65_DESC, _ => std::ptr::null(), } } unsafe extern "C" fn factory_create(_f: *const clap_plugin_factory, host: *const clap_host, id: *const c_char) -> *const clap_plugin { let id = unsafe { CStr::from_ptr(id) }; - let (desc, synth) = if id == c"org.soundcraft.test.gain" { - (&GAIN_DESC as *const _, false) + let (desc, synth, wide) = if id == c"org.soundcraft.test.gain" { + (&GAIN_DESC as *const _, false, None) } else if id == c"org.soundcraft.test.synth" { - (&SYNTH_DESC as *const _, true) + (&SYNTH_DESC as *const _, true, None) + } else if id == c"org.soundcraft.test.wide16" { + (&WIDE16_DESC as *const _, false, Some((16, 64))) + } else if id == c"org.soundcraft.test.wide17ports" { + (&WIDE17_DESC as *const _, false, Some((17, 2))) + } else if id == c"org.soundcraft.test.wide65ch" { + (&WIDE65_DESC as *const _, false, Some((1, 65))) } else { return std::ptr::null(); }; @@ -535,6 +604,7 @@ unsafe extern "C" fn factory_create(_f: *const clap_plugin_factory, host: *const }, host, synth, + wide, gain: 1.0, mode: 0.0, level: 0.0, diff --git a/crates/clap-host/tests/fixture_e2e.rs b/crates/clap-host/tests/fixture_e2e.rs index a6c28cae..8525d740 100644 --- a/crates/clap-host/tests/fixture_e2e.rs +++ b/crates/clap-host/tests/fixture_e2e.rs @@ -13,7 +13,7 @@ const SYNTH: &str = "clap:org.soundcraft.test.synth"; fn scan_lists_both_fixture_plugins() { let dir = fixture_dir(); let found = soundcraft_clap_host::scan_paths(&[dir.to_path_buf()]); - assert_eq!(found.len(), 2); + assert_eq!(found.len(), 5); let g = found.iter().find(|d| d.id == GAIN).unwrap(); assert_eq!(g.name, "Fixture Gain"); assert_eq!(g.vendor, "SoundCraft tests"); @@ -110,6 +110,27 @@ fn synth_plays_notes_at_their_offsets() { assert!(io[0][19].abs() < 1e-6 && (io[0][20] - 64.0 / 127.0).abs() < 1e-6, "controllers arrive as MIDI at their offset"); } +#[test] +fn layouts_beyond_the_host_limits_are_rejected_not_truncated() { + fixture_dir(); + // Control: exactly at the limits (16 ports of 64 channels) activates and gets its full layout. + let mut p = soundcraft_clap_host::create("clap:org.soundcraft.test.wide16").unwrap(); + p.prepare(44_100.0, 64, 2); + assert_eq!(p.latency(), 3); + let mut io = vec![vec![0.5f32; 64]; 2]; + p.process(&mut io, 64); + assert!(io.iter().flatten().all(|&x| x == 1.0), "16x64 layout delivered as advertised: {:?}", &io[0][..4]); + // 17 ports, or a 65-channel port: never activated, never processed with a cut-down layout. + for id in ["clap:org.soundcraft.test.wide17ports", "clap:org.soundcraft.test.wide65ch"] { + let mut p = soundcraft_clap_host::create(id).unwrap(); + p.prepare(44_100.0, 64, 2); + assert_eq!(p.latency(), 0, "{id}: not activated"); + let mut io = vec![vec![0.5f32; 64]; 2]; + p.process(&mut io, 64); + assert!(io.iter().flatten().all(|&x| x == 0.5), "{id}: the plugin never ran: {:?}", &io[0][..4]); + } +} + #[test] fn many_instances_create_and_drop_cleanly() { fixture_dir(); diff --git a/crates/engine/tests/clap_e2e.rs b/crates/engine/tests/clap_e2e.rs index cd554a9d..96743198 100644 --- a/crates/engine/tests/clap_e2e.rs +++ b/crates/engine/tests/clap_e2e.rs @@ -40,7 +40,7 @@ fn install_fixture_now() { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::copy(target.join("debug").join(lib), dir.join("Fixture.clap")).unwrap(); - assert_eq!(soundcraft_clap_host::add_search_dir(&dir), 2); + assert_eq!(soundcraft_clap_host::add_search_dir(&dir), 5); } #[test]