Reject CLAP port layouts beyond host limits instead of truncating

Fixes #435

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilverandClaude Haiku 5.5 committed 2026-10-15 04:07:58 +02:00
1 parent 3a28a65769
commit 0224ca1e24
6 files changed
+132 -15

No files matched your search

+19 -6
View File
@@ -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<PortInfo> {
/// 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<Vec<PortInfo>, 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<NotePort> {
+15 -2
View File
@@ -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::<Vec<_>>(), mb);
+1 -1
View File
@@ -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
})
}
+74 -4
View File
@@ -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<const N: usize> Sync for Features<N> {}
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,
+22 -1
View File
@@ -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();
+1 -1
View File
@@ -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]