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 persist: &[u8],
26) -> Vec<u8> {
27 let mut data = Vec::new();
28
29 data.extend_from_slice(STATE_MAGIC);
31 data.extend_from_slice(&STATE_VERSION.to_le_bytes());
32 data.extend_from_slice(&plugin_id_hash.to_le_bytes());
33
34 let count = crate::cast::len_u32(param_ids.len());
36 data.extend_from_slice(&count.to_le_bytes());
37 for (id, value) in param_ids.iter().zip(param_values.iter()) {
38 data.extend_from_slice(&id.to_le_bytes());
39 data.extend_from_slice(&value.to_le_bytes());
40 }
41
42 data.extend_from_slice(&(extra.len() as u64).to_le_bytes());
44 data.extend_from_slice(extra);
45
46 data.extend_from_slice(&(persist.len() as u64).to_le_bytes());
51 data.extend_from_slice(persist);
52
53 data
54}
55
56pub struct DeserializedState {
58 pub params: Vec<(u32, f64)>,
59 pub extra: Option<Vec<u8>>,
60 pub persist: Vec<u8>,
64}
65
66pub enum StateParse {
71 Ok(DeserializedState),
72 NotAnEnvelope,
75 UnknownVersion(u32),
79 WrongPlugin {
84 found: u64,
85 state: DeserializedState,
86 },
87 Corrupt,
90}
91
92#[must_use]
96pub fn deserialize_state(data: &[u8], expected_plugin_id: u64) -> Option<DeserializedState> {
97 match parse_state(data, expected_plugin_id) {
98 StateParse::Ok(state) => Some(state),
99 _ => None,
100 }
101}
102
103#[must_use]
106pub fn parse_state(data: &[u8], expected_plugin_id: u64) -> StateParse {
107 if data.len() < 8 || &data[0..4] != STATE_MAGIC {
108 return StateParse::NotAnEnvelope;
109 }
110
111 let Ok(version_bytes) = data[4..8].try_into() else {
112 return StateParse::NotAnEnvelope;
113 };
114 let version = u32::from_le_bytes(version_bytes);
115 if version != STATE_VERSION {
116 return StateParse::UnknownVersion(version);
117 }
118
119 let Some(id_bytes) = data.get(8..16) else {
122 return StateParse::Corrupt;
123 };
124 let Ok(id_bytes) = id_bytes.try_into() else {
125 return StateParse::Corrupt;
126 };
127 let plugin_id = u64::from_le_bytes(id_bytes);
128
129 let mut offset = 16;
130
131 if offset + 4 > data.len() {
133 return StateParse::Corrupt;
134 }
135 let Ok(count_bytes) = data[offset..offset + 4].try_into() else {
136 return StateParse::Corrupt;
137 };
138 let count = u32::from_le_bytes(count_bytes) as usize;
139 offset += 4;
140
141 let max_count = data.len().saturating_sub(offset) / 12;
149 let mut params = Vec::with_capacity(count.min(max_count));
150 for _ in 0..count {
151 if offset + 12 > data.len() {
152 return StateParse::Corrupt;
153 }
154 let Ok(id_bytes) = data[offset..offset + 4].try_into() else {
155 return StateParse::Corrupt;
156 };
157 let id = u32::from_le_bytes(id_bytes);
158 offset += 4;
159 let Ok(value_bytes) = data[offset..offset + 8].try_into() else {
160 return StateParse::Corrupt;
161 };
162 let value = f64::from_le_bytes(value_bytes);
163 offset += 8;
164 params.push((id, value));
165 }
166
167 if offset + 8 > data.len() {
169 return StateParse::Corrupt;
170 }
171 let Ok(len_bytes) = data[offset..offset + 8].try_into() else {
172 return StateParse::Corrupt;
173 };
174 #[allow(clippy::cast_possible_truncation)]
178 let extra_len = u64::from_le_bytes(len_bytes) as usize;
179 offset += 8;
180
181 let extra = if extra_len > 0 {
182 match offset.checked_add(extra_len) {
187 Some(end) if end <= data.len() => Some(data[offset..end].to_vec()),
188 _ => return StateParse::Corrupt,
189 }
190 } else {
191 None
192 };
193 offset += extra_len;
194
195 let persist = if offset + 8 <= data.len() {
198 let Ok(len_bytes) = data[offset..offset + 8].try_into() else {
199 return StateParse::Corrupt;
200 };
201 #[allow(clippy::cast_possible_truncation)]
202 let persist_len = u64::from_le_bytes(len_bytes) as usize;
203 offset += 8;
204 match offset.checked_add(persist_len) {
205 Some(end) if end <= data.len() => data[offset..end].to_vec(),
206 _ => return StateParse::Corrupt,
207 }
208 } else {
209 Vec::new()
210 };
211
212 let state = DeserializedState {
213 params,
214 extra,
215 persist,
216 };
217 if plugin_id == expected_plugin_id {
218 StateParse::Ok(state)
219 } else {
220 StateParse::WrongPlugin {
221 found: plugin_id,
222 state,
223 }
224 }
225}
226
227#[must_use]
237pub fn hash_plugin_id(id: &str) -> u64 {
238 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in id.bytes() {
240 hash ^= u64::from(byte);
241 hash = hash.wrapping_mul(0x0100_0000_01b3); }
243 hash
244}
245
246#[must_use]
255pub fn vst3_cid(id: &str) -> [u8; 16] {
256 const FNV_OFFSET_BASIS: u128 = 0x6C62_272E_07BB_0142_62B8_2175_6295_C58D;
257 const FNV_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013B;
258 let mut hash = FNV_OFFSET_BASIS;
259 for byte in id.bytes() {
260 hash ^= u128::from(byte);
261 hash = hash.wrapping_mul(FNV_PRIME);
262 }
263 hash.to_le_bytes()
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn round_trip_state() {
272 let plugin_id = hash_plugin_id("com.test.plugin");
273 let ids = [0u32, 1, 2];
274 let values = [0.5f64, 1.0, -12.0];
275 let extra = b"hello extra state";
276
277 let data = serialize_state(plugin_id, &ids, &values, extra, &[]);
278 let state = deserialize_state(&data, plugin_id).unwrap();
279
280 assert_eq!(state.params.len(), 3);
281 assert_eq!(state.params[0], (0, 0.5));
282 assert_eq!(state.params[1], (1, 1.0));
283 assert_eq!(state.params[2], (2, -12.0));
284 assert_eq!(state.extra.unwrap(), b"hello extra state");
285 }
286
287 #[test]
288 fn wrong_plugin_id_fails() {
289 let plugin_id = hash_plugin_id("com.test.plugin");
290 let data = serialize_state(plugin_id, &[], &[], &[], &[]);
291 assert!(deserialize_state(&data, 12345).is_none());
292 }
293
294 #[test]
295 fn parse_distinguishes_foreign_bytes() {
296 assert!(matches!(
297 parse_state(b"{\"legacy\":true}", 1),
298 StateParse::NotAnEnvelope
299 ));
300 assert!(matches!(parse_state(b"", 1), StateParse::NotAnEnvelope));
301 assert!(matches!(parse_state(b"OAS", 1), StateParse::NotAnEnvelope));
302 }
303
304 #[test]
305 fn parse_distinguishes_future_version() {
306 let mut data = serialize_state(1, &[], &[], &[], &[]);
307 data[4..8].copy_from_slice(&2u32.to_le_bytes());
308 assert!(matches!(
309 parse_state(&data, 1),
310 StateParse::UnknownVersion(2)
311 ));
312 }
313
314 #[test]
315 fn parse_decodes_wrong_plugin_envelope() {
316 let data = serialize_state(99, &[7], &[0.25], b"extra", &[]);
317 match parse_state(&data, 1) {
318 StateParse::WrongPlugin { found, state } => {
319 assert_eq!(found, 99);
320 assert_eq!(state.params, vec![(7, 0.25)]);
321 assert_eq!(state.extra.as_deref(), Some(&b"extra"[..]));
322 }
323 _ => panic!("expected WrongPlugin"),
324 }
325 }
326
327 #[test]
328 fn parse_flags_truncated_envelope_as_corrupt() {
329 let data = serialize_state(1, &[7, 8], &[0.25, 0.5], b"extra", &[]);
330 assert!(matches!(parse_state(&data[..22], 1), StateParse::Corrupt));
332 }
333}