faf_rust_sdk/binary/
string_table.rs1use std::collections::HashMap;
16
17use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
18
19use super::error::{FafbError, FafbResult};
20
21pub const MAX_STRING_TABLE_ENTRIES: usize = 256;
23
24pub const MAX_STRING_LENGTH: usize = 255;
26
27#[derive(Debug, Clone, Default)]
29pub struct StringTable {
30 entries: Vec<String>,
32 index_map: HashMap<String, u8>,
34}
35
36impl StringTable {
37 pub fn new() -> Self {
39 Self {
40 entries: Vec::new(),
41 index_map: HashMap::new(),
42 }
43 }
44
45 pub fn add(&mut self, name: &str) -> FafbResult<u8> {
48 if let Some(&idx) = self.index_map.get(name) {
50 return Ok(idx);
51 }
52
53 if self.entries.len() >= MAX_STRING_TABLE_ENTRIES {
55 return Err(FafbError::StringTableFull {
56 max: MAX_STRING_TABLE_ENTRIES,
57 });
58 }
59 if name.len() > MAX_STRING_LENGTH {
60 return Err(FafbError::StringTableEntryTooLong {
61 length: name.len(),
62 max: MAX_STRING_LENGTH,
63 });
64 }
65
66 let idx = self.entries.len() as u8;
67 self.entries.push(name.to_string());
68 self.index_map.insert(name.to_string(), idx);
69 Ok(idx)
70 }
71
72 pub fn get(&self, index: u8) -> FafbResult<&str> {
74 self.entries.get(index as usize).map(|s| s.as_str()).ok_or(
75 FafbError::StringTableIndexOutOfBounds {
76 index,
77 count: self.entries.len() as u16,
78 },
79 )
80 }
81
82 pub fn index_of(&self, name: &str) -> Option<u8> {
84 self.index_map.get(name).copied()
85 }
86
87 pub fn len(&self) -> usize {
89 self.entries.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.entries.is_empty()
95 }
96
97 pub fn entries(&self) -> &[String] {
99 &self.entries
100 }
101
102 pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
106 let mut buf = Vec::new();
107 buf.write_u16::<LittleEndian>(self.entries.len() as u16)?;
108 for entry in &self.entries {
109 buf.write_u16::<LittleEndian>(entry.len() as u16)?;
110 buf.extend_from_slice(entry.as_bytes());
111 }
112 Ok(buf)
113 }
114
115 pub fn from_bytes(data: &[u8]) -> FafbResult<Self> {
117 if data.len() < 2 {
118 return Err(FafbError::FileTooSmall {
119 expected: 2,
120 actual: data.len(),
121 });
122 }
123
124 let mut cursor = std::io::Cursor::new(data);
125 let count = cursor.read_u16::<LittleEndian>()? as usize;
126
127 if count > MAX_STRING_TABLE_ENTRIES {
128 return Err(FafbError::StringTableFull {
129 max: MAX_STRING_TABLE_ENTRIES,
130 });
131 }
132
133 let mut table = Self::new();
134 for _ in 0..count {
135 let len = cursor.read_u16::<LittleEndian>()? as usize;
136 if len > MAX_STRING_LENGTH {
137 return Err(FafbError::StringTableEntryTooLong {
138 length: len,
139 max: MAX_STRING_LENGTH,
140 });
141 }
142
143 let pos = cursor.position() as usize;
144 if pos + len > data.len() {
145 return Err(FafbError::FileTooSmall {
146 expected: pos + len,
147 actual: data.len(),
148 });
149 }
150
151 let name = std::str::from_utf8(&data[pos..pos + len])
152 .map_err(|e| FafbError::InvalidUtf8(e.to_string()))?;
153 cursor.set_position((pos + len) as u64);
154
155 let idx = table.entries.len() as u8;
157 table.entries.push(name.to_string());
158 table.index_map.insert(name.to_string(), idx);
159 }
160
161 Ok(table)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_add_and_get() {
171 let mut table = StringTable::new();
172 let idx = table.add("project").unwrap();
173 assert_eq!(idx, 0);
174 assert_eq!(table.get(0).unwrap(), "project");
175 }
176
177 #[test]
178 fn test_dedup() {
179 let mut table = StringTable::new();
180 let idx1 = table.add("project").unwrap();
181 let idx2 = table.add("project").unwrap();
182 assert_eq!(idx1, idx2);
183 assert_eq!(table.len(), 1);
184 }
185
186 #[test]
187 fn test_multiple_entries() {
188 let mut table = StringTable::new();
189 assert_eq!(table.add("project").unwrap(), 0);
190 assert_eq!(table.add("tech_stack").unwrap(), 1);
191 assert_eq!(table.add("commands").unwrap(), 2);
192 assert_eq!(table.len(), 3);
193 }
194
195 #[test]
196 fn test_index_of() {
197 let mut table = StringTable::new();
198 table.add("project").unwrap();
199 table.add("commands").unwrap();
200 assert_eq!(table.index_of("project"), Some(0));
201 assert_eq!(table.index_of("commands"), Some(1));
202 assert_eq!(table.index_of("missing"), None);
203 }
204
205 #[test]
206 fn test_roundtrip() {
207 let mut table = StringTable::new();
208 table.add("project").unwrap();
209 table.add("tech_stack").unwrap();
210 table.add("docs").unwrap();
211
212 let bytes = table.to_bytes().unwrap();
213 let recovered = StringTable::from_bytes(&bytes).unwrap();
214
215 assert_eq!(recovered.len(), 3);
216 assert_eq!(recovered.get(0).unwrap(), "project");
217 assert_eq!(recovered.get(1).unwrap(), "tech_stack");
218 assert_eq!(recovered.get(2).unwrap(), "docs");
219 }
220
221 #[test]
222 fn test_max_entries() {
223 let mut table = StringTable::new();
224 for i in 0..256 {
225 table.add(&format!("key_{}", i)).unwrap();
226 }
227 assert_eq!(table.len(), 256);
228
229 let result = table.add("overflow");
231 assert!(matches!(result, Err(FafbError::StringTableFull { .. })));
232 }
233
234 #[test]
235 fn test_max_name_length() {
236 let mut table = StringTable::new();
237 let name_255 = "a".repeat(255);
239 assert!(table.add(&name_255).is_ok());
240
241 let name_256 = "b".repeat(256);
243 let result = table.add(&name_256);
244 assert!(matches!(
245 result,
246 Err(FafbError::StringTableEntryTooLong { .. })
247 ));
248 }
249
250 #[test]
251 fn test_unicode_names() {
252 let mut table = StringTable::new();
253 table.add("日本語").unwrap();
254 table.add("émojis").unwrap();
255
256 let bytes = table.to_bytes().unwrap();
257 let recovered = StringTable::from_bytes(&bytes).unwrap();
258
259 assert_eq!(recovered.get(0).unwrap(), "日本語");
260 assert_eq!(recovered.get(1).unwrap(), "émojis");
261 }
262
263 #[test]
264 fn test_empty_table_roundtrip() {
265 let table = StringTable::new();
266 let bytes = table.to_bytes().unwrap();
267 let recovered = StringTable::from_bytes(&bytes).unwrap();
268 assert_eq!(recovered.len(), 0);
269 assert!(recovered.is_empty());
270 }
271
272 #[test]
273 fn test_index_out_of_bounds() {
274 let table = StringTable::new();
275 let result = table.get(0);
276 assert!(matches!(
277 result,
278 Err(FafbError::StringTableIndexOutOfBounds { .. })
279 ));
280 }
281
282 #[test]
283 fn test_truncated_data() {
284 let mut table = StringTable::new();
285 table.add("hello").unwrap();
286 let bytes = table.to_bytes().unwrap();
287
288 let truncated = &bytes[..5]; let result = StringTable::from_bytes(truncated);
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn test_empty_string_name() {
296 let mut table = StringTable::new();
297 let idx = table.add("").unwrap();
298 assert_eq!(idx, 0);
299 assert_eq!(table.get(0).unwrap(), "");
300 }
301
302 #[test]
303 fn test_dedup_preserves_first_index() {
304 let mut table = StringTable::new();
305 table.add("a").unwrap();
306 table.add("b").unwrap();
307 table.add("c").unwrap();
308 assert_eq!(table.add("a").unwrap(), 0);
310 assert_eq!(table.len(), 3);
311 }
312
313 #[test]
314 fn test_serialized_size() {
315 let mut table = StringTable::new();
316 table.add("abc").unwrap(); table.add("de").unwrap(); let bytes = table.to_bytes().unwrap();
320 assert_eq!(bytes.len(), 11);
322 }
323
324 #[test]
325 fn test_too_small_data() {
326 let result = StringTable::from_bytes(&[0x01]);
328 assert!(matches!(result, Err(FafbError::FileTooSmall { .. })));
329 }
330}