Skip to main content

truce_core/
state.rs

1/// Magic bytes for state identification.
2const STATE_MAGIC: &[u8; 4] = b"OAST";
3const STATE_VERSION: u32 = 1;
4
5/// Serialize plugin state: parameter values + optional extra state.
6pub fn serialize_state(
7    plugin_id_hash: u64,
8    param_ids: &[u32],
9    param_values: &[f64],
10    extra: Option<&[u8]>,
11) -> Vec<u8> {
12    let mut data = Vec::new();
13
14    // Header
15    data.extend_from_slice(STATE_MAGIC);
16    data.extend_from_slice(&STATE_VERSION.to_le_bytes());
17    data.extend_from_slice(&plugin_id_hash.to_le_bytes());
18
19    // Parameter block
20    let count = param_ids.len() as u32;
21    data.extend_from_slice(&count.to_le_bytes());
22    for (id, value) in param_ids.iter().zip(param_values.iter()) {
23        data.extend_from_slice(&id.to_le_bytes());
24        data.extend_from_slice(&value.to_le_bytes());
25    }
26
27    // Extra state block
28    if let Some(extra) = extra {
29        let len = extra.len() as u64;
30        data.extend_from_slice(&len.to_le_bytes());
31        data.extend_from_slice(extra);
32    } else {
33        data.extend_from_slice(&0u64.to_le_bytes());
34    }
35
36    data
37}
38
39/// Deserialized state.
40pub struct DeserializedState {
41    pub params: Vec<(u32, f64)>,
42    pub extra: Option<Vec<u8>>,
43}
44
45/// Deserialize plugin state.
46pub fn deserialize_state(data: &[u8], expected_plugin_id: u64) -> Option<DeserializedState> {
47    if data.len() < 16 {
48        return None;
49    }
50
51    // Check magic
52    if &data[0..4] != STATE_MAGIC {
53        return None;
54    }
55
56    let version = u32::from_le_bytes(data[4..8].try_into().ok()?);
57    if version != STATE_VERSION {
58        return None;
59    }
60
61    let plugin_id = u64::from_le_bytes(data[8..16].try_into().ok()?);
62    if plugin_id != expected_plugin_id {
63        return None;
64    }
65
66    let mut offset = 16;
67
68    // Parameter block
69    if offset + 4 > data.len() {
70        return None;
71    }
72    let count = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
73    offset += 4;
74
75    let mut params = Vec::with_capacity(count);
76    for _ in 0..count {
77        if offset + 12 > data.len() {
78            return None;
79        }
80        let id = u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?);
81        offset += 4;
82        let value = f64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
83        offset += 8;
84        params.push((id, value));
85    }
86
87    // Extra state block
88    if offset + 8 > data.len() {
89        return None;
90    }
91    let extra_len = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?) as usize;
92    offset += 8;
93
94    let extra = if extra_len > 0 {
95        if offset + extra_len > data.len() {
96            return None;
97        }
98        Some(data[offset..offset + extra_len].to_vec())
99    } else {
100        None
101    };
102
103    Some(DeserializedState { params, extra })
104}
105
106/// Compute a simple hash of the plugin ID string for state identification.
107pub fn hash_plugin_id(id: &str) -> u64 {
108    let mut hash: u64 = 0xcbf29ce484222325; // FNV-1a offset basis
109    for byte in id.bytes() {
110        hash ^= byte as u64;
111        hash = hash.wrapping_mul(0x100000001b3); // FNV prime
112    }
113    hash
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn round_trip_state() {
122        let plugin_id = hash_plugin_id("com.test.plugin");
123        let ids = [0u32, 1, 2];
124        let values = [0.5f64, 1.0, -12.0];
125        let extra = b"hello extra state";
126
127        let data = serialize_state(plugin_id, &ids, &values, Some(extra));
128        let state = deserialize_state(&data, plugin_id).unwrap();
129
130        assert_eq!(state.params.len(), 3);
131        assert_eq!(state.params[0], (0, 0.5));
132        assert_eq!(state.params[1], (1, 1.0));
133        assert_eq!(state.params[2], (2, -12.0));
134        assert_eq!(state.extra.unwrap(), b"hello extra state");
135    }
136
137    #[test]
138    fn wrong_plugin_id_fails() {
139        let plugin_id = hash_plugin_id("com.test.plugin");
140        let data = serialize_state(plugin_id, &[], &[], None);
141        assert!(deserialize_state(&data, 12345).is_none());
142    }
143}