Skip to main content

serializer/machine/
intern.rs

1//! String interning for DX-Machine
2//!
3//! Provides zero-copy string deduplication to reduce serialized size by 50-90%
4//! for data with repeated strings (logs, configs, etc.).
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────┐
10//! │ Serialized Format                                       │
11//! ├─────────────────────────────────────────────────────────┤
12//! │ Header (4 bytes) - FLAG_HAS_INTERN set                  │
13//! ├─────────────────────────────────────────────────────────┤
14//! │ String Pool:                                            │
15//! │   - Pool size: u32 (4 bytes)                            │
16//! │   - String count: u32 (4 bytes)                         │
17//! │   - Offsets: [u32; count] (4 * count bytes)             │
18//! │   - Strings: concatenated UTF-8 data                    │
19//! ├─────────────────────────────────────────────────────────┤
20//! │ RKYV Data (strings replaced with pool indices)          │
21//! └─────────────────────────────────────────────────────────┘
22//! ```
23
24use std::collections::HashMap;
25
26/// String interning pool for deduplication
27pub struct InternPool {
28    /// Map from string content to pool index
29    map: HashMap<String, u32>,
30    /// Ordered list of unique strings
31    strings: Vec<String>,
32}
33
34impl InternPool {
35    /// Create a new empty intern pool
36    #[inline]
37    pub fn new() -> Self {
38        Self {
39            map: HashMap::new(),
40            strings: Vec::new(),
41        }
42    }
43
44    /// Create a pool with pre-allocated capacity
45    #[inline]
46    pub fn with_capacity(capacity: usize) -> Self {
47        Self {
48            map: HashMap::with_capacity(capacity),
49            strings: Vec::with_capacity(capacity),
50        }
51    }
52
53    /// Intern a string, returning its pool index
54    ///
55    /// If the string already exists, returns the existing index.
56    /// Otherwise, adds it to the pool and returns the new index.
57    pub fn intern(&mut self, s: &str) -> u32 {
58        if let Some(&idx) = self.map.get(s) {
59            return idx;
60        }
61
62        let idx = self.strings.len() as u32;
63        self.strings.push(s.to_string());
64        self.map.insert(s.to_string(), idx);
65        idx
66    }
67
68    /// Get a string by its pool index
69    #[inline]
70    pub fn get(&self, idx: u32) -> Option<&str> {
71        self.strings.get(idx as usize).map(|s| s.as_str())
72    }
73
74    /// Get the number of unique strings in the pool
75    #[inline]
76    pub fn len(&self) -> usize {
77        self.strings.len()
78    }
79
80    /// Check if the pool is empty
81    #[inline]
82    pub fn is_empty(&self) -> bool {
83        self.strings.is_empty()
84    }
85
86    /// Serialize the pool to bytes
87    ///
88    /// Format:
89    /// - Pool size: u32 (total bytes for pool section)
90    /// - String count: u32
91    /// - Offsets: [u32; count] (byte offset of each string in data section)
92    /// - Data: concatenated UTF-8 strings
93    pub fn serialize(&self) -> Vec<u8> {
94        if self.strings.is_empty() {
95            // Empty pool: just write zeros
96            return vec![0, 0, 0, 0, 0, 0, 0, 0];
97        }
98
99        let count = self.strings.len() as u32;
100        let mut offsets = Vec::with_capacity(self.strings.len());
101        let mut data = Vec::new();
102
103        // Build offsets and concatenate strings
104        for s in &self.strings {
105            offsets.push(data.len() as u32);
106            data.extend_from_slice(s.as_bytes());
107        }
108
109        // Calculate total pool size
110        let pool_size = 8 + (offsets.len() * 4) + data.len();
111
112        let mut bytes = Vec::with_capacity(pool_size);
113
114        // Write pool size
115        bytes.extend_from_slice(&(pool_size as u32).to_le_bytes());
116
117        // Write string count
118        bytes.extend_from_slice(&count.to_le_bytes());
119
120        // Write offsets
121        for offset in offsets {
122            bytes.extend_from_slice(&offset.to_le_bytes());
123        }
124
125        // Write string data
126        bytes.extend_from_slice(&data);
127
128        bytes
129    }
130
131    /// Deserialize a pool from bytes
132    ///
133    /// Returns the pool and the number of bytes consumed.
134    pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize), InternError> {
135        if bytes.len() < 8 {
136            return Err(InternError::BufferTooSmall);
137        }
138
139        // Read pool size
140        let pool_size = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
141
142        if pool_size == 0 {
143            // Empty pool
144            return Ok((Self::new(), 8));
145        }
146
147        if bytes.len() < pool_size {
148            return Err(InternError::BufferTooSmall);
149        }
150
151        // Read string count
152        let count = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
153
154        if count == 0 {
155            return Ok((Self::new(), pool_size));
156        }
157
158        // Read offsets
159        let offsets_start = 8;
160        let offsets_end = offsets_start + (count * 4);
161
162        if bytes.len() < offsets_end {
163            return Err(InternError::BufferTooSmall);
164        }
165
166        let mut offsets = Vec::with_capacity(count);
167        for i in 0..count {
168            let offset_pos = offsets_start + (i * 4);
169            let offset = u32::from_le_bytes([
170                bytes[offset_pos],
171                bytes[offset_pos + 1],
172                bytes[offset_pos + 2],
173                bytes[offset_pos + 3],
174            ]) as usize;
175            offsets.push(offset);
176        }
177
178        // Read string data
179        let data_start = offsets_end;
180        let data = &bytes[data_start..pool_size];
181
182        // Reconstruct strings
183        let mut strings = Vec::with_capacity(count);
184        let mut map = HashMap::with_capacity(count);
185
186        for (idx, &offset) in offsets.iter().enumerate() {
187            let end = if idx + 1 < offsets.len() {
188                offsets[idx + 1]
189            } else {
190                data.len()
191            };
192
193            if offset > data.len() || end > data.len() || offset > end {
194                return Err(InternError::InvalidOffset);
195            }
196
197            let s = std::str::from_utf8(&data[offset..end])
198                .map_err(|_| InternError::InvalidUtf8)?
199                .to_string();
200
201            map.insert(s.clone(), idx as u32);
202            strings.push(s);
203        }
204
205        Ok((Self { map, strings }, pool_size))
206    }
207}
208
209impl Default for InternPool {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215/// Errors that can occur during string interning
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum InternError {
218    /// Buffer too small to contain pool data
219    BufferTooSmall,
220    /// Invalid string offset in pool
221    InvalidOffset,
222    /// Invalid UTF-8 in string data
223    InvalidUtf8,
224}
225
226impl std::fmt::Display for InternError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        match self {
229            Self::BufferTooSmall => write!(f, "Buffer too small to contain intern pool"),
230            Self::InvalidOffset => write!(f, "Invalid string offset in intern pool"),
231            Self::InvalidUtf8 => write!(f, "Invalid UTF-8 in intern pool string data"),
232        }
233    }
234}
235
236impl std::error::Error for InternError {}
237
238/// Serializer with string interning support
239pub struct InterningSerializer {
240    pool: InternPool,
241}
242
243impl InterningSerializer {
244    /// Create a new interning serializer
245    #[inline]
246    pub fn new() -> Self {
247        Self {
248            pool: InternPool::new(),
249        }
250    }
251
252    /// Create with pre-allocated capacity
253    #[inline]
254    pub fn with_capacity(capacity: usize) -> Self {
255        Self {
256            pool: InternPool::with_capacity(capacity),
257        }
258    }
259
260    /// Get a reference to the intern pool
261    #[inline]
262    pub fn pool(&self) -> &InternPool {
263        &self.pool
264    }
265
266    /// Get a mutable reference to the intern pool
267    #[inline]
268    pub fn pool_mut(&mut self) -> &mut InternPool {
269        &mut self.pool
270    }
271
272    /// Intern a string and return its pool index
273    #[inline]
274    pub fn intern(&mut self, s: &str) -> u32 {
275        self.pool.intern(s)
276    }
277
278    /// Serialize the intern pool
279    #[inline]
280    pub fn serialize_pool(&self) -> Vec<u8> {
281        self.pool.serialize()
282    }
283}
284
285impl Default for InterningSerializer {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291/// Deserializer with string interning support
292pub struct InterningDeserializer {
293    pool: InternPool,
294}
295
296impl InterningDeserializer {
297    /// Create a new interning deserializer from serialized bytes
298    pub fn new(bytes: &[u8]) -> Result<(Self, usize), InternError> {
299        let (pool, consumed) = InternPool::deserialize(bytes)?;
300        Ok((Self { pool }, consumed))
301    }
302
303    /// Get a string by its pool index
304    #[inline]
305    pub fn get(&self, idx: u32) -> Option<&str> {
306        self.pool.get(idx)
307    }
308
309    /// Get a reference to the intern pool
310    #[inline]
311    pub fn pool(&self) -> &InternPool {
312        &self.pool
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_intern_pool_basic() {
322        let mut pool = InternPool::new();
323
324        let idx1 = pool.intern("hello");
325        let idx2 = pool.intern("world");
326        let idx3 = pool.intern("hello"); // Duplicate
327
328        assert_eq!(idx1, 0);
329        assert_eq!(idx2, 1);
330        assert_eq!(idx3, 0); // Same as first "hello"
331
332        assert_eq!(pool.get(0), Some("hello"));
333        assert_eq!(pool.get(1), Some("world"));
334        assert_eq!(pool.get(2), None);
335    }
336
337    #[test]
338    fn test_intern_pool_roundtrip() {
339        let mut pool = InternPool::new();
340        pool.intern("foo");
341        pool.intern("bar");
342        pool.intern("baz");
343        pool.intern("foo"); // Duplicate
344
345        let bytes = pool.serialize();
346        let (deserialized, consumed) = InternPool::deserialize(&bytes).unwrap();
347
348        assert_eq!(consumed, bytes.len());
349        assert_eq!(deserialized.len(), 3); // Only unique strings
350        assert_eq!(deserialized.get(0), Some("foo"));
351        assert_eq!(deserialized.get(1), Some("bar"));
352        assert_eq!(deserialized.get(2), Some("baz"));
353    }
354
355    #[test]
356    fn test_empty_pool() {
357        let pool = InternPool::new();
358        let bytes = pool.serialize();
359
360        let (deserialized, consumed) = InternPool::deserialize(&bytes).unwrap();
361        assert_eq!(consumed, 8);
362        assert!(deserialized.is_empty());
363    }
364
365    #[test]
366    fn test_interning_serializer() {
367        let mut serializer = InterningSerializer::new();
368
369        let idx1 = serializer.intern("test");
370        let idx2 = serializer.intern("data");
371        let idx3 = serializer.intern("test");
372
373        assert_eq!(idx1, 0);
374        assert_eq!(idx2, 1);
375        assert_eq!(idx3, 0);
376
377        let bytes = serializer.serialize_pool();
378        let (deserializer, _) = InterningDeserializer::new(&bytes).unwrap();
379
380        assert_eq!(deserializer.get(0), Some("test"));
381        assert_eq!(deserializer.get(1), Some("data"));
382    }
383
384    #[test]
385    fn test_unicode_strings() {
386        let mut pool = InternPool::new();
387        pool.intern("Hello 世界");
388        pool.intern("🦀 Rust");
389        pool.intern("Привет");
390
391        let bytes = pool.serialize();
392        let (deserialized, _) = InternPool::deserialize(&bytes).unwrap();
393
394        assert_eq!(deserialized.get(0), Some("Hello 世界"));
395        assert_eq!(deserialized.get(1), Some("🦀 Rust"));
396        assert_eq!(deserialized.get(2), Some("Привет"));
397    }
398
399    #[test]
400    fn test_large_pool() {
401        let mut pool = InternPool::new();
402
403        // Add 1000 unique strings
404        for i in 0..1000 {
405            pool.intern(&format!("string_{}", i));
406        }
407
408        // Add duplicates
409        for i in 0..500 {
410            pool.intern(&format!("string_{}", i));
411        }
412
413        assert_eq!(pool.len(), 1000); // Only unique strings
414
415        let bytes = pool.serialize();
416        let (deserialized, _) = InternPool::deserialize(&bytes).unwrap();
417
418        assert_eq!(deserialized.len(), 1000);
419        for i in 0..1000 {
420            assert_eq!(deserialized.get(i), Some(format!("string_{}", i).as_str()));
421        }
422    }
423}