const STATE_MAGIC: &[u8; 4] = b"OAST";
const STATE_VERSION: u32 = 1;
#[must_use]
pub fn serialize_state(
plugin_id_hash: u64,
param_ids: &[u32],
param_values: &[f64],
extra: &[u8],
) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(STATE_MAGIC);
data.extend_from_slice(&STATE_VERSION.to_le_bytes());
data.extend_from_slice(&plugin_id_hash.to_le_bytes());
let count = crate::cast::len_u32(param_ids.len());
data.extend_from_slice(&count.to_le_bytes());
for (id, value) in param_ids.iter().zip(param_values.iter()) {
data.extend_from_slice(&id.to_le_bytes());
data.extend_from_slice(&value.to_le_bytes());
}
let len = extra.len() as u64;
data.extend_from_slice(&len.to_le_bytes());
data.extend_from_slice(extra);
data
}
pub struct DeserializedState {
pub params: Vec<(u32, f64)>,
pub extra: Option<Vec<u8>>,
}
pub enum StateParse {
Ok(DeserializedState),
NotAnEnvelope,
UnknownVersion(u32),
WrongPlugin {
found: u64,
state: DeserializedState,
},
Corrupt,
}
#[must_use]
pub fn deserialize_state(data: &[u8], expected_plugin_id: u64) -> Option<DeserializedState> {
match parse_state(data, expected_plugin_id) {
StateParse::Ok(state) => Some(state),
_ => None,
}
}
#[must_use]
pub fn parse_state(data: &[u8], expected_plugin_id: u64) -> StateParse {
if data.len() < 8 || &data[0..4] != STATE_MAGIC {
return StateParse::NotAnEnvelope;
}
let Ok(version_bytes) = data[4..8].try_into() else {
return StateParse::NotAnEnvelope;
};
let version = u32::from_le_bytes(version_bytes);
if version != STATE_VERSION {
return StateParse::UnknownVersion(version);
}
let Some(id_bytes) = data.get(8..16) else {
return StateParse::Corrupt;
};
let Ok(id_bytes) = id_bytes.try_into() else {
return StateParse::Corrupt;
};
let plugin_id = u64::from_le_bytes(id_bytes);
let mut offset = 16;
if offset + 4 > data.len() {
return StateParse::Corrupt;
}
let Ok(count_bytes) = data[offset..offset + 4].try_into() else {
return StateParse::Corrupt;
};
let count = u32::from_le_bytes(count_bytes) as usize;
offset += 4;
let max_count = data.len().saturating_sub(offset) / 12;
let mut params = Vec::with_capacity(count.min(max_count));
for _ in 0..count {
if offset + 12 > data.len() {
return StateParse::Corrupt;
}
let Ok(id_bytes) = data[offset..offset + 4].try_into() else {
return StateParse::Corrupt;
};
let id = u32::from_le_bytes(id_bytes);
offset += 4;
let Ok(value_bytes) = data[offset..offset + 8].try_into() else {
return StateParse::Corrupt;
};
let value = f64::from_le_bytes(value_bytes);
offset += 8;
params.push((id, value));
}
if offset + 8 > data.len() {
return StateParse::Corrupt;
}
let Ok(len_bytes) = data[offset..offset + 8].try_into() else {
return StateParse::Corrupt;
};
#[allow(clippy::cast_possible_truncation)]
let extra_len = u64::from_le_bytes(len_bytes) as usize;
offset += 8;
let extra = if extra_len > 0 {
match offset.checked_add(extra_len) {
Some(end) if end <= data.len() => Some(data[offset..end].to_vec()),
_ => return StateParse::Corrupt,
}
} else {
None
};
let state = DeserializedState { params, extra };
if plugin_id == expected_plugin_id {
StateParse::Ok(state)
} else {
StateParse::WrongPlugin {
found: plugin_id,
state,
}
}
}
#[must_use]
pub fn hash_plugin_id(id: &str) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in id.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0100_0000_01b3); }
hash
}
#[must_use]
pub fn vst3_cid(id: &str) -> [u8; 16] {
const FNV_OFFSET_BASIS: u128 = 0x6C62_272E_07BB_0142_62B8_2175_6295_C58D;
const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013B;
let mut hash = FNV_OFFSET_BASIS;
for byte in id.bytes() {
hash ^= u128::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash.to_le_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_state() {
let plugin_id = hash_plugin_id("com.test.plugin");
let ids = [0u32, 1, 2];
let values = [0.5f64, 1.0, -12.0];
let extra = b"hello extra state";
let data = serialize_state(plugin_id, &ids, &values, extra);
let state = deserialize_state(&data, plugin_id).unwrap();
assert_eq!(state.params.len(), 3);
assert_eq!(state.params[0], (0, 0.5));
assert_eq!(state.params[1], (1, 1.0));
assert_eq!(state.params[2], (2, -12.0));
assert_eq!(state.extra.unwrap(), b"hello extra state");
}
#[test]
fn wrong_plugin_id_fails() {
let plugin_id = hash_plugin_id("com.test.plugin");
let data = serialize_state(plugin_id, &[], &[], &[]);
assert!(deserialize_state(&data, 12345).is_none());
}
#[test]
fn parse_distinguishes_foreign_bytes() {
assert!(matches!(
parse_state(b"{\"legacy\":true}", 1),
StateParse::NotAnEnvelope
));
assert!(matches!(parse_state(b"", 1), StateParse::NotAnEnvelope));
assert!(matches!(parse_state(b"OAS", 1), StateParse::NotAnEnvelope));
}
#[test]
fn parse_distinguishes_future_version() {
let mut data = serialize_state(1, &[], &[], &[]);
data[4..8].copy_from_slice(&2u32.to_le_bytes());
assert!(matches!(
parse_state(&data, 1),
StateParse::UnknownVersion(2)
));
}
#[test]
fn parse_decodes_wrong_plugin_envelope() {
let data = serialize_state(99, &[7], &[0.25], b"extra");
match parse_state(&data, 1) {
StateParse::WrongPlugin { found, state } => {
assert_eq!(found, 99);
assert_eq!(state.params, vec![(7, 0.25)]);
assert_eq!(state.extra.as_deref(), Some(&b"extra"[..]));
}
_ => panic!("expected WrongPlugin"),
}
}
#[test]
fn parse_flags_truncated_envelope_as_corrupt() {
let data = serialize_state(1, &[7, 8], &[0.25, 0.5], b"extra");
assert!(matches!(parse_state(&data[..22], 1), StateParse::Corrupt));
}
}