1const STATE_MAGIC: &[u8; 4] = b"OAST";
13const STATE_VERSION: u32 = 1;
14
15#[must_use]
20pub fn serialize_state(
21 plugin_id_hash: u64,
22 param_ids: &[u32],
23 param_values: &[f64],
24 extra: &[u8],
25) -> Vec<u8> {
26 let mut data = Vec::new();
27
28 data.extend_from_slice(STATE_MAGIC);
30 data.extend_from_slice(&STATE_VERSION.to_le_bytes());
31 data.extend_from_slice(&plugin_id_hash.to_le_bytes());
32
33 let count = crate::cast::len_u32(param_ids.len());
35 data.extend_from_slice(&count.to_le_bytes());
36 for (id, value) in param_ids.iter().zip(param_values.iter()) {
37 data.extend_from_slice(&id.to_le_bytes());
38 data.extend_from_slice(&value.to_le_bytes());
39 }
40
41 let len = extra.len() as u64;
43 data.extend_from_slice(&len.to_le_bytes());
44 data.extend_from_slice(extra);
45
46 data
47}
48
49pub struct DeserializedState {
51 pub params: Vec<(u32, f64)>,
52 pub extra: Option<Vec<u8>>,
53}
54
55pub enum StateParse {
60 Ok(DeserializedState),
61 NotAnEnvelope,
64 UnknownVersion(u32),
68 WrongPlugin {
73 found: u64,
74 state: DeserializedState,
75 },
76 Corrupt,
79}
80
81#[must_use]
85pub fn deserialize_state(data: &[u8], expected_plugin_id: u64) -> Option<DeserializedState> {
86 match parse_state(data, expected_plugin_id) {
87 StateParse::Ok(state) => Some(state),
88 _ => None,
89 }
90}
91
92#[must_use]
95pub fn parse_state(data: &[u8], expected_plugin_id: u64) -> StateParse {
96 if data.len() < 8 || &data[0..4] != STATE_MAGIC {
97 return StateParse::NotAnEnvelope;
98 }
99
100 let Ok(version_bytes) = data[4..8].try_into() else {
101 return StateParse::NotAnEnvelope;
102 };
103 let version = u32::from_le_bytes(version_bytes);
104 if version != STATE_VERSION {
105 return StateParse::UnknownVersion(version);
106 }
107
108 let Some(id_bytes) = data.get(8..16) else {
111 return StateParse::Corrupt;
112 };
113 let Ok(id_bytes) = id_bytes.try_into() else {
114 return StateParse::Corrupt;
115 };
116 let plugin_id = u64::from_le_bytes(id_bytes);
117
118 let mut offset = 16;
119
120 if offset + 4 > data.len() {
122 return StateParse::Corrupt;
123 }
124 let Ok(count_bytes) = data[offset..offset + 4].try_into() else {
125 return StateParse::Corrupt;
126 };
127 let count = u32::from_le_bytes(count_bytes) as usize;
128 offset += 4;
129
130 let max_count = data.len().saturating_sub(offset) / 12;
138 let mut params = Vec::with_capacity(count.min(max_count));
139 for _ in 0..count {
140 if offset + 12 > data.len() {
141 return StateParse::Corrupt;
142 }
143 let Ok(id_bytes) = data[offset..offset + 4].try_into() else {
144 return StateParse::Corrupt;
145 };
146 let id = u32::from_le_bytes(id_bytes);
147 offset += 4;
148 let Ok(value_bytes) = data[offset..offset + 8].try_into() else {
149 return StateParse::Corrupt;
150 };
151 let value = f64::from_le_bytes(value_bytes);
152 offset += 8;
153 params.push((id, value));
154 }
155
156 if offset + 8 > data.len() {
158 return StateParse::Corrupt;
159 }
160 let Ok(len_bytes) = data[offset..offset + 8].try_into() else {
161 return StateParse::Corrupt;
162 };
163 #[allow(clippy::cast_possible_truncation)]
167 let extra_len = u64::from_le_bytes(len_bytes) as usize;
168 offset += 8;
169
170 let extra = if extra_len > 0 {
171 match offset.checked_add(extra_len) {
176 Some(end) if end <= data.len() => Some(data[offset..end].to_vec()),
177 _ => return StateParse::Corrupt,
178 }
179 } else {
180 None
181 };
182
183 let state = DeserializedState { params, extra };
184 if plugin_id == expected_plugin_id {
185 StateParse::Ok(state)
186 } else {
187 StateParse::WrongPlugin {
188 found: plugin_id,
189 state,
190 }
191 }
192}
193
194#[must_use]
204pub fn hash_plugin_id(id: &str) -> u64 {
205 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in id.bytes() {
207 hash ^= u64::from(byte);
208 hash = hash.wrapping_mul(0x0100_0000_01b3); }
210 hash
211}
212
213#[must_use]
222pub fn vst3_cid(id: &str) -> [u8; 16] {
223 const FNV_OFFSET_BASIS: u128 = 0x6C62_272E_07BB_0142_62B8_2175_6295_C58D;
224 const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013B;
225 let mut hash = FNV_OFFSET_BASIS;
226 for byte in id.bytes() {
227 hash ^= u128::from(byte);
228 hash = hash.wrapping_mul(FNV_PRIME);
229 }
230 hash.to_le_bytes()
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn round_trip_state() {
239 let plugin_id = hash_plugin_id("com.test.plugin");
240 let ids = [0u32, 1, 2];
241 let values = [0.5f64, 1.0, -12.0];
242 let extra = b"hello extra state";
243
244 let data = serialize_state(plugin_id, &ids, &values, extra);
245 let state = deserialize_state(&data, plugin_id).unwrap();
246
247 assert_eq!(state.params.len(), 3);
248 assert_eq!(state.params[0], (0, 0.5));
249 assert_eq!(state.params[1], (1, 1.0));
250 assert_eq!(state.params[2], (2, -12.0));
251 assert_eq!(state.extra.unwrap(), b"hello extra state");
252 }
253
254 #[test]
255 fn wrong_plugin_id_fails() {
256 let plugin_id = hash_plugin_id("com.test.plugin");
257 let data = serialize_state(plugin_id, &[], &[], &[]);
258 assert!(deserialize_state(&data, 12345).is_none());
259 }
260
261 #[test]
262 fn parse_distinguishes_foreign_bytes() {
263 assert!(matches!(
264 parse_state(b"{\"legacy\":true}", 1),
265 StateParse::NotAnEnvelope
266 ));
267 assert!(matches!(parse_state(b"", 1), StateParse::NotAnEnvelope));
268 assert!(matches!(parse_state(b"OAS", 1), StateParse::NotAnEnvelope));
269 }
270
271 #[test]
272 fn parse_distinguishes_future_version() {
273 let mut data = serialize_state(1, &[], &[], &[]);
274 data[4..8].copy_from_slice(&2u32.to_le_bytes());
275 assert!(matches!(
276 parse_state(&data, 1),
277 StateParse::UnknownVersion(2)
278 ));
279 }
280
281 #[test]
282 fn parse_decodes_wrong_plugin_envelope() {
283 let data = serialize_state(99, &[7], &[0.25], b"extra");
284 match parse_state(&data, 1) {
285 StateParse::WrongPlugin { found, state } => {
286 assert_eq!(found, 99);
287 assert_eq!(state.params, vec![(7, 0.25)]);
288 assert_eq!(state.extra.as_deref(), Some(&b"extra"[..]));
289 }
290 _ => panic!("expected WrongPlugin"),
291 }
292 }
293
294 #[test]
295 fn parse_flags_truncated_envelope_as_corrupt() {
296 let data = serialize_state(1, &[7, 8], &[0.25, 0.5], b"extra");
297 assert!(matches!(parse_state(&data[..22], 1), StateParse::Corrupt));
299 }
300}