Skip to main content

driven/binary/
string_table.rs

1//! Interned String Table
2//!
3//! O(1) string access with u32 indices - no length prefixes needed.
4
5use crate::{DrivenError, Result};
6use std::collections::HashMap;
7
8/// String ID (index into string table)
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10pub struct StringId(pub u32);
11
12impl StringId {
13    /// Null/empty string ID
14    pub const NULL: Self = Self(0);
15
16    /// Create from raw index
17    pub fn new(index: u32) -> Self {
18        Self(index)
19    }
20
21    /// Get raw index
22    pub fn index(self) -> u32 {
23        self.0
24    }
25
26    /// Check if null
27    pub fn is_null(self) -> bool {
28        self.0 == 0
29    }
30}
31
32/// String table for zero-copy string access
33#[derive(Debug)]
34pub struct StringTable<'a> {
35    /// Number of strings
36    count: u32,
37    /// Offset table (O(1) access)
38    offsets: &'a [u32],
39    /// Packed string data
40    data: &'a [u8],
41}
42
43impl<'a> StringTable<'a> {
44    /// Parse string table from bytes (zero-copy)
45    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self> {
46        if bytes.len() < 8 {
47            return Err(DrivenError::InvalidBinary("String table too small".into()));
48        }
49
50        let count = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
51        let total_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
52
53        let offset_table_size = (count as usize) * 4;
54        if bytes.len() < 8 + offset_table_size {
55            return Err(DrivenError::InvalidBinary(
56                "String table offset table truncated".into(),
57            ));
58        }
59
60        // Safety: We've verified the size
61        let offsets_bytes = &bytes[8..8 + offset_table_size];
62        let offsets: &[u32] = bytemuck::cast_slice(offsets_bytes);
63
64        let data_start = 8 + offset_table_size;
65        let data_end = data_start + total_size as usize;
66        if bytes.len() < data_end {
67            return Err(DrivenError::InvalidBinary(
68                "String table data truncated".into(),
69            ));
70        }
71
72        let data = &bytes[data_start..data_end];
73
74        Ok(Self {
75            count,
76            offsets,
77            data,
78        })
79    }
80
81    /// Get string by ID (O(1))
82    pub fn get(&self, id: StringId) -> Option<&'a str> {
83        if id.is_null() {
84            return Some("");
85        }
86
87        let idx = id.0 as usize;
88        if idx == 0 || idx > self.count as usize {
89            return None;
90        }
91
92        // Adjust for 1-based indexing
93        let idx = idx - 1;
94
95        let start = self.offsets[idx] as usize;
96        let end = if idx + 1 < self.count as usize {
97            self.offsets[idx + 1] as usize
98        } else {
99            self.data.len()
100        };
101
102        if end > self.data.len() || start > end {
103            return None;
104        }
105
106        std::str::from_utf8(&self.data[start..end]).ok()
107    }
108
109    /// Number of strings
110    pub fn len(&self) -> usize {
111        self.count as usize
112    }
113
114    /// Check if empty
115    pub fn is_empty(&self) -> bool {
116        self.count == 0
117    }
118
119    /// Iterate over all strings
120    pub fn iter(&self) -> impl Iterator<Item = (StringId, &'a str)> + '_ {
121        (1..=self.count).filter_map(move |i| {
122            let id = StringId(i);
123            self.get(id).map(|s| (id, s))
124        })
125    }
126}
127
128/// Builder for creating string tables
129#[derive(Debug, Default)]
130pub struct StringTableBuilder {
131    /// Strings to intern
132    strings: Vec<String>,
133    /// Deduplication map
134    index: HashMap<String, StringId>,
135}
136
137impl StringTableBuilder {
138    /// Create a new builder
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Intern a string, returning its ID
144    pub fn intern(&mut self, s: &str) -> StringId {
145        if s.is_empty() {
146            return StringId::NULL;
147        }
148
149        if let Some(&id) = self.index.get(s) {
150            return id;
151        }
152
153        let id = StringId((self.strings.len() + 1) as u32);
154        self.strings.push(s.to_string());
155        self.index.insert(s.to_string(), id);
156        id
157    }
158
159    /// Get ID for existing string (if interned)
160    pub fn get(&self, s: &str) -> Option<StringId> {
161        if s.is_empty() {
162            return Some(StringId::NULL);
163        }
164        self.index.get(s).copied()
165    }
166
167    /// Number of interned strings
168    pub fn len(&self) -> usize {
169        self.strings.len()
170    }
171
172    /// Check if empty
173    pub fn is_empty(&self) -> bool {
174        self.strings.is_empty()
175    }
176
177    /// Build the binary string table
178    pub fn build(&self) -> Vec<u8> {
179        let count = self.strings.len() as u32;
180
181        // Calculate total string data size
182        let total_size: usize = self.strings.iter().map(|s| s.len()).sum();
183
184        // Build offset table
185        let mut offsets = Vec::with_capacity(self.strings.len());
186        let mut offset = 0u32;
187        for s in &self.strings {
188            offsets.push(offset);
189            offset += s.len() as u32;
190        }
191
192        // Build output
193        let mut output = Vec::with_capacity(8 + offsets.len() * 4 + total_size);
194
195        // Header
196        output.extend_from_slice(&count.to_le_bytes());
197        output.extend_from_slice(&(total_size as u32).to_le_bytes());
198
199        // Offset table
200        for off in &offsets {
201            output.extend_from_slice(&off.to_le_bytes());
202        }
203
204        // String data
205        for s in &self.strings {
206            output.extend_from_slice(s.as_bytes());
207        }
208
209        output
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_string_id_null() {
219        assert!(StringId::NULL.is_null());
220        assert!(!StringId(1).is_null());
221    }
222
223    #[test]
224    fn test_builder_intern() {
225        let mut builder = StringTableBuilder::new();
226
227        let id1 = builder.intern("hello");
228        let id2 = builder.intern("world");
229        let id3 = builder.intern("hello"); // Duplicate
230
231        assert_eq!(id1, id3); // Same ID for duplicate
232        assert_ne!(id1, id2);
233        assert_eq!(builder.len(), 2);
234    }
235
236    #[test]
237    fn test_roundtrip() {
238        let mut builder = StringTableBuilder::new();
239        let id1 = builder.intern("hello");
240        let id2 = builder.intern("world");
241        let id3 = builder.intern("test");
242
243        let bytes = builder.build();
244        let table = StringTable::from_bytes(&bytes).unwrap();
245
246        assert_eq!(table.len(), 3);
247        assert_eq!(table.get(id1), Some("hello"));
248        assert_eq!(table.get(id2), Some("world"));
249        assert_eq!(table.get(id3), Some("test"));
250        assert_eq!(table.get(StringId::NULL), Some(""));
251    }
252
253    #[test]
254    fn test_empty_string() {
255        let builder = StringTableBuilder::new();
256        let id = builder.get("").unwrap();
257        assert!(id.is_null());
258    }
259}