use std::ffi::{CString, c_char, c_void};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use truce_core::export::PluginExport;
use truce_core::state::{DeserializedState, deserialize_state, serialize_state};
use truce_params::Params;
use crate::Lv2Instance;
use crate::urid::Urid;
pub(crate) const LV2_STATE__INTERFACE_URI: &str = "http://lv2plug.in/ns/ext/state#interface";
const TRUCE_STATE_KEY_URI: &str = "urn:truce:state-blob";
const LV2_STATE_SUCCESS: u32 = 0;
const _LV2_STATE_ERR_UNKNOWN: u32 = 1;
const LV2_STATE_IS_POD: u32 = 1 << 0;
const LV2_STATE_IS_PORTABLE: u32 = 1 << 1;
type StoreFn = unsafe extern "C" fn(
handle: *mut c_void,
key: Urid,
value: *const c_void,
size: usize,
type_: Urid,
flags: u32,
) -> u32;
type RetrieveFn = unsafe extern "C" fn(
handle: *mut c_void,
key: Urid,
size: *mut usize,
type_: *mut Urid,
flags: *mut u32,
) -> *const c_void;
#[repr(C)]
pub struct Lv2StateInterface {
pub save: unsafe extern "C" fn(
instance: *mut c_void,
store: StoreFn,
handle: *mut c_void,
flags: u32,
features: *const *const crate::types::LV2Feature,
) -> u32,
pub restore: unsafe extern "C" fn(
instance: *mut c_void,
retrieve: RetrieveFn,
handle: *mut c_void,
flags: u32,
features: *const *const crate::types::LV2Feature,
) -> u32,
}
pub(crate) fn state_interface<P: PluginExport>() -> &'static Lv2StateInterface {
struct Holder<P>(std::marker::PhantomData<P>);
impl<P: PluginExport> Holder<P> {
const IFACE: Lv2StateInterface = Lv2StateInterface {
save: save_cb::<P>,
restore: restore_cb::<P>,
};
}
&<Holder<P>>::IFACE
}
fn decode_base64_envelope(slice: &[u8], plugin_id_hash: u64) -> Option<DeserializedState> {
let cleaned: Vec<u8> = slice
.iter()
.copied()
.filter(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'='))
.collect();
let bytes = BASE64.decode(&cleaned).ok()?;
deserialize_state(&bytes, plugin_id_hash)
}
unsafe extern "C" fn save_cb<P: PluginExport>(
instance: *mut c_void,
store: StoreFn,
handle: *mut c_void,
_flags: u32,
_features: *const *const crate::types::LV2Feature,
) -> u32 {
unsafe {
if instance.is_null() {
return 0;
}
let inst = &mut *instance.cast::<Lv2Instance<P>>();
let (ids, values) = inst.plugin.params().collect_values();
let extra = inst.plugin.save_state();
let blob = serialize_state(inst.plugin_id_hash, &ids, &values, &extra);
let key = inst.urid_map.intern(TRUCE_STATE_KEY_URI);
let chunk_urid = inst.urid_map.atom_chunk;
if key == 0 || chunk_urid == 0 {
return 0;
}
let flags = LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE;
let _ = store(
handle,
key,
blob.as_ptr().cast::<c_void>(),
blob.len(),
chunk_urid,
flags,
);
LV2_STATE_SUCCESS
}
}
unsafe extern "C" fn restore_cb<P: PluginExport>(
instance: *mut c_void,
retrieve: RetrieveFn,
handle: *mut c_void,
_flags: u32,
_features: *const *const crate::types::LV2Feature,
) -> u32 {
unsafe {
if instance.is_null() {
return 0;
}
let inst = &mut *instance.cast::<Lv2Instance<P>>();
let key = inst.urid_map.intern(TRUCE_STATE_KEY_URI);
if key == 0 {
return 0;
}
let mut size = 0usize;
let mut type_: Urid = 0;
let mut state_flags: u32 = 0;
let data = retrieve(
handle,
key,
&raw mut size,
&raw mut type_,
&raw mut state_flags,
);
if data.is_null() || size == 0 {
return 0;
}
let slice = core::slice::from_raw_parts(data.cast::<u8>(), size);
let state = deserialize_state(slice, inst.plugin_id_hash)
.or_else(|| decode_base64_envelope(slice, inst.plugin_id_hash));
if let Some(state) = state {
inst.plugin.params().restore_values(&state.params);
inst.plugin.params().snap_smoothers();
if let Some(extra) = state.extra
&& let Err(e) = inst.plugin.load_state(&extra)
{
eprintln!("truce: lv2 load_state failed: {e}");
}
}
LV2_STATE_SUCCESS
}
}
const _: Option<CString> = None;
const _: Option<*const c_char> = None;
#[cfg(test)]
mod tests {
use base64::Engine as _;
use super::{BASE64, decode_base64_envelope};
use truce_core::state::serialize_state;
#[test]
fn base64_text_envelope_round_trips() {
let hash = 0x1234_5678_9abc_def0_u64;
let ids = [1_u32, 2, 3];
let values = [0.25_f64, -6.0, 8000.0];
let blob = serialize_state(hash, &ids, &values, &[]);
let mut text = BASE64.encode(&blob).into_bytes();
text.push(0);
let state = decode_base64_envelope(&text, hash).expect("base64 fallback should decode");
assert_eq!(state.params, vec![(1, 0.25), (2, -6.0), (3, 8000.0)]);
assert!(state.extra.is_none());
}
#[test]
fn base64_fallback_rejects_foreign_plugin_hash() {
let hash = 0xAAAA_BBBB_CCCC_DDDD_u64;
let blob = serialize_state(hash, &[7_u32], &[1.0_f64], &[]);
let text = BASE64.encode(&blob).into_bytes();
assert!(decode_base64_envelope(&text, hash ^ 1).is_none());
}
#[test]
fn base64_fallback_rejects_garbage() {
assert!(decode_base64_envelope(b"not base64 !!!", 0).is_none());
assert!(decode_base64_envelope(&[], 0).is_none());
}
}